Spaces:
Running
feat: OpenEEGBench submission flow (arena runs the eval)
Browse filesReplace the leftover LLM-leaderboard submission with a first-class
OpenEEGBench flow. Users submit a model reference (model_cls + checkpoint
+ finetuning strategy/head/datasets); the backend validates it, enforces a
5-per-month per-user cap, and queues a request to braindecode/requests. A
worker (stub) runs oeb.benchmark() and maps results to braindecode/contents.
Backend:
- submission_config: OEB enums/defaults (single source of truth)
- submissions: pure validation, request building, monthly cap
- results_mapping: OEB DataFrame -> contents schema (balanced_accuracy)
- model_validation: EEG/OEB weights-source + license checks
- OEB-shaped submit endpoint/service; eval worker stub (run_eval_worker.py)
Frontend:
- OEB submission form (model_cls, weights source, strategies, heads, PEFT
modules, datasets, seeds, contact/visibility) with conditional fields
- rewritten submission guide (5/month, help -> OpenEEGBench issues, citation)
- queue/limit copy aligned; fixed duplicate style key in EvaluationQueues
Tests: submission validation + results mapping (16 passing).
- backend/app/api/endpoints/models.py +4 -4
- backend/app/config/benchmarks.py +1 -0
- backend/app/config/submission_config.py +35 -0
- backend/app/services/models.py +52 -181
- backend/app/services/results_mapping.py +71 -0
- backend/app/services/submissions.py +129 -0
- backend/app/utils/model_validation.py +28 -0
- backend/scripts/run_eval_worker.py +69 -0
- backend/tests/__init__.py +0 -0
- backend/tests/test_results_mapping.py +55 -0
- backend/tests/test_submissions.py +92 -0
- frontend/src/pages/AddModelPage/components/EvaluationQueues/EvaluationQueues.js +12 -10
- frontend/src/pages/AddModelPage/components/ModelSubmissionForm/ModelSubmissionForm.js +481 -116
- frontend/src/pages/AddModelPage/components/SubmissionGuide/SubmissionGuide.js +111 -57
- frontend/src/pages/AddModelPage/components/SubmissionLimitChecker/SubmissionLimitChecker.js +5 -4
- frontend/src/pages/LeaderboardPage/components/Leaderboard/constants/modelTypes.js +24 -0
|
@@ -60,11 +60,11 @@ async def submit_model(
|
|
| 60 |
|
| 61 |
# Log submission details
|
| 62 |
submission_info = {
|
| 63 |
-
"
|
|
|
|
| 64 |
"User": user_id,
|
| 65 |
-
"
|
| 66 |
-
"
|
| 67 |
-
"Model_Type": model_data.get("model_type")
|
| 68 |
}
|
| 69 |
for line in LogFormatter.tree(submission_info, "Submission Details"):
|
| 70 |
logger.info(line)
|
|
|
|
| 60 |
|
| 61 |
# Log submission details
|
| 62 |
submission_info = {
|
| 63 |
+
"Model_Class": model_data.get("model_cls"),
|
| 64 |
+
"Model_Name": model_data.get("model_name"),
|
| 65 |
"User": user_id,
|
| 66 |
+
"Strategies": model_data.get("finetuning_strategies"),
|
| 67 |
+
"Datasets": model_data.get("datasets"),
|
|
|
|
| 68 |
}
|
| 69 |
for line in LogFormatter.tree(submission_info, "Submission Details"):
|
| 70 |
logger.info(line)
|
|
@@ -22,6 +22,7 @@ class BenchmarkInfo:
|
|
| 22 |
accuracy_field: str
|
| 23 |
split: str = "test"
|
| 24 |
config: str = "default"
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
EEG_BENCHMARKS: Dict[str, BenchmarkInfo] = {
|
|
|
|
| 22 |
accuracy_field: str
|
| 23 |
split: str = "test"
|
| 24 |
config: str = "default"
|
| 25 |
+
metric: str = "balanced_accuracy" # OEB test metric; arena uses balanced_accuracy
|
| 26 |
|
| 27 |
|
| 28 |
EEG_BENCHMARKS: Dict[str, BenchmarkInfo] = {
|
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenEEGBench submission constants — single source of truth for the
|
| 2 |
+
submission form, validators, and the eval worker. Mirrors the accepted
|
| 3 |
+
values of open_eeg_bench.benchmark()."""
|
| 4 |
+
|
| 5 |
+
from app.config.benchmarks import EEG_BENCHMARKS
|
| 6 |
+
|
| 7 |
+
# Finetuning strategies accepted by oeb.benchmark()
|
| 8 |
+
FINETUNING_STRATEGIES = [
|
| 9 |
+
"frozen", "ridge_probe", "lora", "ia3", "adalora",
|
| 10 |
+
"dora", "oft", "full_finetune", "two_stages",
|
| 11 |
+
]
|
| 12 |
+
# Subset requiring peft_target_modules
|
| 13 |
+
PEFT_STRATEGIES = {"lora", "ia3", "adalora", "dora", "oft"}
|
| 14 |
+
IA3_STRATEGY = "ia3" # additionally requires peft_ff_modules
|
| 15 |
+
|
| 16 |
+
HEADS = ["linear_head", "mlp_head", "original_head"]
|
| 17 |
+
WEIGHTS_SOURCES = ["hub_repo", "checkpoint_url"]
|
| 18 |
+
|
| 19 |
+
# The smaller, classification-only dataset set the arena evaluates on.
|
| 20 |
+
DATASET_KEYS = list(EEG_BENCHMARKS.keys())
|
| 21 |
+
# hf_id (e.g. "braindecode/bcic2a") -> accuracy field (e.g. "bcic2a_accuracy")
|
| 22 |
+
DATASET_ID_TO_ACCURACY_FIELD = {
|
| 23 |
+
b.dataset_id: b.accuracy_field for b in EEG_BENCHMARKS.values()
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
# Defaults (linear probing)
|
| 27 |
+
DEFAULT_STRATEGIES = ["frozen"]
|
| 28 |
+
DEFAULT_HEADS = ["linear_head"]
|
| 29 |
+
DEFAULT_N_SEEDS = 3
|
| 30 |
+
DEFAULT_HEAD_MODULE_NAME = "final_layer"
|
| 31 |
+
|
| 32 |
+
# Submission cap (per user, per calendar month)
|
| 33 |
+
MONTHLY_QUOTA = 5
|
| 34 |
+
|
| 35 |
+
FRAMEWORK = "open-eeg-bench"
|
|
@@ -22,6 +22,7 @@ from app.config import (
|
|
| 22 |
)
|
| 23 |
from app.config.hf_config import HF_ORGANIZATION
|
| 24 |
from app.services.hf_service import HuggingFaceService
|
|
|
|
| 25 |
from app.utils.model_validation import ModelValidator
|
| 26 |
from app.services.votes import VoteService
|
| 27 |
from app.core.cache import cache_config
|
|
@@ -322,193 +323,63 @@ class ModelService(HuggingFaceService):
|
|
| 322 |
logger.info(LogFormatter.info(f"Using cached data ({cache_age:.1f}s old)"))
|
| 323 |
return self.cached_models
|
| 324 |
|
| 325 |
-
async def submit_model(
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
"
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
]
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
model_info = self.hf_api.model_info(
|
| 356 |
-
model_data["model_id"],
|
| 357 |
-
revision=model_data["revision"],
|
| 358 |
-
token=self.token
|
| 359 |
-
)
|
| 360 |
-
|
| 361 |
-
if not model_info:
|
| 362 |
-
raise Exception(f"Model {model_data['model_id']} not found on HuggingFace Hub")
|
| 363 |
-
|
| 364 |
-
logger.info(LogFormatter.success("Model exists on HuggingFace Hub"))
|
| 365 |
-
|
| 366 |
-
except Exception as e:
|
| 367 |
-
logger.error(LogFormatter.error("Model validation failed", e))
|
| 368 |
-
raise
|
| 369 |
-
|
| 370 |
-
# Update model revision with commit sha
|
| 371 |
-
model_data["revision"] = model_info.sha
|
| 372 |
-
|
| 373 |
-
# Check if model already exists in the system
|
| 374 |
try:
|
| 375 |
-
logger.info(LogFormatter.subsection("CHECKING EXISTING SUBMISSIONS"))
|
| 376 |
-
existing_models = await self.get_models()
|
| 377 |
-
|
| 378 |
-
# Check in all statuses (pending, evaluating, finished)
|
| 379 |
-
for status, models in existing_models.items():
|
| 380 |
-
for model in models:
|
| 381 |
-
if model["name"] == model_data["model_id"] and model["revision"] == model_data["revision"]:
|
| 382 |
-
error_msg = f"Model {model_data['model_id']} revision {model_data['revision']} is already in the system with status: {status}"
|
| 383 |
-
logger.error(LogFormatter.error("Submission rejected", error_msg))
|
| 384 |
-
raise ValueError(error_msg)
|
| 385 |
-
|
| 386 |
-
logger.info(LogFormatter.success("No existing submission found"))
|
| 387 |
-
except ValueError:
|
| 388 |
-
raise
|
| 389 |
-
except Exception as e:
|
| 390 |
-
logger.error(LogFormatter.error("Failed to check existing submissions", e))
|
| 391 |
-
raise
|
| 392 |
-
|
| 393 |
-
# Check that model on hub and valid
|
| 394 |
-
valid, error, model_config = await self.validator.is_model_on_hub(
|
| 395 |
-
model_data["model_id"],
|
| 396 |
-
model_data["revision"],
|
| 397 |
-
test_tokenizer=False
|
| 398 |
-
)
|
| 399 |
-
if not valid:
|
| 400 |
-
logger.error(LogFormatter.error("Model on hub validation failed", error))
|
| 401 |
-
raise Exception(error)
|
| 402 |
-
logger.info(LogFormatter.success("Model on hub validation passed"))
|
| 403 |
-
|
| 404 |
-
# Validate model card
|
| 405 |
-
valid, error, model_card = await self.validator.check_model_card(
|
| 406 |
-
model_data["model_id"]
|
| 407 |
-
)
|
| 408 |
-
if not valid:
|
| 409 |
-
logger.error(LogFormatter.error("Model card validation failed", error))
|
| 410 |
-
raise Exception(error)
|
| 411 |
-
logger.info(LogFormatter.success("Model card validation passed"))
|
| 412 |
-
|
| 413 |
-
# Check size limits
|
| 414 |
-
model_size, error = await self.validator.get_model_size(
|
| 415 |
-
model_info,
|
| 416 |
-
model_data["precision"],
|
| 417 |
-
model_data["base_model"],
|
| 418 |
-
revision=model_data["revision"]
|
| 419 |
-
)
|
| 420 |
-
if model_size is None:
|
| 421 |
-
logger.error(LogFormatter.error("Model size validation failed", error))
|
| 422 |
-
raise Exception(error)
|
| 423 |
-
logger.info(LogFormatter.success(f"Model size validation passed: {model_size:.1f}B"))
|
| 424 |
-
|
| 425 |
-
# Size limits for EEG models (much smaller than LLMs - limit at 10B params)
|
| 426 |
-
if model_data["precision"] in ["float16", "bfloat16"] and model_size > 10:
|
| 427 |
-
error_msg = f"Model too large for {model_data['precision']} (limit: 10B)"
|
| 428 |
-
logger.error(LogFormatter.error("Size limit exceeded", error_msg))
|
| 429 |
-
raise Exception(error_msg)
|
| 430 |
-
|
| 431 |
-
architectures = model_info.config.get("architectures", "")
|
| 432 |
-
if architectures:
|
| 433 |
-
architectures = ";".join(architectures)
|
| 434 |
-
|
| 435 |
-
# Create eval entry
|
| 436 |
-
eval_entry = {
|
| 437 |
-
"model": model_data["model_id"],
|
| 438 |
-
"base_model": model_data["base_model"],
|
| 439 |
-
"revision": model_info.sha,
|
| 440 |
-
"precision": model_data["precision"],
|
| 441 |
-
"params": model_size,
|
| 442 |
-
"architectures": architectures,
|
| 443 |
-
"status": "PENDING",
|
| 444 |
-
"submitted_time": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 445 |
-
"model_type": model_data["model_type"],
|
| 446 |
-
"job_id": -1,
|
| 447 |
-
"job_start_time": None,
|
| 448 |
-
"sender": user_id
|
| 449 |
-
}
|
| 450 |
-
|
| 451 |
-
logger.info(LogFormatter.subsection("EVALUATION ENTRY"))
|
| 452 |
-
for line in LogFormatter.tree(eval_entry):
|
| 453 |
-
logger.info(line)
|
| 454 |
-
|
| 455 |
-
# Upload to HF dataset
|
| 456 |
-
try:
|
| 457 |
-
logger.info(LogFormatter.subsection("UPLOADING TO HUGGINGFACE"))
|
| 458 |
-
logger.info(LogFormatter.info(f"Uploading to {HF_ORGANIZATION}/requests..."))
|
| 459 |
-
|
| 460 |
-
# Construct the path in the dataset
|
| 461 |
-
org_or_user = model_data["model_id"].split("/")[0] if "/" in model_data["model_id"] else ""
|
| 462 |
-
model_path = model_data["model_id"].split("/")[-1]
|
| 463 |
-
relative_path = f"{org_or_user}/{model_path}_eval_request_False_{model_data['precision']}.json"
|
| 464 |
-
|
| 465 |
-
# Create a temporary file with the request
|
| 466 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as temp_file:
|
| 467 |
-
json.dump(eval_entry, temp_file, indent=2)
|
| 468 |
-
temp_file.flush()
|
| 469 |
-
temp_path = temp_file.name
|
| 470 |
-
|
| 471 |
-
# Upload file directly
|
| 472 |
self.hf_api.upload_file(
|
| 473 |
-
path_or_fileobj=
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
commit_message=f"Add {model_data['model_id']} to eval queue",
|
| 478 |
-
token=self.token
|
| 479 |
)
|
|
|
|
|
|
|
| 480 |
|
| 481 |
-
|
| 482 |
-
os.unlink(temp_path)
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
except Exception as e:
|
| 487 |
-
logger.error(LogFormatter.error("Upload failed", e))
|
| 488 |
-
raise
|
| 489 |
-
|
| 490 |
-
# Add automatic vote
|
| 491 |
try:
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
except Exception as e:
|
| 505 |
-
logger.error(LogFormatter.error("Failed to record vote", e))
|
| 506 |
-
# Don't raise here as the main submission was successful
|
| 507 |
-
|
| 508 |
-
return {
|
| 509 |
-
"status": "success",
|
| 510 |
-
"message": "The model was submitted successfully, and the vote has been recorded"
|
| 511 |
-
}
|
| 512 |
|
| 513 |
async def get_model_status(self, model_id: str) -> Dict[str, Any]:
|
| 514 |
"""Get evaluation status of a model"""
|
|
|
|
| 22 |
)
|
| 23 |
from app.config.hf_config import HF_ORGANIZATION
|
| 24 |
from app.services.hf_service import HuggingFaceService
|
| 25 |
+
from app.services.submissions import validate_submission, build_request_entry, monthly_quota_exceeded
|
| 26 |
from app.utils.model_validation import ModelValidator
|
| 27 |
from app.services.votes import VoteService
|
| 28 |
from app.core.cache import cache_config
|
|
|
|
| 323 |
logger.info(LogFormatter.info(f"Using cached data ({cache_age:.1f}s old)"))
|
| 324 |
return self.cached_models
|
| 325 |
|
| 326 |
+
async def submit_model(self, model_data: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 327 |
+
logger.info(LogFormatter.section("OPENEEGBENCH SUBMISSION"))
|
| 328 |
+
|
| 329 |
+
errors = validate_submission(model_data)
|
| 330 |
+
if errors:
|
| 331 |
+
raise ValueError("; ".join(errors))
|
| 332 |
+
|
| 333 |
+
# Monthly cap: count this user's existing queue entries for the current month.
|
| 334 |
+
existing = await self._load_request_entries()
|
| 335 |
+
if monthly_quota_exceeded(existing, user_id):
|
| 336 |
+
raise ValueError("You have reached the limit of 5 submissions this month. Please try again next month.")
|
| 337 |
+
|
| 338 |
+
# Validate weights source (hub repo exists / url well-formed) and grab license.
|
| 339 |
+
kwargs = model_data
|
| 340 |
+
ok, err = await self.validator.validate_weights_source(
|
| 341 |
+
(kwargs.get("hub_repo") or "").strip(), (kwargs.get("checkpoint_url") or "").strip())
|
| 342 |
+
if not ok:
|
| 343 |
+
raise ValueError(err)
|
| 344 |
+
|
| 345 |
+
entry = build_request_entry(model_data, user_id)
|
| 346 |
+
hub_repo = entry["benchmark_kwargs"].get("hub_repo")
|
| 347 |
+
if hub_repo:
|
| 348 |
+
entry["hub_license"] = await self.validator.get_repo_license(hub_repo)
|
| 349 |
+
|
| 350 |
+
# Write request JSON to the queue dataset.
|
| 351 |
+
slug = entry["model_name"].lower().replace("/", "-").replace(" ", "-")
|
| 352 |
+
relative_path = f"{user_id}/{slug}_eval_request_{entry['visibility']}.json"
|
| 353 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
| 354 |
+
json.dump(entry, f, indent=2)
|
| 355 |
+
tmp = f.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
self.hf_api.upload_file(
|
| 358 |
+
path_or_fileobj=tmp, path_in_repo=relative_path,
|
| 359 |
+
repo_id=QUEUE_REPO, repo_type="dataset",
|
| 360 |
+
commit_message=f"Add {entry['model_name']} to OpenEEGBench eval queue",
|
| 361 |
+
token=self.token,
|
|
|
|
|
|
|
| 362 |
)
|
| 363 |
+
finally:
|
| 364 |
+
os.unlink(tmp)
|
| 365 |
|
| 366 |
+
return {"status": "success", "message": "Submission queued for OpenEEGBench evaluation."}
|
|
|
|
| 367 |
|
| 368 |
+
async def _load_request_entries(self) -> List[Dict[str, Any]]:
|
| 369 |
+
"""Load all queue request JSONs (for the monthly cap). Empty on failure."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
try:
|
| 371 |
+
with suppress_output():
|
| 372 |
+
local_dir = self.hf_api.snapshot_download(
|
| 373 |
+
repo_id=QUEUE_REPO, repo_type="dataset", token=self.token)
|
| 374 |
+
out = []
|
| 375 |
+
for p in Path(local_dir).glob("**/*.json"):
|
| 376 |
+
try:
|
| 377 |
+
out.append(json.loads(p.read_text()))
|
| 378 |
+
except Exception:
|
| 379 |
+
continue
|
| 380 |
+
return out
|
| 381 |
+
except Exception:
|
| 382 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
async def get_model_status(self, model_id: str) -> Dict[str, Any]:
|
| 385 |
"""Get evaluation status of a model"""
|
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure mapping from an OpenEEGBench results DataFrame to the
|
| 2 |
+
`braindecode/contents` leaderboard schema. No network, no oeb import."""
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
|
| 7 |
+
from app.config.submission_config import DATASET_ID_TO_ACCURACY_FIELD
|
| 8 |
+
|
| 9 |
+
AVERAGE_FIELD = "Average ⬆️" # "Average ⬆️"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _submitted_date(request: Dict[str, Any]) -> str:
|
| 13 |
+
ts = str(request.get("submitted_time", ""))
|
| 14 |
+
return ts.split("T", 1)[0] if "T" in ts else ts
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def map_oeb_results_to_contents(df, request: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 18 |
+
"""One contents row per finetuning strategy present in `df`."""
|
| 19 |
+
if df is None or len(df) == 0:
|
| 20 |
+
return []
|
| 21 |
+
|
| 22 |
+
kwargs = request.get("benchmark_kwargs", {})
|
| 23 |
+
hub_repo = kwargs.get("hub_repo")
|
| 24 |
+
model_cls = kwargs.get("model_cls", "")
|
| 25 |
+
arch_fallback = model_cls.split(".")[-1] if model_cls else ""
|
| 26 |
+
date = _submitted_date(request)
|
| 27 |
+
|
| 28 |
+
completed = df[(df.get("status") == "completed") & df["test_balanced_accuracy"].notna()] \
|
| 29 |
+
if "status" in df.columns else df[df["test_balanced_accuracy"].notna()]
|
| 30 |
+
|
| 31 |
+
rows: List[Dict[str, Any]] = []
|
| 32 |
+
for strategy, grp in completed.groupby("finetuning"):
|
| 33 |
+
backbone = grp["backbone"].iloc[0] if "backbone" in grp.columns else arch_fallback
|
| 34 |
+
# mean balanced_accuracy across seeds, per dataset (hf_id)
|
| 35 |
+
per_dataset = grp.groupby("dataset")["test_balanced_accuracy"].mean()
|
| 36 |
+
|
| 37 |
+
accuracies: Dict[str, float] = {}
|
| 38 |
+
for hf_id, score in per_dataset.items():
|
| 39 |
+
field = DATASET_ID_TO_ACCURACY_FIELD.get(hf_id)
|
| 40 |
+
if field is not None:
|
| 41 |
+
accuracies[field] = float(score)
|
| 42 |
+
|
| 43 |
+
avg_pct = round(sum(accuracies.values()) / len(accuracies) * 100, 2) if accuracies else 0.0
|
| 44 |
+
n_params_m = None
|
| 45 |
+
if "trainable_params" in grp.columns and grp["trainable_params"].notna().any():
|
| 46 |
+
n_params_m = round(float(grp["trainable_params"].dropna().iloc[0]) / 1e6, 2)
|
| 47 |
+
|
| 48 |
+
row: Dict[str, Any] = {
|
| 49 |
+
"fullname": request.get("model_name", ""),
|
| 50 |
+
"adapter": str(strategy),
|
| 51 |
+
"Precision": "",
|
| 52 |
+
"Model sha": None,
|
| 53 |
+
"Architecture": backbone or arch_fallback,
|
| 54 |
+
AVERAGE_FIELD: avg_pct,
|
| 55 |
+
"#Params (M)": n_params_m,
|
| 56 |
+
"Available on the hub": bool(hub_repo),
|
| 57 |
+
"Upload To Hub Date": date,
|
| 58 |
+
"Submission Date": date,
|
| 59 |
+
"Base Model": backbone or arch_fallback,
|
| 60 |
+
"Hub License": request.get("hub_license", ""),
|
| 61 |
+
"Hub ❤️": 0,
|
| 62 |
+
"model_url": request.get("model_url", ""),
|
| 63 |
+
"paper_url": request.get("paper_url", ""),
|
| 64 |
+
}
|
| 65 |
+
# Initialise every benchmark field to 0, then fill the ones we have.
|
| 66 |
+
for field in DATASET_ID_TO_ACCURACY_FIELD.values():
|
| 67 |
+
row.setdefault(field, 0)
|
| 68 |
+
row.update(accuracies)
|
| 69 |
+
rows.append(row)
|
| 70 |
+
|
| 71 |
+
return rows
|
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure helpers to validate and build OpenEEGBench eval submissions."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import re
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
from typing import Any, Dict, List
|
| 7 |
+
|
| 8 |
+
from app.config.submission_config import (
|
| 9 |
+
FINETUNING_STRATEGIES, PEFT_STRATEGIES, IA3_STRATEGY, HEADS,
|
| 10 |
+
DATASET_KEYS, DEFAULT_STRATEGIES, DEFAULT_HEADS, DEFAULT_N_SEEDS,
|
| 11 |
+
DEFAULT_HEAD_MODULE_NAME, MONTHLY_QUOTA, FRAMEWORK,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
_DOTTED_RE = re.compile(r"^[A-Za-z_]\w*(\.[A-Za-z_]\w*)+$")
|
| 15 |
+
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _as_list(value) -> list:
|
| 19 |
+
if value is None:
|
| 20 |
+
return []
|
| 21 |
+
return value if isinstance(value, list) else [value]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _str(payload, key) -> str:
|
| 25 |
+
return (payload.get(key) or "").strip()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def validate_submission(payload: Dict[str, Any]) -> List[str]:
|
| 29 |
+
"""Return human-readable validation errors (empty list == valid)."""
|
| 30 |
+
errors: List[str] = []
|
| 31 |
+
|
| 32 |
+
model_cls = _str(payload, "model_cls")
|
| 33 |
+
if not model_cls:
|
| 34 |
+
errors.append("model_cls is required (dotted path, e.g. braindecode.models.BIOT).")
|
| 35 |
+
elif not _DOTTED_RE.match(model_cls):
|
| 36 |
+
errors.append(f"model_cls '{model_cls}' is not a valid dotted import path.")
|
| 37 |
+
|
| 38 |
+
if not _str(payload, "model_name"):
|
| 39 |
+
errors.append("model_name is required.")
|
| 40 |
+
if not _str(payload, "organization"):
|
| 41 |
+
errors.append("organization is required.")
|
| 42 |
+
|
| 43 |
+
hub_repo = _str(payload, "hub_repo")
|
| 44 |
+
checkpoint_url = _str(payload, "checkpoint_url")
|
| 45 |
+
if not hub_repo and not checkpoint_url:
|
| 46 |
+
errors.append("Provide a weights source: either hub_repo or checkpoint_url.")
|
| 47 |
+
if hub_repo and checkpoint_url:
|
| 48 |
+
errors.append("Provide only one weights source (hub_repo or checkpoint_url).")
|
| 49 |
+
|
| 50 |
+
strategies = _as_list(payload.get("finetuning_strategies")) or DEFAULT_STRATEGIES
|
| 51 |
+
bad = [s for s in strategies if s not in FINETUNING_STRATEGIES]
|
| 52 |
+
if bad:
|
| 53 |
+
errors.append(f"Unknown finetuning strategies: {bad}. Valid: {FINETUNING_STRATEGIES}.")
|
| 54 |
+
|
| 55 |
+
heads = _as_list(payload.get("heads")) or DEFAULT_HEADS
|
| 56 |
+
bad = [h for h in heads if h not in HEADS]
|
| 57 |
+
if bad:
|
| 58 |
+
errors.append(f"Unknown heads: {bad}. Valid: {HEADS}.")
|
| 59 |
+
|
| 60 |
+
datasets = _as_list(payload.get("datasets")) or DATASET_KEYS
|
| 61 |
+
bad = [d for d in datasets if d not in DATASET_KEYS]
|
| 62 |
+
if bad:
|
| 63 |
+
errors.append(f"Unknown datasets: {bad}. Valid: {DATASET_KEYS}.")
|
| 64 |
+
|
| 65 |
+
if any(s in PEFT_STRATEGIES for s in strategies) and not _as_list(payload.get("peft_target_modules")):
|
| 66 |
+
errors.append("peft_target_modules is required when a PEFT strategy (lora/ia3/adalora/dora/oft) is selected.")
|
| 67 |
+
if IA3_STRATEGY in strategies and not _as_list(payload.get("peft_ff_modules")):
|
| 68 |
+
errors.append("peft_ff_modules is required when the ia3 strategy is selected.")
|
| 69 |
+
|
| 70 |
+
n_seeds = payload.get("n_seeds", DEFAULT_N_SEEDS)
|
| 71 |
+
if not isinstance(n_seeds, int) or isinstance(n_seeds, bool) or n_seeds < 1:
|
| 72 |
+
errors.append("n_seeds must be an integer >= 1.")
|
| 73 |
+
|
| 74 |
+
email = _str(payload, "contact_email")
|
| 75 |
+
if email and not _EMAIL_RE.match(email):
|
| 76 |
+
errors.append("contact_email is not a valid email address.")
|
| 77 |
+
|
| 78 |
+
if payload.get("visibility", "Public") not in ("Public", "Private"):
|
| 79 |
+
errors.append("visibility must be 'Public' or 'Private'.")
|
| 80 |
+
|
| 81 |
+
return errors
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def build_request_entry(payload: Dict[str, Any], submitter: str, now: datetime | None = None) -> Dict[str, Any]:
|
| 85 |
+
now = now or datetime.now(timezone.utc)
|
| 86 |
+
strategies = _as_list(payload.get("finetuning_strategies")) or DEFAULT_STRATEGIES
|
| 87 |
+
heads = _as_list(payload.get("heads")) or DEFAULT_HEADS
|
| 88 |
+
datasets = _as_list(payload.get("datasets")) or DATASET_KEYS
|
| 89 |
+
return {
|
| 90 |
+
"schema_version": 1,
|
| 91 |
+
"framework": FRAMEWORK,
|
| 92 |
+
"model_name": _str(payload, "model_name"),
|
| 93 |
+
"organization": _str(payload, "organization"),
|
| 94 |
+
"submitter": submitter,
|
| 95 |
+
"contact_email": _str(payload, "contact_email"),
|
| 96 |
+
"model_url": _str(payload, "model_url"),
|
| 97 |
+
"paper_url": _str(payload, "paper_url"),
|
| 98 |
+
"additional_info": _str(payload, "additional_info"),
|
| 99 |
+
"visibility": payload.get("visibility", "Public"),
|
| 100 |
+
"benchmark_kwargs": {
|
| 101 |
+
"model_cls": _str(payload, "model_cls"),
|
| 102 |
+
"hub_repo": _str(payload, "hub_repo") or None,
|
| 103 |
+
"checkpoint_url": _str(payload, "checkpoint_url") or None,
|
| 104 |
+
"model_kwargs": payload.get("model_kwargs") or {},
|
| 105 |
+
"head_module_name": payload.get("head_module_name") or DEFAULT_HEAD_MODULE_NAME,
|
| 106 |
+
"peft_target_modules": _as_list(payload.get("peft_target_modules")),
|
| 107 |
+
"peft_ff_modules": _as_list(payload.get("peft_ff_modules")),
|
| 108 |
+
"finetuning_strategies": strategies,
|
| 109 |
+
"heads": heads,
|
| 110 |
+
"datasets": datasets,
|
| 111 |
+
"n_seeds": int(payload.get("n_seeds", DEFAULT_N_SEEDS)),
|
| 112 |
+
},
|
| 113 |
+
"status": "PENDING",
|
| 114 |
+
"submitted_time": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 115 |
+
"job_id": -1,
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def count_user_submissions_this_month(entries, submitter: str, now: datetime | None = None) -> int:
|
| 120 |
+
now = now or datetime.now(timezone.utc)
|
| 121 |
+
month = now.strftime("%Y-%m")
|
| 122 |
+
return sum(
|
| 123 |
+
1 for e in entries
|
| 124 |
+
if e.get("submitter") == submitter and str(e.get("submitted_time", "")).startswith(month)
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def monthly_quota_exceeded(entries, submitter: str, now: datetime | None = None) -> bool:
|
| 129 |
+
return count_user_submissions_this_month(entries, submitter, now) >= MONTHLY_QUOTA
|
|
@@ -166,3 +166,31 @@ class ModelValidator:
|
|
| 166 |
if "You are trying to access a gated repo." in str(e):
|
| 167 |
return True, "The model is gated and requires special access permissions.", None
|
| 168 |
return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
if "You are trying to access a gated repo." in str(e):
|
| 167 |
return True, "The model is gated and requires special access permissions.", None
|
| 168 |
return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None
|
| 169 |
+
|
| 170 |
+
async def validate_weights_source(self, hub_repo: str, checkpoint_url: str):
|
| 171 |
+
"""Validate the OEB weights source. Returns (ok, error)."""
|
| 172 |
+
if hub_repo:
|
| 173 |
+
try:
|
| 174 |
+
await asyncio.to_thread(self.api.repo_info, repo_id=hub_repo, token=self.token)
|
| 175 |
+
return True, ""
|
| 176 |
+
except Exception as e:
|
| 177 |
+
return False, f"hub_repo '{hub_repo}' was not found on the Hub: {e}"
|
| 178 |
+
if checkpoint_url:
|
| 179 |
+
if not str(checkpoint_url).startswith(("http://", "https://")):
|
| 180 |
+
return False, "checkpoint_url must be an http(s) URL."
|
| 181 |
+
return True, ""
|
| 182 |
+
return False, "No weights source provided."
|
| 183 |
+
|
| 184 |
+
async def get_repo_license(self, hub_repo: str):
|
| 185 |
+
"""Best-effort license tag for a hub repo (empty string if unknown)."""
|
| 186 |
+
if not hub_repo:
|
| 187 |
+
return ""
|
| 188 |
+
try:
|
| 189 |
+
info = await asyncio.to_thread(self.api.repo_info, repo_id=hub_repo, token=self.token)
|
| 190 |
+
tags = getattr(info, "tags", None) or []
|
| 191 |
+
for t in tags:
|
| 192 |
+
if isinstance(t, str) and t.startswith("license:"):
|
| 193 |
+
return t.split("license:", 1)[1]
|
| 194 |
+
return ""
|
| 195 |
+
except Exception:
|
| 196 |
+
return ""
|
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenEEGBench eval worker (stub).
|
| 2 |
+
|
| 3 |
+
Polls the requests queue for PENDING submissions, runs oeb.benchmark(), maps
|
| 4 |
+
results to the contents schema, and writes them back. The GPU run is guarded:
|
| 5 |
+
without --run it only previews. open_eeg_bench is imported lazily so this file
|
| 6 |
+
imports without the heavy dependency installed.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python -m scripts.run_eval_worker --dry-run
|
| 10 |
+
python -m scripts.run_eval_worker --run --device cuda
|
| 11 |
+
"""
|
| 12 |
+
import argparse
|
| 13 |
+
import json
|
| 14 |
+
import logging
|
| 15 |
+
import tempfile
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
from app.config.base import HF_TOKEN
|
| 19 |
+
from app.config.hf_config import API as hf_api, QUEUE_REPO, AGGREGATED_REPO
|
| 20 |
+
from app.services.results_mapping import map_oeb_results_to_contents
|
| 21 |
+
|
| 22 |
+
logging.basicConfig(level=logging.INFO)
|
| 23 |
+
logger = logging.getLogger(__name__)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load_pending():
|
| 27 |
+
local_dir = hf_api.snapshot_download(repo_id=QUEUE_REPO, repo_type="dataset", token=HF_TOKEN)
|
| 28 |
+
pending = []
|
| 29 |
+
for p in Path(local_dir).glob("**/*.json"):
|
| 30 |
+
try:
|
| 31 |
+
entry = json.loads(p.read_text())
|
| 32 |
+
except Exception:
|
| 33 |
+
continue
|
| 34 |
+
if entry.get("status") == "PENDING" and entry.get("framework") == "open-eeg-bench":
|
| 35 |
+
pending.append((p, entry))
|
| 36 |
+
return pending
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run_one(entry, device):
|
| 40 |
+
import open_eeg_bench as oeb # lazy heavy import
|
| 41 |
+
df = oeb.benchmark(device=device, **entry["benchmark_kwargs"])
|
| 42 |
+
return map_oeb_results_to_contents(df, entry)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main():
|
| 46 |
+
ap = argparse.ArgumentParser()
|
| 47 |
+
ap.add_argument("--dry-run", action="store_true", help="List pending submissions only.")
|
| 48 |
+
ap.add_argument("--run", action="store_true", help="Actually run oeb.benchmark (needs GPU + open-eeg-bench).")
|
| 49 |
+
ap.add_argument("--device", default="cpu")
|
| 50 |
+
args = ap.parse_args()
|
| 51 |
+
|
| 52 |
+
pending = load_pending()
|
| 53 |
+
logger.info("Found %d pending OpenEEGBench submissions.", len(pending))
|
| 54 |
+
for path, entry in pending:
|
| 55 |
+
logger.info("PENDING: %s (%s)", entry.get("model_name"), entry["benchmark_kwargs"].get("model_cls"))
|
| 56 |
+
if args.dry_run or not args.run:
|
| 57 |
+
continue
|
| 58 |
+
try:
|
| 59 |
+
rows = run_one(entry, args.device)
|
| 60 |
+
logger.info("Mapped %d leaderboard row(s) for %s.", len(rows), entry.get("model_name"))
|
| 61 |
+
# NOTE: appending to AGGREGATED_REPO (contents) is intentionally left for the
|
| 62 |
+
# production worker; print the rows here so the contract is observable.
|
| 63 |
+
print(json.dumps(rows, indent=2, default=str))
|
| 64 |
+
except Exception as e:
|
| 65 |
+
logger.error("Run failed for %s: %s", entry.get("model_name"), e)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from app.services.results_mapping import map_oeb_results_to_contents
|
| 3 |
+
|
| 4 |
+
def _request():
|
| 5 |
+
return {
|
| 6 |
+
"model_name": "BIOT-frozen",
|
| 7 |
+
"organization": "braindecode",
|
| 8 |
+
"model_url": "https://huggingface.co/braindecode/biot",
|
| 9 |
+
"paper_url": "",
|
| 10 |
+
"submitted_time": "2026-06-15T12:00:00Z",
|
| 11 |
+
"benchmark_kwargs": {"hub_repo": "braindecode/biot", "model_cls": "braindecode.models.BIOT"},
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
def _df():
|
| 15 |
+
# two datasets x one strategy x two seeds, all completed
|
| 16 |
+
return pd.DataFrame([
|
| 17 |
+
{"backbone": "BIOT", "dataset": "braindecode/bcic2a", "finetuning": "frozen",
|
| 18 |
+
"head": "linear_head", "seed": 0, "status": "completed",
|
| 19 |
+
"test_balanced_accuracy": 0.40, "trainable_params": 3_190_000},
|
| 20 |
+
{"backbone": "BIOT", "dataset": "braindecode/bcic2a", "finetuning": "frozen",
|
| 21 |
+
"head": "linear_head", "seed": 1, "status": "completed",
|
| 22 |
+
"test_balanced_accuracy": 0.60, "trainable_params": 3_190_000},
|
| 23 |
+
{"backbone": "BIOT", "dataset": "braindecode/physionet", "finetuning": "frozen",
|
| 24 |
+
"head": "linear_head", "seed": 0, "status": "completed",
|
| 25 |
+
"test_balanced_accuracy": 0.50, "trainable_params": 3_190_000},
|
| 26 |
+
])
|
| 27 |
+
|
| 28 |
+
def test_one_row_per_strategy():
|
| 29 |
+
rows = map_oeb_results_to_contents(_df(), _request())
|
| 30 |
+
assert len(rows) == 1
|
| 31 |
+
row = rows[0]
|
| 32 |
+
assert row["fullname"] == "BIOT-frozen"
|
| 33 |
+
assert row["adapter"] == "frozen"
|
| 34 |
+
assert row["Architecture"] == "BIOT"
|
| 35 |
+
# mean over seeds: bcic2a = (0.40+0.60)/2 = 0.50 (fraction)
|
| 36 |
+
assert abs(row["bcic2a_accuracy"] - 0.50) < 1e-9
|
| 37 |
+
assert abs(row["physionet_accuracy"] - 0.50) < 1e-9
|
| 38 |
+
# Average over available datasets * 100 = 50.0
|
| 39 |
+
assert abs(row["Average ⬆️"] - 50.0) < 1e-9
|
| 40 |
+
assert abs(row["#Params (M)"] - 3.19) < 1e-6
|
| 41 |
+
assert row["Available on the hub"] is True
|
| 42 |
+
|
| 43 |
+
def test_failed_rows_ignored():
|
| 44 |
+
df = _df()
|
| 45 |
+
df.loc[len(df)] = {"backbone": "BIOT", "dataset": "braindecode/tuab", "finetuning": "frozen",
|
| 46 |
+
"head": "linear_head", "seed": 0, "status": "failed",
|
| 47 |
+
"exception": "boom", "test_balanced_accuracy": None, "trainable_params": None}
|
| 48 |
+
rows = map_oeb_results_to_contents(df, _request())
|
| 49 |
+
assert rows[0].get("tuab_accuracy", 0) == 0 # failed dataset not scored
|
| 50 |
+
|
| 51 |
+
def test_multiple_strategies_multiple_rows():
|
| 52 |
+
df = _df()
|
| 53 |
+
extra = df.copy(); extra["finetuning"] = "lora"
|
| 54 |
+
rows = map_oeb_results_to_contents(pd.concat([df, extra], ignore_index=True), _request())
|
| 55 |
+
assert {r["adapter"] for r in rows} == {"frozen", "lora"}
|
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timezone
|
| 2 |
+
|
| 3 |
+
from app.services.submissions import (
|
| 4 |
+
validate_submission,
|
| 5 |
+
build_request_entry,
|
| 6 |
+
count_user_submissions_this_month,
|
| 7 |
+
monthly_quota_exceeded,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
def _valid_payload(**overrides):
|
| 11 |
+
p = {
|
| 12 |
+
"model_cls": "braindecode.models.BIOT",
|
| 13 |
+
"model_name": "BIOT-frozen",
|
| 14 |
+
"organization": "braindecode",
|
| 15 |
+
"hub_repo": "braindecode/biot-weights",
|
| 16 |
+
"finetuning_strategies": ["frozen"],
|
| 17 |
+
"heads": ["linear_head"],
|
| 18 |
+
"datasets": ["bcic2a", "physionet"],
|
| 19 |
+
"n_seeds": 3,
|
| 20 |
+
"visibility": "Public",
|
| 21 |
+
}
|
| 22 |
+
p.update(overrides)
|
| 23 |
+
return p
|
| 24 |
+
|
| 25 |
+
def test_valid_payload_has_no_errors():
|
| 26 |
+
assert validate_submission(_valid_payload()) == []
|
| 27 |
+
|
| 28 |
+
def test_model_cls_required():
|
| 29 |
+
errs = validate_submission(_valid_payload(model_cls=""))
|
| 30 |
+
assert any("model_cls" in e for e in errs)
|
| 31 |
+
|
| 32 |
+
def test_model_cls_must_be_dotted_path():
|
| 33 |
+
errs = validate_submission(_valid_payload(model_cls="notdotted"))
|
| 34 |
+
assert any("dotted import path" in e for e in errs)
|
| 35 |
+
|
| 36 |
+
def test_weights_source_required():
|
| 37 |
+
errs = validate_submission(_valid_payload(hub_repo="", checkpoint_url=""))
|
| 38 |
+
assert any("weights source" in e for e in errs)
|
| 39 |
+
|
| 40 |
+
def test_only_one_weights_source():
|
| 41 |
+
errs = validate_submission(_valid_payload(checkpoint_url="https://x/w.pth"))
|
| 42 |
+
assert any("only one weights source" in e for e in errs)
|
| 43 |
+
|
| 44 |
+
def test_peft_strategy_requires_target_modules():
|
| 45 |
+
errs = validate_submission(_valid_payload(finetuning_strategies=["lora"]))
|
| 46 |
+
assert any("peft_target_modules" in e for e in errs)
|
| 47 |
+
|
| 48 |
+
def test_ia3_requires_ff_modules():
|
| 49 |
+
errs = validate_submission(_valid_payload(
|
| 50 |
+
finetuning_strategies=["ia3"], peft_target_modules=["to_q"]))
|
| 51 |
+
assert any("peft_ff_modules" in e for e in errs)
|
| 52 |
+
|
| 53 |
+
def test_unknown_strategy_rejected():
|
| 54 |
+
errs = validate_submission(_valid_payload(finetuning_strategies=["magic"]))
|
| 55 |
+
assert any("finetuning strategies" in e for e in errs)
|
| 56 |
+
|
| 57 |
+
def test_unknown_dataset_rejected():
|
| 58 |
+
errs = validate_submission(_valid_payload(datasets=["not_a_dataset"]))
|
| 59 |
+
assert any("datasets" in e for e in errs)
|
| 60 |
+
|
| 61 |
+
def test_bad_email_rejected():
|
| 62 |
+
errs = validate_submission(_valid_payload(contact_email="nope"))
|
| 63 |
+
assert any("email" in e for e in errs)
|
| 64 |
+
|
| 65 |
+
def test_n_seeds_must_be_positive_int():
|
| 66 |
+
errs = validate_submission(_valid_payload(n_seeds=0))
|
| 67 |
+
assert any("n_seeds" in e for e in errs)
|
| 68 |
+
|
| 69 |
+
def test_build_request_entry_shape():
|
| 70 |
+
now = datetime(2026, 6, 15, 12, 0, 0, tzinfo=timezone.utc)
|
| 71 |
+
entry = build_request_entry(_valid_payload(), "alice", now=now)
|
| 72 |
+
assert entry["framework"] == "open-eeg-bench"
|
| 73 |
+
assert entry["status"] == "PENDING"
|
| 74 |
+
assert entry["submitter"] == "alice"
|
| 75 |
+
assert entry["submitted_time"] == "2026-06-15T12:00:00Z"
|
| 76 |
+
assert entry["benchmark_kwargs"]["model_cls"] == "braindecode.models.BIOT"
|
| 77 |
+
assert entry["benchmark_kwargs"]["hub_repo"] == "braindecode/biot-weights"
|
| 78 |
+
assert entry["benchmark_kwargs"]["checkpoint_url"] is None
|
| 79 |
+
assert entry["benchmark_kwargs"]["finetuning_strategies"] == ["frozen"]
|
| 80 |
+
|
| 81 |
+
def test_monthly_count_and_quota():
|
| 82 |
+
entries = [
|
| 83 |
+
{"submitter": "alice", "submitted_time": "2026-06-01T00:00:00Z"},
|
| 84 |
+
{"submitter": "alice", "submitted_time": "2026-06-09T00:00:00Z"},
|
| 85 |
+
{"submitter": "bob", "submitted_time": "2026-06-09T00:00:00Z"},
|
| 86 |
+
{"submitter": "alice", "submitted_time": "2026-05-30T00:00:00Z"},
|
| 87 |
+
]
|
| 88 |
+
now = datetime(2026, 6, 15, tzinfo=timezone.utc)
|
| 89 |
+
assert count_user_submissions_this_month(entries, "alice", now=now) == 2
|
| 90 |
+
assert monthly_quota_exceeded(entries, "alice", now=now) is False
|
| 91 |
+
five = entries + [{"submitter": "alice", "submitted_time": "2026-06-1%dT00:00:00Z" % d} for d in range(3, 6)]
|
| 92 |
+
assert monthly_quota_exceeded(five, "alice", now=now) is True
|
|
@@ -67,13 +67,13 @@ const columns = [
|
|
| 67 |
{
|
| 68 |
id: "wait_time",
|
| 69 |
label: "Submitted",
|
| 70 |
-
width: "
|
| 71 |
align: "center",
|
| 72 |
},
|
| 73 |
{
|
| 74 |
-
id: "
|
| 75 |
-
label: "
|
| 76 |
-
width: "
|
| 77 |
align: "center",
|
| 78 |
},
|
| 79 |
{
|
|
@@ -196,14 +196,16 @@ const ModelTable = ({ models, emptyMessage, status }) => {
|
|
| 196 |
padding: 0,
|
| 197 |
position: "relative",
|
| 198 |
width: "100%",
|
| 199 |
-
height: `${rowVirtualizer.getTotalSize()}px`,
|
| 200 |
}}
|
| 201 |
colSpan={columns.length}
|
| 202 |
>
|
| 203 |
<>
|
| 204 |
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
| 205 |
const model = models[virtualRow.index];
|
| 206 |
-
const
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
return (
|
| 209 |
<TableRow
|
|
@@ -234,7 +236,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
|
|
| 234 |
}}
|
| 235 |
>
|
| 236 |
<Link
|
| 237 |
-
href={`https://huggingface.co/${
|
| 238 |
target="_blank"
|
| 239 |
rel="noopener noreferrer"
|
| 240 |
sx={{
|
|
@@ -251,7 +253,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
|
|
| 251 |
},
|
| 252 |
}}
|
| 253 |
>
|
| 254 |
-
{
|
| 255 |
<OpenInNewIcon />
|
| 256 |
</Link>
|
| 257 |
</TableCell>
|
|
@@ -311,7 +313,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
|
|
| 311 |
}}
|
| 312 |
>
|
| 313 |
<Typography variant="body2" color="text.secondary">
|
| 314 |
-
{model.
|
| 315 |
</Typography>
|
| 316 |
</TableCell>
|
| 317 |
<TableCell
|
|
@@ -327,7 +329,7 @@ const ModelTable = ({ models, emptyMessage, status }) => {
|
|
| 327 |
justifyContent: "center",
|
| 328 |
}}
|
| 329 |
>
|
| 330 |
-
{model.revision.substring(0, 7)}
|
| 331 |
</TableCell>
|
| 332 |
<TableCell
|
| 333 |
align={columns[5].align}
|
|
|
|
| 67 |
{
|
| 68 |
id: "wait_time",
|
| 69 |
label: "Submitted",
|
| 70 |
+
width: "13%",
|
| 71 |
align: "center",
|
| 72 |
},
|
| 73 |
{
|
| 74 |
+
id: "adapter",
|
| 75 |
+
label: "Strategy",
|
| 76 |
+
width: "12%",
|
| 77 |
align: "center",
|
| 78 |
},
|
| 79 |
{
|
|
|
|
| 196 |
padding: 0,
|
| 197 |
position: "relative",
|
| 198 |
width: "100%",
|
|
|
|
| 199 |
}}
|
| 200 |
colSpan={columns.length}
|
| 201 |
>
|
| 202 |
<>
|
| 203 |
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
| 204 |
const model = models[virtualRow.index];
|
| 205 |
+
const modelName = model.model_name || model.name;
|
| 206 |
+
const waitTime = model.wait_time
|
| 207 |
+
? formatWaitTime(model.wait_time)
|
| 208 |
+
: "";
|
| 209 |
|
| 210 |
return (
|
| 211 |
<TableRow
|
|
|
|
| 236 |
}}
|
| 237 |
>
|
| 238 |
<Link
|
| 239 |
+
href={`https://huggingface.co/${modelName}`}
|
| 240 |
target="_blank"
|
| 241 |
rel="noopener noreferrer"
|
| 242 |
sx={{
|
|
|
|
| 253 |
},
|
| 254 |
}}
|
| 255 |
>
|
| 256 |
+
{modelName}
|
| 257 |
<OpenInNewIcon />
|
| 258 |
</Link>
|
| 259 |
</TableCell>
|
|
|
|
| 313 |
}}
|
| 314 |
>
|
| 315 |
<Typography variant="body2" color="text.secondary">
|
| 316 |
+
{model.adapter || "—"}
|
| 317 |
</Typography>
|
| 318 |
</TableCell>
|
| 319 |
<TableCell
|
|
|
|
| 329 |
justifyContent: "center",
|
| 330 |
}}
|
| 331 |
>
|
| 332 |
+
{model.revision ? model.revision.substring(0, 7) : "—"}
|
| 333 |
</TableCell>
|
| 334 |
<TableCell
|
| 335 |
align={columns[5].align}
|
|
@@ -6,6 +6,10 @@ import {
|
|
| 6 |
TextField,
|
| 7 |
Button,
|
| 8 |
FormControl,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
InputLabel,
|
| 10 |
Select,
|
| 11 |
MenuItem,
|
|
@@ -18,98 +22,201 @@ import RocketLaunchIcon from "@mui/icons-material/RocketLaunch";
|
|
| 18 |
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
| 19 |
import { alpha } from "@mui/material/styles";
|
| 20 |
import InfoIconWithTooltip from "../../../../components/shared/InfoIconWithTooltip";
|
| 21 |
-
import {
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
import AuthContainer from "../../../../components/shared/AuthContainer";
|
| 24 |
|
| 25 |
const HELP_TEXTS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
modelName: (
|
| 27 |
<Box sx={{ p: 1 }}>
|
| 28 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 29 |
-
Model Name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
</Typography>
|
| 31 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
safety and loading performance. Example: braindecode/EEGNetv4
|
| 35 |
</Typography>
|
| 36 |
</Box>
|
| 37 |
),
|
| 38 |
-
|
| 39 |
<Box sx={{ p: 1 }}>
|
| 40 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 41 |
-
|
| 42 |
</Typography>
|
| 43 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
stable and contains all necessary files.
|
| 47 |
</Typography>
|
| 48 |
</Box>
|
| 49 |
),
|
| 50 |
-
|
| 51 |
<Box sx={{ p: 1 }}>
|
| 52 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 53 |
-
|
| 54 |
</Typography>
|
| 55 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
AdaLoRA: Adaptive rank allocation{" "}
|
| 59 |
-
DoRA: Weight-decomposed adaptation{" "}
|
| 60 |
-
OFT: Orthogonal fine-tuning{" "}
|
| 61 |
-
Probe: Linear probing (head only){" "}
|
| 62 |
-
Full Fine-tune: All parameters updated
|
| 63 |
</Typography>
|
| 64 |
</Box>
|
| 65 |
),
|
| 66 |
-
|
| 67 |
<Box sx={{ p: 1 }}>
|
| 68 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 69 |
-
|
| 70 |
</Typography>
|
| 71 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
combining base model and adapter/delta parameters.
|
| 75 |
</Typography>
|
| 76 |
</Box>
|
| 77 |
),
|
| 78 |
-
|
| 79 |
<Box sx={{ p: 1 }}>
|
| 80 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 81 |
-
|
| 82 |
</Typography>
|
| 83 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
Choose carefully as incorrect precision can cause evaluation errors.
|
| 87 |
</Typography>
|
| 88 |
</Box>
|
| 89 |
),
|
| 90 |
};
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
function ModelSubmissionForm({ user, isAuthenticated }) {
|
| 101 |
-
const [formData, setFormData] = useState(
|
| 102 |
-
modelName: "",
|
| 103 |
-
revision: "main",
|
| 104 |
-
modelType: "lora",
|
| 105 |
-
precision: "float16",
|
| 106 |
-
baseModel: "",
|
| 107 |
-
});
|
| 108 |
const [error, setError] = useState(null);
|
| 109 |
const [submitting, setSubmitting] = useState(false);
|
| 110 |
const [success, setSuccess] = useState(false);
|
| 111 |
const [submittedData, setSubmittedData] = useState(null);
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
const handleChange = (event) => {
|
| 114 |
const { name, value, checked } = event.target;
|
| 115 |
setFormData((prev) => ({
|
|
@@ -124,19 +231,37 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 124 |
setSubmitting(true);
|
| 125 |
|
| 126 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
const response = await fetch("/api/models/submit", {
|
| 128 |
method: "POST",
|
| 129 |
headers: {
|
| 130 |
"Content-Type": "application/json",
|
| 131 |
},
|
| 132 |
-
body: JSON.stringify(
|
| 133 |
-
model_id: formData.modelName,
|
| 134 |
-
revision: formData.revision,
|
| 135 |
-
model_type: formData.modelType,
|
| 136 |
-
precision: formData.precision,
|
| 137 |
-
base_model: formData.baseModel,
|
| 138 |
-
user_id: user.username,
|
| 139 |
-
}),
|
| 140 |
});
|
| 141 |
|
| 142 |
if (!response.ok) {
|
|
@@ -206,10 +331,10 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 206 |
color="text.secondary"
|
| 207 |
sx={{ width: 120 }}
|
| 208 |
>
|
| 209 |
-
|
| 210 |
</Typography>
|
| 211 |
-
<Typography variant="body2">
|
| 212 |
-
{submittedData.
|
| 213 |
</Typography>
|
| 214 |
</Stack>
|
| 215 |
<Stack direction="row" spacing={2}>
|
|
@@ -218,10 +343,12 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 218 |
color="text.secondary"
|
| 219 |
sx={{ width: 120 }}
|
| 220 |
>
|
| 221 |
-
|
| 222 |
</Typography>
|
| 223 |
-
<Typography variant="body2"
|
| 224 |
-
{submittedData.
|
|
|
|
|
|
|
| 225 |
</Typography>
|
| 226 |
</Stack>
|
| 227 |
<Stack direction="row" spacing={2}>
|
|
@@ -230,32 +357,18 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 230 |
color="text.secondary"
|
| 231 |
sx={{ width: 120 }}
|
| 232 |
>
|
| 233 |
-
|
| 234 |
</Typography>
|
| 235 |
<Typography variant="body2">
|
| 236 |
-
{submittedData.
|
| 237 |
</Typography>
|
| 238 |
</Stack>
|
| 239 |
-
{submittedData.baseModel && (
|
| 240 |
-
<Stack direction="row" spacing={2}>
|
| 241 |
-
<Typography
|
| 242 |
-
variant="body2"
|
| 243 |
-
color="text.secondary"
|
| 244 |
-
sx={{ width: 120 }}
|
| 245 |
-
>
|
| 246 |
-
Base model:
|
| 247 |
-
</Typography>
|
| 248 |
-
<Typography variant="body2">
|
| 249 |
-
{submittedData.baseModel}
|
| 250 |
-
</Typography>
|
| 251 |
-
</Stack>
|
| 252 |
-
)}
|
| 253 |
</Stack>
|
| 254 |
</Paper>
|
| 255 |
|
| 256 |
<Typography variant="body2" color="text.secondary">
|
| 257 |
-
|
| 258 |
-
|
| 259 |
</Typography>
|
| 260 |
|
| 261 |
<Stack direction="row" spacing={2}>
|
|
@@ -265,13 +378,7 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 265 |
onClick={() => {
|
| 266 |
setSuccess(false);
|
| 267 |
setSubmittedData(null);
|
| 268 |
-
setFormData(
|
| 269 |
-
modelName: "",
|
| 270 |
-
revision: "main",
|
| 271 |
-
modelType: "lora",
|
| 272 |
-
precision: "float16",
|
| 273 |
-
baseModel: "",
|
| 274 |
-
});
|
| 275 |
}}
|
| 276 |
>
|
| 277 |
Submit another model
|
|
@@ -334,20 +441,38 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 334 |
<Grid item xs={12}>
|
| 335 |
<Stack direction="row" spacing={1} alignItems="center">
|
| 336 |
<Typography variant="h6">Model Information</Typography>
|
| 337 |
-
<InfoIconWithTooltip tooltip={HELP_TEXTS.
|
| 338 |
</Stack>
|
| 339 |
</Grid>
|
| 340 |
|
| 341 |
-
<Grid item xs={12}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
<TextField
|
| 343 |
required
|
| 344 |
fullWidth
|
| 345 |
name="modelName"
|
| 346 |
label="Model Name"
|
| 347 |
-
placeholder="
|
| 348 |
value={formData.modelName}
|
| 349 |
onChange={handleChange}
|
| 350 |
-
helperText="
|
| 351 |
InputProps={{
|
| 352 |
endAdornment: (
|
| 353 |
<InfoIconWithTooltip tooltip={HELP_TEXTS.modelName} />
|
|
@@ -356,47 +481,126 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 356 |
/>
|
| 357 |
</Grid>
|
| 358 |
|
| 359 |
-
<Grid item xs={12} sm={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
<TextField
|
| 361 |
fullWidth
|
| 362 |
-
name="
|
| 363 |
-
label="
|
| 364 |
-
value={formData.
|
| 365 |
onChange={handleChange}
|
| 366 |
-
helperText="Default:
|
| 367 |
InputProps={{
|
| 368 |
endAdornment: (
|
| 369 |
-
<InfoIconWithTooltip
|
|
|
|
|
|
|
| 370 |
),
|
| 371 |
}}
|
| 372 |
/>
|
| 373 |
</Grid>
|
| 374 |
|
| 375 |
-
{/*
|
| 376 |
<Grid item xs={12}>
|
| 377 |
<Stack direction="row" spacing={1} alignItems="center">
|
| 378 |
-
<Typography variant="h6">
|
| 379 |
</Stack>
|
| 380 |
</Grid>
|
| 381 |
|
| 382 |
<Grid item xs={12} sm={6}>
|
| 383 |
<FormControl fullWidth>
|
| 384 |
-
<InputLabel>
|
| 385 |
<Select
|
| 386 |
-
|
| 387 |
-
|
|
|
|
| 388 |
onChange={handleChange}
|
| 389 |
-
label="
|
|
|
|
|
|
|
|
|
|
| 390 |
endAdornment={
|
| 391 |
<InfoIconWithTooltip
|
| 392 |
-
tooltip={HELP_TEXTS.
|
| 393 |
sx={{ mr: 2 }}
|
| 394 |
/>
|
| 395 |
}
|
| 396 |
>
|
| 397 |
-
{
|
| 398 |
-
<MenuItem key={
|
| 399 |
-
{
|
| 400 |
</MenuItem>
|
| 401 |
))}
|
| 402 |
</Select>
|
|
@@ -405,44 +609,205 @@ function ModelSubmissionForm({ user, isAuthenticated }) {
|
|
| 405 |
|
| 406 |
<Grid item xs={12} sm={6}>
|
| 407 |
<FormControl fullWidth>
|
| 408 |
-
<InputLabel>
|
| 409 |
<Select
|
| 410 |
-
|
| 411 |
-
|
|
|
|
| 412 |
onChange={handleChange}
|
| 413 |
-
label="
|
|
|
|
|
|
|
|
|
|
| 414 |
endAdornment={
|
| 415 |
<InfoIconWithTooltip
|
| 416 |
-
tooltip={HELP_TEXTS.
|
| 417 |
sx={{ mr: 2 }}
|
| 418 |
/>
|
| 419 |
}
|
| 420 |
>
|
| 421 |
-
{
|
| 422 |
-
<MenuItem key={
|
| 423 |
-
{
|
| 424 |
</MenuItem>
|
| 425 |
))}
|
| 426 |
</Select>
|
| 427 |
</FormControl>
|
| 428 |
</Grid>
|
| 429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
<Grid item xs={12}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
<TextField
|
| 432 |
fullWidth
|
| 433 |
-
name="
|
| 434 |
-
label="
|
| 435 |
-
|
|
|
|
| 436 |
onChange={handleChange}
|
| 437 |
-
helperText="Optional.
|
| 438 |
InputProps={{
|
| 439 |
endAdornment: (
|
| 440 |
-
<InfoIconWithTooltip tooltip={HELP_TEXTS.
|
| 441 |
),
|
| 442 |
}}
|
| 443 |
/>
|
| 444 |
</Grid>
|
| 445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
{/* Submit Button */}
|
| 447 |
<Grid item xs={12}>
|
| 448 |
<Box
|
|
|
|
| 6 |
TextField,
|
| 7 |
Button,
|
| 8 |
FormControl,
|
| 9 |
+
FormLabel,
|
| 10 |
+
RadioGroup,
|
| 11 |
+
FormControlLabel,
|
| 12 |
+
Radio,
|
| 13 |
InputLabel,
|
| 14 |
Select,
|
| 15 |
MenuItem,
|
|
|
|
| 22 |
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
| 23 |
import { alpha } from "@mui/material/styles";
|
| 24 |
import InfoIconWithTooltip from "../../../../components/shared/InfoIconWithTooltip";
|
| 25 |
+
import {
|
| 26 |
+
FINETUNING_STRATEGIES,
|
| 27 |
+
HEADS,
|
| 28 |
+
PEFT_STRATEGIES,
|
| 29 |
+
OEB_DATASETS,
|
| 30 |
+
} from "../../../../pages/LeaderboardPage/components/Leaderboard/constants/modelTypes";
|
| 31 |
import AuthContainer from "../../../../components/shared/AuthContainer";
|
| 32 |
|
| 33 |
const HELP_TEXTS = {
|
| 34 |
+
modelClass: (
|
| 35 |
+
<Box sx={{ p: 1 }}>
|
| 36 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 37 |
+
Model Class (dotted import path)
|
| 38 |
+
</Typography>
|
| 39 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 40 |
+
Fully-qualified import path to the model class evaluated by
|
| 41 |
+
OpenEEGBench. The class must accept (batch, n_chans, n_times) and
|
| 42 |
+
return (batch, n_outputs). Example: braindecode.models.BIOT
|
| 43 |
+
</Typography>
|
| 44 |
+
</Box>
|
| 45 |
+
),
|
| 46 |
modelName: (
|
| 47 |
<Box sx={{ p: 1 }}>
|
| 48 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 49 |
+
Model Name
|
| 50 |
+
</Typography>
|
| 51 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 52 |
+
A human-readable name for your submission. This is how your model will
|
| 53 |
+
appear on the leaderboard.
|
| 54 |
+
</Typography>
|
| 55 |
+
</Box>
|
| 56 |
+
),
|
| 57 |
+
weightsSource: (
|
| 58 |
+
<Box sx={{ p: 1 }}>
|
| 59 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 60 |
+
Weights Source
|
| 61 |
+
</Typography>
|
| 62 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 63 |
+
Provide exactly one source for the pre-trained weights: a public
|
| 64 |
+
Hugging Face Hub repository (with a license tag) or a direct
|
| 65 |
+
http(s) URL to a checkpoint.
|
| 66 |
+
</Typography>
|
| 67 |
+
</Box>
|
| 68 |
+
),
|
| 69 |
+
headModuleName: (
|
| 70 |
+
<Box sx={{ p: 1 }}>
|
| 71 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 72 |
+
Head Module Name
|
| 73 |
+
</Typography>
|
| 74 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 75 |
+
Name of the model's classification head module. OpenEEGBench replaces
|
| 76 |
+
this module to attach evaluation heads. Default: final_layer
|
| 77 |
+
</Typography>
|
| 78 |
+
</Box>
|
| 79 |
+
),
|
| 80 |
+
finetuningStrategies: (
|
| 81 |
+
<Box sx={{ p: 1 }}>
|
| 82 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 83 |
+
Fine-tuning Strategies
|
| 84 |
+
</Typography>
|
| 85 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 86 |
+
Frozen: linear probing (encoder frozen). LoRA / IA3 / AdaLoRA / DoRA /
|
| 87 |
+
OFT: parameter-efficient fine-tuning. Full fine-tune: all parameters
|
| 88 |
+
updated. Two stages: probe then fine-tune. Select one or more.
|
| 89 |
+
</Typography>
|
| 90 |
+
</Box>
|
| 91 |
+
),
|
| 92 |
+
heads: (
|
| 93 |
+
<Box sx={{ p: 1 }}>
|
| 94 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 95 |
+
Evaluation Heads
|
| 96 |
+
</Typography>
|
| 97 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 98 |
+
Linear head: single linear layer. MLP head: small multi-layer
|
| 99 |
+
perceptron. Original head: keep the model's own head. Select one or
|
| 100 |
+
more.
|
| 101 |
+
</Typography>
|
| 102 |
+
</Box>
|
| 103 |
+
),
|
| 104 |
+
peftTargetModules: (
|
| 105 |
+
<Box sx={{ p: 1 }}>
|
| 106 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 107 |
+
PEFT Target Modules
|
| 108 |
+
</Typography>
|
| 109 |
+
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 110 |
+
Comma-separated module names to which the PEFT adapter is applied
|
| 111 |
+
(e.g. to_q, to_v). Required when a PEFT strategy (LoRA, IA3, AdaLoRA,
|
| 112 |
+
DoRA, OFT) is selected.
|
| 113 |
+
</Typography>
|
| 114 |
+
</Box>
|
| 115 |
+
),
|
| 116 |
+
peftFfModules: (
|
| 117 |
+
<Box sx={{ p: 1 }}>
|
| 118 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 119 |
+
PEFT Feed-Forward Modules
|
| 120 |
</Typography>
|
| 121 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 122 |
+
Comma-separated feed-forward module names scaled by IA3. Required when
|
| 123 |
+
the IA3 strategy is selected.
|
|
|
|
| 124 |
</Typography>
|
| 125 |
</Box>
|
| 126 |
),
|
| 127 |
+
datasets: (
|
| 128 |
<Box sx={{ p: 1 }}>
|
| 129 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 130 |
+
Datasets
|
| 131 |
</Typography>
|
| 132 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 133 |
+
The downstream datasets to evaluate on. Leave empty to run all 10
|
| 134 |
+
datasets in the OpenEEGBench classification suite.
|
|
|
|
| 135 |
</Typography>
|
| 136 |
</Box>
|
| 137 |
),
|
| 138 |
+
nSeeds: (
|
| 139 |
<Box sx={{ p: 1 }}>
|
| 140 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 141 |
+
Number of Seeds
|
| 142 |
</Typography>
|
| 143 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 144 |
+
How many random seeds to average over per dataset. More seeds give a
|
| 145 |
+
more stable estimate but take longer. Default: 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
</Typography>
|
| 147 |
</Box>
|
| 148 |
),
|
| 149 |
+
contactEmail: (
|
| 150 |
<Box sx={{ p: 1 }}>
|
| 151 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 152 |
+
Contact Email
|
| 153 |
</Typography>
|
| 154 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 155 |
+
Optional. Stored privately and used only to reach you about your
|
| 156 |
+
submission. It is never shown on the public leaderboard.
|
|
|
|
| 157 |
</Typography>
|
| 158 |
</Box>
|
| 159 |
),
|
| 160 |
+
visibility: (
|
| 161 |
<Box sx={{ p: 1 }}>
|
| 162 |
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
| 163 |
+
Visibility
|
| 164 |
</Typography>
|
| 165 |
<Typography variant="body2" sx={{ opacity: 0.9, lineHeight: 1.4 }}>
|
| 166 |
+
Public submissions appear on the leaderboard. Private submissions are
|
| 167 |
+
evaluated but kept out of the public table.
|
|
|
|
| 168 |
</Typography>
|
| 169 |
</Box>
|
| 170 |
),
|
| 171 |
};
|
| 172 |
|
| 173 |
+
const VISIBILITY_OPTIONS = ["Public", "Private"];
|
| 174 |
+
|
| 175 |
+
const INITIAL_FORM_DATA = {
|
| 176 |
+
modelClass: "", // dotted import path
|
| 177 |
+
modelName: "",
|
| 178 |
+
organization: "",
|
| 179 |
+
weightsSource: "hub_repo", // "hub_repo" | "checkpoint_url"
|
| 180 |
+
hubRepo: "",
|
| 181 |
+
checkpointUrl: "",
|
| 182 |
+
headModuleName: "final_layer",
|
| 183 |
+
finetuningStrategies: ["frozen"],
|
| 184 |
+
heads: ["linear_head"],
|
| 185 |
+
peftTargetModules: "", // comma-separated -> array on submit
|
| 186 |
+
peftFfModules: "", // comma-separated -> array on submit
|
| 187 |
+
datasets: [], // empty = all
|
| 188 |
+
nSeeds: 3,
|
| 189 |
+
modelUrl: "",
|
| 190 |
+
paperUrl: "",
|
| 191 |
+
contactEmail: "",
|
| 192 |
+
additionalInfo: "",
|
| 193 |
+
visibility: "Public",
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
const toList = (s) =>
|
| 197 |
+
s
|
| 198 |
+
.split(",")
|
| 199 |
+
.map((x) => x.trim())
|
| 200 |
+
.filter(Boolean);
|
| 201 |
+
|
| 202 |
+
const strategyLabel = (value) =>
|
| 203 |
+
FINETUNING_STRATEGIES.find((s) => s.value === value)?.label || value;
|
| 204 |
+
|
| 205 |
+
const headLabel = (value) =>
|
| 206 |
+
HEADS.find((h) => h.value === value)?.label || value;
|
| 207 |
|
| 208 |
function ModelSubmissionForm({ user, isAuthenticated }) {
|
| 209 |
+
const [formData, setFormData] = useState(INITIAL_FORM_DATA);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
const [error, setError] = useState(null);
|
| 211 |
const [submitting, setSubmitting] = useState(false);
|
| 212 |
const [success, setSuccess] = useState(false);
|
| 213 |
const [submittedData, setSubmittedData] = useState(null);
|
| 214 |
|
| 215 |
+
const showPeftTargetModules = formData.finetuningStrategies.some((s) =>
|
| 216 |
+
PEFT_STRATEGIES.includes(s)
|
| 217 |
+
);
|
| 218 |
+
const showPeftFfModules = formData.finetuningStrategies.includes("ia3");
|
| 219 |
+
|
| 220 |
const handleChange = (event) => {
|
| 221 |
const { name, value, checked } = event.target;
|
| 222 |
setFormData((prev) => ({
|
|
|
|
| 231 |
setSubmitting(true);
|
| 232 |
|
| 233 |
try {
|
| 234 |
+
const body = {
|
| 235 |
+
user_id: user.username,
|
| 236 |
+
model_cls: formData.modelClass,
|
| 237 |
+
model_name: formData.modelName,
|
| 238 |
+
organization: formData.organization,
|
| 239 |
+
hub_repo:
|
| 240 |
+
formData.weightsSource === "hub_repo" ? formData.hubRepo : "",
|
| 241 |
+
checkpoint_url:
|
| 242 |
+
formData.weightsSource === "checkpoint_url"
|
| 243 |
+
? formData.checkpointUrl
|
| 244 |
+
: "",
|
| 245 |
+
head_module_name: formData.headModuleName,
|
| 246 |
+
finetuning_strategies: formData.finetuningStrategies,
|
| 247 |
+
heads: formData.heads,
|
| 248 |
+
peft_target_modules: toList(formData.peftTargetModules),
|
| 249 |
+
peft_ff_modules: toList(formData.peftFfModules),
|
| 250 |
+
datasets: formData.datasets,
|
| 251 |
+
n_seeds: Number(formData.nSeeds),
|
| 252 |
+
model_url: formData.modelUrl,
|
| 253 |
+
paper_url: formData.paperUrl,
|
| 254 |
+
contact_email: formData.contactEmail,
|
| 255 |
+
additional_info: formData.additionalInfo,
|
| 256 |
+
visibility: formData.visibility,
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
const response = await fetch("/api/models/submit", {
|
| 260 |
method: "POST",
|
| 261 |
headers: {
|
| 262 |
"Content-Type": "application/json",
|
| 263 |
},
|
| 264 |
+
body: JSON.stringify(body),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
});
|
| 266 |
|
| 267 |
if (!response.ok) {
|
|
|
|
| 331 |
color="text.secondary"
|
| 332 |
sx={{ width: 120 }}
|
| 333 |
>
|
| 334 |
+
Model class:
|
| 335 |
</Typography>
|
| 336 |
+
<Typography variant="body2" sx={{ fontFamily: "monospace" }}>
|
| 337 |
+
{submittedData.modelClass}
|
| 338 |
</Typography>
|
| 339 |
</Stack>
|
| 340 |
<Stack direction="row" spacing={2}>
|
|
|
|
| 343 |
color="text.secondary"
|
| 344 |
sx={{ width: 120 }}
|
| 345 |
>
|
| 346 |
+
Strategies:
|
| 347 |
</Typography>
|
| 348 |
+
<Typography variant="body2">
|
| 349 |
+
{submittedData.finetuningStrategies
|
| 350 |
+
.map(strategyLabel)
|
| 351 |
+
.join(", ")}
|
| 352 |
</Typography>
|
| 353 |
</Stack>
|
| 354 |
<Stack direction="row" spacing={2}>
|
|
|
|
| 357 |
color="text.secondary"
|
| 358 |
sx={{ width: 120 }}
|
| 359 |
>
|
| 360 |
+
Visibility:
|
| 361 |
</Typography>
|
| 362 |
<Typography variant="body2">
|
| 363 |
+
{submittedData.visibility}
|
| 364 |
</Typography>
|
| 365 |
</Stack>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</Stack>
|
| 367 |
</Paper>
|
| 368 |
|
| 369 |
<Typography variant="body2" color="text.secondary">
|
| 370 |
+
A worker will run oeb.benchmark() and post the official scores to
|
| 371 |
+
the leaderboard.
|
| 372 |
</Typography>
|
| 373 |
|
| 374 |
<Stack direction="row" spacing={2}>
|
|
|
|
| 378 |
onClick={() => {
|
| 379 |
setSuccess(false);
|
| 380 |
setSubmittedData(null);
|
| 381 |
+
setFormData(INITIAL_FORM_DATA);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
}}
|
| 383 |
>
|
| 384 |
Submit another model
|
|
|
|
| 441 |
<Grid item xs={12}>
|
| 442 |
<Stack direction="row" spacing={1} alignItems="center">
|
| 443 |
<Typography variant="h6">Model Information</Typography>
|
| 444 |
+
<InfoIconWithTooltip tooltip={HELP_TEXTS.modelClass} />
|
| 445 |
</Stack>
|
| 446 |
</Grid>
|
| 447 |
|
| 448 |
+
<Grid item xs={12}>
|
| 449 |
+
<TextField
|
| 450 |
+
required
|
| 451 |
+
fullWidth
|
| 452 |
+
name="modelClass"
|
| 453 |
+
label="Model Class"
|
| 454 |
+
placeholder="braindecode.models.BIOT"
|
| 455 |
+
value={formData.modelClass}
|
| 456 |
+
onChange={handleChange}
|
| 457 |
+
helperText="e.g. braindecode.models.BIOT"
|
| 458 |
+
InputProps={{
|
| 459 |
+
endAdornment: (
|
| 460 |
+
<InfoIconWithTooltip tooltip={HELP_TEXTS.modelClass} />
|
| 461 |
+
),
|
| 462 |
+
}}
|
| 463 |
+
/>
|
| 464 |
+
</Grid>
|
| 465 |
+
|
| 466 |
+
<Grid item xs={12} sm={6}>
|
| 467 |
<TextField
|
| 468 |
required
|
| 469 |
fullWidth
|
| 470 |
name="modelName"
|
| 471 |
label="Model Name"
|
| 472 |
+
placeholder="BIOT-frozen"
|
| 473 |
value={formData.modelName}
|
| 474 |
onChange={handleChange}
|
| 475 |
+
helperText="Display name on the leaderboard"
|
| 476 |
InputProps={{
|
| 477 |
endAdornment: (
|
| 478 |
<InfoIconWithTooltip tooltip={HELP_TEXTS.modelName} />
|
|
|
|
| 481 |
/>
|
| 482 |
</Grid>
|
| 483 |
|
| 484 |
+
<Grid item xs={12} sm={6}>
|
| 485 |
+
<TextField
|
| 486 |
+
required
|
| 487 |
+
fullWidth
|
| 488 |
+
name="organization"
|
| 489 |
+
label="Organization"
|
| 490 |
+
placeholder="braindecode"
|
| 491 |
+
value={formData.organization}
|
| 492 |
+
onChange={handleChange}
|
| 493 |
+
helperText="Author or organization name"
|
| 494 |
+
/>
|
| 495 |
+
</Grid>
|
| 496 |
+
|
| 497 |
+
{/* Weights Source */}
|
| 498 |
+
<Grid item xs={12}>
|
| 499 |
+
<Stack direction="row" spacing={1} alignItems="center">
|
| 500 |
+
<Typography variant="h6">Weights Source</Typography>
|
| 501 |
+
<InfoIconWithTooltip tooltip={HELP_TEXTS.weightsSource} />
|
| 502 |
+
</Stack>
|
| 503 |
+
</Grid>
|
| 504 |
+
|
| 505 |
+
<Grid item xs={12}>
|
| 506 |
+
<FormControl>
|
| 507 |
+
<FormLabel id="weights-source-label">
|
| 508 |
+
Provide one weights source
|
| 509 |
+
</FormLabel>
|
| 510 |
+
<RadioGroup
|
| 511 |
+
row
|
| 512 |
+
aria-labelledby="weights-source-label"
|
| 513 |
+
name="weightsSource"
|
| 514 |
+
value={formData.weightsSource}
|
| 515 |
+
onChange={handleChange}
|
| 516 |
+
>
|
| 517 |
+
<FormControlLabel
|
| 518 |
+
value="hub_repo"
|
| 519 |
+
control={<Radio />}
|
| 520 |
+
label="Hugging Face Hub repo"
|
| 521 |
+
/>
|
| 522 |
+
<FormControlLabel
|
| 523 |
+
value="checkpoint_url"
|
| 524 |
+
control={<Radio />}
|
| 525 |
+
label="Checkpoint URL"
|
| 526 |
+
/>
|
| 527 |
+
</RadioGroup>
|
| 528 |
+
</FormControl>
|
| 529 |
+
</Grid>
|
| 530 |
+
|
| 531 |
+
{formData.weightsSource === "hub_repo" ? (
|
| 532 |
+
<Grid item xs={12}>
|
| 533 |
+
<TextField
|
| 534 |
+
fullWidth
|
| 535 |
+
name="hubRepo"
|
| 536 |
+
label="Hub Repository"
|
| 537 |
+
placeholder="organization/model-weights"
|
| 538 |
+
value={formData.hubRepo}
|
| 539 |
+
onChange={handleChange}
|
| 540 |
+
helperText="Public HF Hub repo containing the weights (with a license tag)"
|
| 541 |
+
/>
|
| 542 |
+
</Grid>
|
| 543 |
+
) : (
|
| 544 |
+
<Grid item xs={12}>
|
| 545 |
+
<TextField
|
| 546 |
+
fullWidth
|
| 547 |
+
name="checkpointUrl"
|
| 548 |
+
label="Checkpoint URL"
|
| 549 |
+
placeholder="https://example.com/weights.pth"
|
| 550 |
+
value={formData.checkpointUrl}
|
| 551 |
+
onChange={handleChange}
|
| 552 |
+
helperText="Direct http(s) URL to a checkpoint file"
|
| 553 |
+
/>
|
| 554 |
+
</Grid>
|
| 555 |
+
)}
|
| 556 |
+
|
| 557 |
+
<Grid item xs={12}>
|
| 558 |
<TextField
|
| 559 |
fullWidth
|
| 560 |
+
name="headModuleName"
|
| 561 |
+
label="Head Module Name"
|
| 562 |
+
value={formData.headModuleName}
|
| 563 |
onChange={handleChange}
|
| 564 |
+
helperText="Default: final_layer"
|
| 565 |
InputProps={{
|
| 566 |
endAdornment: (
|
| 567 |
+
<InfoIconWithTooltip
|
| 568 |
+
tooltip={HELP_TEXTS.headModuleName}
|
| 569 |
+
/>
|
| 570 |
),
|
| 571 |
}}
|
| 572 |
/>
|
| 573 |
</Grid>
|
| 574 |
|
| 575 |
+
{/* Evaluation Configuration */}
|
| 576 |
<Grid item xs={12}>
|
| 577 |
<Stack direction="row" spacing={1} alignItems="center">
|
| 578 |
+
<Typography variant="h6">Evaluation Configuration</Typography>
|
| 579 |
</Stack>
|
| 580 |
</Grid>
|
| 581 |
|
| 582 |
<Grid item xs={12} sm={6}>
|
| 583 |
<FormControl fullWidth>
|
| 584 |
+
<InputLabel>Fine-tuning Strategies</InputLabel>
|
| 585 |
<Select
|
| 586 |
+
multiple
|
| 587 |
+
name="finetuningStrategies"
|
| 588 |
+
value={formData.finetuningStrategies}
|
| 589 |
onChange={handleChange}
|
| 590 |
+
label="Fine-tuning Strategies"
|
| 591 |
+
renderValue={(selected) =>
|
| 592 |
+
selected.map(strategyLabel).join(", ")
|
| 593 |
+
}
|
| 594 |
endAdornment={
|
| 595 |
<InfoIconWithTooltip
|
| 596 |
+
tooltip={HELP_TEXTS.finetuningStrategies}
|
| 597 |
sx={{ mr: 2 }}
|
| 598 |
/>
|
| 599 |
}
|
| 600 |
>
|
| 601 |
+
{FINETUNING_STRATEGIES.map((strategy) => (
|
| 602 |
+
<MenuItem key={strategy.value} value={strategy.value}>
|
| 603 |
+
{strategy.label}
|
| 604 |
</MenuItem>
|
| 605 |
))}
|
| 606 |
</Select>
|
|
|
|
| 609 |
|
| 610 |
<Grid item xs={12} sm={6}>
|
| 611 |
<FormControl fullWidth>
|
| 612 |
+
<InputLabel>Heads</InputLabel>
|
| 613 |
<Select
|
| 614 |
+
multiple
|
| 615 |
+
name="heads"
|
| 616 |
+
value={formData.heads}
|
| 617 |
onChange={handleChange}
|
| 618 |
+
label="Heads"
|
| 619 |
+
renderValue={(selected) =>
|
| 620 |
+
selected.map(headLabel).join(", ")
|
| 621 |
+
}
|
| 622 |
endAdornment={
|
| 623 |
<InfoIconWithTooltip
|
| 624 |
+
tooltip={HELP_TEXTS.heads}
|
| 625 |
sx={{ mr: 2 }}
|
| 626 |
/>
|
| 627 |
}
|
| 628 |
>
|
| 629 |
+
{HEADS.map((head) => (
|
| 630 |
+
<MenuItem key={head.value} value={head.value}>
|
| 631 |
+
{head.label}
|
| 632 |
</MenuItem>
|
| 633 |
))}
|
| 634 |
</Select>
|
| 635 |
</FormControl>
|
| 636 |
</Grid>
|
| 637 |
|
| 638 |
+
{showPeftTargetModules && (
|
| 639 |
+
<Grid item xs={12} sm={showPeftFfModules ? 6 : 12}>
|
| 640 |
+
<TextField
|
| 641 |
+
fullWidth
|
| 642 |
+
name="peftTargetModules"
|
| 643 |
+
label="PEFT Target Modules"
|
| 644 |
+
placeholder="to_q, to_v"
|
| 645 |
+
value={formData.peftTargetModules}
|
| 646 |
+
onChange={handleChange}
|
| 647 |
+
helperText="comma-separated module names"
|
| 648 |
+
InputProps={{
|
| 649 |
+
endAdornment: (
|
| 650 |
+
<InfoIconWithTooltip
|
| 651 |
+
tooltip={HELP_TEXTS.peftTargetModules}
|
| 652 |
+
/>
|
| 653 |
+
),
|
| 654 |
+
}}
|
| 655 |
+
/>
|
| 656 |
+
</Grid>
|
| 657 |
+
)}
|
| 658 |
+
|
| 659 |
+
{showPeftFfModules && (
|
| 660 |
+
<Grid item xs={12} sm={6}>
|
| 661 |
+
<TextField
|
| 662 |
+
fullWidth
|
| 663 |
+
name="peftFfModules"
|
| 664 |
+
label="PEFT Feed-Forward Modules"
|
| 665 |
+
placeholder="to_ff"
|
| 666 |
+
value={formData.peftFfModules}
|
| 667 |
+
onChange={handleChange}
|
| 668 |
+
helperText="comma-separated module names (IA3 only)"
|
| 669 |
+
InputProps={{
|
| 670 |
+
endAdornment: (
|
| 671 |
+
<InfoIconWithTooltip
|
| 672 |
+
tooltip={HELP_TEXTS.peftFfModules}
|
| 673 |
+
/>
|
| 674 |
+
),
|
| 675 |
+
}}
|
| 676 |
+
/>
|
| 677 |
+
</Grid>
|
| 678 |
+
)}
|
| 679 |
+
|
| 680 |
+
<Grid item xs={12} sm={8}>
|
| 681 |
+
<FormControl fullWidth>
|
| 682 |
+
<InputLabel>Datasets</InputLabel>
|
| 683 |
+
<Select
|
| 684 |
+
multiple
|
| 685 |
+
name="datasets"
|
| 686 |
+
value={formData.datasets}
|
| 687 |
+
onChange={handleChange}
|
| 688 |
+
label="Datasets"
|
| 689 |
+
renderValue={(selected) =>
|
| 690 |
+
selected.length ? selected.join(", ") : "All 10 datasets"
|
| 691 |
+
}
|
| 692 |
+
endAdornment={
|
| 693 |
+
<InfoIconWithTooltip
|
| 694 |
+
tooltip={HELP_TEXTS.datasets}
|
| 695 |
+
sx={{ mr: 2 }}
|
| 696 |
+
/>
|
| 697 |
+
}
|
| 698 |
+
>
|
| 699 |
+
{OEB_DATASETS.map((dataset) => (
|
| 700 |
+
<MenuItem key={dataset} value={dataset}>
|
| 701 |
+
{dataset}
|
| 702 |
+
</MenuItem>
|
| 703 |
+
))}
|
| 704 |
+
</Select>
|
| 705 |
+
</FormControl>
|
| 706 |
+
</Grid>
|
| 707 |
+
|
| 708 |
+
<Grid item xs={12} sm={4}>
|
| 709 |
+
<TextField
|
| 710 |
+
fullWidth
|
| 711 |
+
type="number"
|
| 712 |
+
name="nSeeds"
|
| 713 |
+
label="Number of Seeds"
|
| 714 |
+
value={formData.nSeeds}
|
| 715 |
+
onChange={handleChange}
|
| 716 |
+
helperText="Default: 3"
|
| 717 |
+
inputProps={{ min: 1 }}
|
| 718 |
+
InputProps={{
|
| 719 |
+
endAdornment: (
|
| 720 |
+
<InfoIconWithTooltip tooltip={HELP_TEXTS.nSeeds} />
|
| 721 |
+
),
|
| 722 |
+
}}
|
| 723 |
+
/>
|
| 724 |
+
</Grid>
|
| 725 |
+
|
| 726 |
+
{/* Additional Information */}
|
| 727 |
<Grid item xs={12}>
|
| 728 |
+
<Stack direction="row" spacing={1} alignItems="center">
|
| 729 |
+
<Typography variant="h6">Additional Information</Typography>
|
| 730 |
+
</Stack>
|
| 731 |
+
</Grid>
|
| 732 |
+
|
| 733 |
+
<Grid item xs={12} sm={6}>
|
| 734 |
+
<TextField
|
| 735 |
+
fullWidth
|
| 736 |
+
name="modelUrl"
|
| 737 |
+
label="Model URL"
|
| 738 |
+
placeholder="https://huggingface.co/organization/model"
|
| 739 |
+
value={formData.modelUrl}
|
| 740 |
+
onChange={handleChange}
|
| 741 |
+
helperText="Optional. Link to the model card or repo."
|
| 742 |
+
/>
|
| 743 |
+
</Grid>
|
| 744 |
+
|
| 745 |
+
<Grid item xs={12} sm={6}>
|
| 746 |
+
<TextField
|
| 747 |
+
fullWidth
|
| 748 |
+
name="paperUrl"
|
| 749 |
+
label="Paper URL"
|
| 750 |
+
placeholder="https://arxiv.org/abs/..."
|
| 751 |
+
value={formData.paperUrl}
|
| 752 |
+
onChange={handleChange}
|
| 753 |
+
helperText="Optional. Link to the related paper."
|
| 754 |
+
/>
|
| 755 |
+
</Grid>
|
| 756 |
+
|
| 757 |
+
<Grid item xs={12} sm={6}>
|
| 758 |
<TextField
|
| 759 |
fullWidth
|
| 760 |
+
name="contactEmail"
|
| 761 |
+
label="Contact Email"
|
| 762 |
+
placeholder="you@example.com"
|
| 763 |
+
value={formData.contactEmail}
|
| 764 |
onChange={handleChange}
|
| 765 |
+
helperText="Optional. Stored privately."
|
| 766 |
InputProps={{
|
| 767 |
endAdornment: (
|
| 768 |
+
<InfoIconWithTooltip tooltip={HELP_TEXTS.contactEmail} />
|
| 769 |
),
|
| 770 |
}}
|
| 771 |
/>
|
| 772 |
</Grid>
|
| 773 |
|
| 774 |
+
<Grid item xs={12} sm={6}>
|
| 775 |
+
<FormControl fullWidth>
|
| 776 |
+
<InputLabel>Visibility</InputLabel>
|
| 777 |
+
<Select
|
| 778 |
+
name="visibility"
|
| 779 |
+
value={formData.visibility}
|
| 780 |
+
onChange={handleChange}
|
| 781 |
+
label="Visibility"
|
| 782 |
+
endAdornment={
|
| 783 |
+
<InfoIconWithTooltip
|
| 784 |
+
tooltip={HELP_TEXTS.visibility}
|
| 785 |
+
sx={{ mr: 2 }}
|
| 786 |
+
/>
|
| 787 |
+
}
|
| 788 |
+
>
|
| 789 |
+
{VISIBILITY_OPTIONS.map((option) => (
|
| 790 |
+
<MenuItem key={option} value={option}>
|
| 791 |
+
{option}
|
| 792 |
+
</MenuItem>
|
| 793 |
+
))}
|
| 794 |
+
</Select>
|
| 795 |
+
</FormControl>
|
| 796 |
+
</Grid>
|
| 797 |
+
|
| 798 |
+
<Grid item xs={12}>
|
| 799 |
+
<TextField
|
| 800 |
+
fullWidth
|
| 801 |
+
multiline
|
| 802 |
+
minRows={2}
|
| 803 |
+
name="additionalInfo"
|
| 804 |
+
label="Additional Info"
|
| 805 |
+
value={formData.additionalInfo}
|
| 806 |
+
onChange={handleChange}
|
| 807 |
+
helperText="Optional. Anything else we should know."
|
| 808 |
+
/>
|
| 809 |
+
</Grid>
|
| 810 |
+
|
| 811 |
{/* Submit Button */}
|
| 812 |
<Grid item xs={12}>
|
| 813 |
<Box
|
|
@@ -50,32 +50,36 @@ const StepNumber = ({ number }) => (
|
|
| 50 |
</Box>
|
| 51 |
);
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
const TUTORIAL_STEPS = [
|
| 54 |
{
|
| 55 |
-
title: "
|
| 56 |
-
content: (
|
| 57 |
-
<Stack spacing={2}>
|
| 58 |
-
<Typography variant="body2" color="text.secondary">
|
| 59 |
-
Your model should be <strong>public</strong> on the Hub and follow the{" "}
|
| 60 |
-
<strong>username/model-id</strong> format (e.g.
|
| 61 |
-
braindecode/labram-lora-bcic2a). Specify the <strong>revision</strong>{" "}
|
| 62 |
-
(commit hash or branch) and <strong>adapter method</strong> used
|
| 63 |
-
for fine-tuning.
|
| 64 |
-
</Typography>
|
| 65 |
-
<DocLink href="https://huggingface.co/docs/hub/models-uploading">
|
| 66 |
-
Model uploading guide
|
| 67 |
-
</DocLink>
|
| 68 |
-
</Stack>
|
| 69 |
-
),
|
| 70 |
-
},
|
| 71 |
-
{
|
| 72 |
-
title: "Technical Details",
|
| 73 |
content: (
|
| 74 |
<Stack spacing={2}>
|
| 75 |
<Typography variant="body2" color="text.secondary">
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
</Typography>
|
| 80 |
<Box
|
| 81 |
sx={{
|
|
@@ -94,72 +98,76 @@ const TUTORIAL_STEPS = [
|
|
| 94 |
}}
|
| 95 |
>
|
| 96 |
<pre>
|
| 97 |
-
{`
|
| 98 |
-
from peft import get_peft_model, LoraConfig
|
| 99 |
-
|
| 100 |
-
# Load the foundation model
|
| 101 |
-
model = LaBraM(n_chans=22, n_outputs=4, n_times=1000)
|
| 102 |
|
| 103 |
-
#
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
)
|
| 109 |
-
|
| 110 |
-
print(model.print_trainable_parameters())`}
|
| 111 |
</pre>
|
| 112 |
</Box>
|
| 113 |
-
<DocLink href="https://
|
| 114 |
-
|
| 115 |
</DocLink>
|
| 116 |
</Stack>
|
| 117 |
),
|
| 118 |
},
|
| 119 |
{
|
| 120 |
-
title: "
|
| 121 |
content: (
|
| 122 |
<Stack spacing={2}>
|
| 123 |
<Typography variant="body2" color="text.secondary">
|
| 124 |
-
|
| 125 |
-
<strong>
|
| 126 |
-
|
|
|
|
| 127 |
</Typography>
|
| 128 |
-
<DocLink href="https://huggingface.co/docs/hub/
|
| 129 |
-
|
| 130 |
</DocLink>
|
| 131 |
</Stack>
|
| 132 |
),
|
| 133 |
},
|
| 134 |
{
|
| 135 |
-
title: "
|
| 136 |
content: (
|
| 137 |
<Stack spacing={2}>
|
| 138 |
<Typography variant="body2" color="text.secondary">
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
<strong>
|
| 142 |
-
<strong>
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
</Typography>
|
| 145 |
-
<DocLink href="https://huggingface.co/docs/hub/model-cards">
|
| 146 |
-
Model cards guide
|
| 147 |
-
</DocLink>
|
| 148 |
</Stack>
|
| 149 |
),
|
| 150 |
},
|
| 151 |
{
|
| 152 |
-
title: "
|
| 153 |
content: (
|
| 154 |
<Stack spacing={2}>
|
| 155 |
<Typography variant="body2" color="text.secondary">
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
<strong>license tag</strong>, and <strong>loads correctly</strong>{" "}
|
| 159 |
-
with braindecode + PEFT.
|
| 160 |
</Typography>
|
| 161 |
-
<DocLink href="https://
|
| 162 |
-
|
| 163 |
</DocLink>
|
| 164 |
</Stack>
|
| 165 |
),
|
|
@@ -276,6 +284,52 @@ function SubmissionGuide() {
|
|
| 276 |
)}
|
| 277 |
</Box>
|
| 278 |
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
</Stack>
|
| 280 |
</Box>
|
| 281 |
</Collapse>
|
|
|
|
| 50 |
</Box>
|
| 51 |
);
|
| 52 |
|
| 53 |
+
const CITATION = `@software{open_eeg_bench,
|
| 54 |
+
title = {Open EEG Bench: Benchmarking parameter-efficient fine-tuning of EEG foundation models},
|
| 55 |
+
author = {Guetschel, Pierre and Aristimunha, Bruno and Truong, Dung and Kokate, Kuntal and Moreau, Thomas and Tangermann, Michael and Delorme, Arnaud},
|
| 56 |
+
year = {2026},
|
| 57 |
+
url = {https://github.com/braindecode/OpenEEGBench},
|
| 58 |
+
doi = {10.5281/zenodo.19698863}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
@inproceedings{guetschel2026openeegbench,
|
| 62 |
+
title = {Toward {OpenEEG-Bench}: A Live Community-Driven Benchmark for {EEG} Foundation Models},
|
| 63 |
+
author = {Guetschel, Pierre and Aristimunha, Bruno and Truong, Dung and Kokate, Kuntal and Tangermann, Michael and Delorme, Arnaud},
|
| 64 |
+
booktitle = {Proceedings of the 34th European Signal Processing Conference (EUSIPCO 2026)},
|
| 65 |
+
year = {2026},
|
| 66 |
+
address = {Bruges, Belgium},
|
| 67 |
+
month = aug,
|
| 68 |
+
pages = {1--5},
|
| 69 |
+
organization = {EURASIP}
|
| 70 |
+
}`;
|
| 71 |
+
|
| 72 |
const TUTORIAL_STEPS = [
|
| 73 |
{
|
| 74 |
+
title: "Install & implement the model contract",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
content: (
|
| 76 |
<Stack spacing={2}>
|
| 77 |
<Typography variant="body2" color="text.secondary">
|
| 78 |
+
Install the benchmark with{" "}
|
| 79 |
+
<strong>pip install open-eeg-bench</strong>. Your model must accept
|
| 80 |
+
input of shape <strong>(batch, n_chans, n_times)</strong> and return{" "}
|
| 81 |
+
<strong>(batch, n_outputs)</strong>, exposing a named classification
|
| 82 |
+
head module (default <strong>final_layer</strong>).
|
| 83 |
</Typography>
|
| 84 |
<Box
|
| 85 |
sx={{
|
|
|
|
| 98 |
}}
|
| 99 |
>
|
| 100 |
<pre>
|
| 101 |
+
{`import open_eeg_bench as oeb
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
# Your model: (batch, n_chans, n_times) -> (batch, n_outputs)
|
| 104 |
+
results = oeb.benchmark(
|
| 105 |
+
model_cls="my_package.MyModel",
|
| 106 |
+
checkpoint_url="https://my-weights.pth",
|
| 107 |
+
head_module_name="final_layer",
|
| 108 |
)
|
| 109 |
+
print(results) # pd.DataFrame`}
|
|
|
|
| 110 |
</pre>
|
| 111 |
</Box>
|
| 112 |
+
<DocLink href="https://github.com/braindecode/OpenEEGBench">
|
| 113 |
+
OpenEEGBench repository
|
| 114 |
</DocLink>
|
| 115 |
</Stack>
|
| 116 |
),
|
| 117 |
},
|
| 118 |
{
|
| 119 |
+
title: "Publish your weights",
|
| 120 |
content: (
|
| 121 |
<Stack spacing={2}>
|
| 122 |
<Typography variant="body2" color="text.secondary">
|
| 123 |
+
Push your checkpoint to a <strong>public HuggingFace Hub repo</strong>{" "}
|
| 124 |
+
(with a <strong>license tag</strong>) or host it at a direct{" "}
|
| 125 |
+
<strong>checkpoint URL</strong>. The arena loads the weights to run
|
| 126 |
+
the evaluation.
|
| 127 |
</Typography>
|
| 128 |
+
<DocLink href="https://huggingface.co/docs/hub/models-uploading">
|
| 129 |
+
Model uploading guide
|
| 130 |
</DocLink>
|
| 131 |
</Stack>
|
| 132 |
),
|
| 133 |
},
|
| 134 |
{
|
| 135 |
+
title: "Sign in & fill the form",
|
| 136 |
content: (
|
| 137 |
<Stack spacing={2}>
|
| 138 |
<Typography variant="body2" color="text.secondary">
|
| 139 |
+
Provide your <strong>model class import path</strong>, the{" "}
|
| 140 |
+
<strong>weights source</strong>, a{" "}
|
| 141 |
+
<strong>fine-tuning strategy</strong> and{" "}
|
| 142 |
+
<strong>classification head</strong>, the{" "}
|
| 143 |
+
<strong>datasets</strong> (default: all 10), and the number of{" "}
|
| 144 |
+
<strong>seeds</strong>.
|
| 145 |
+
</Typography>
|
| 146 |
+
</Stack>
|
| 147 |
+
),
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
title: "Submit & wait",
|
| 151 |
+
content: (
|
| 152 |
+
<Stack spacing={2}>
|
| 153 |
+
<Typography variant="body2" color="text.secondary">
|
| 154 |
+
The arena queues your model and a worker runs{" "}
|
| 155 |
+
<strong>oeb.benchmark()</strong> to post official scores. Limit:{" "}
|
| 156 |
+
<strong>5 submissions per month</strong>.
|
| 157 |
</Typography>
|
|
|
|
|
|
|
|
|
|
| 158 |
</Stack>
|
| 159 |
),
|
| 160 |
},
|
| 161 |
{
|
| 162 |
+
title: "Privacy & help",
|
| 163 |
content: (
|
| 164 |
<Stack spacing={2}>
|
| 165 |
<Typography variant="body2" color="text.secondary">
|
| 166 |
+
Your <strong>contact email</strong> is stored privately. For help or
|
| 167 |
+
to request removal of a submission, open an issue.
|
|
|
|
|
|
|
| 168 |
</Typography>
|
| 169 |
+
<DocLink href="https://github.com/braindecode/OpenEEGBench/issues">
|
| 170 |
+
OpenEEGBench issues
|
| 171 |
</DocLink>
|
| 172 |
</Stack>
|
| 173 |
),
|
|
|
|
| 284 |
)}
|
| 285 |
</Box>
|
| 286 |
))}
|
| 287 |
+
<Box
|
| 288 |
+
sx={{
|
| 289 |
+
mt: 4,
|
| 290 |
+
pt: 4,
|
| 291 |
+
px: 4,
|
| 292 |
+
borderTop: "1px solid",
|
| 293 |
+
borderColor: (theme) =>
|
| 294 |
+
theme.palette.mode === "dark" ? "grey.800" : "grey.100",
|
| 295 |
+
}}
|
| 296 |
+
>
|
| 297 |
+
<Stack spacing={2}>
|
| 298 |
+
<Typography
|
| 299 |
+
variant="subtitle1"
|
| 300 |
+
sx={{
|
| 301 |
+
fontWeight: 600,
|
| 302 |
+
color: "text.primary",
|
| 303 |
+
letterSpacing: "-0.01em",
|
| 304 |
+
}}
|
| 305 |
+
>
|
| 306 |
+
Citation
|
| 307 |
+
</Typography>
|
| 308 |
+
<Typography variant="body2" color="text.secondary">
|
| 309 |
+
If you use OpenEEGBench in your research, please cite it.
|
| 310 |
+
</Typography>
|
| 311 |
+
<Box
|
| 312 |
+
sx={{
|
| 313 |
+
p: 2,
|
| 314 |
+
bgcolor: (theme) =>
|
| 315 |
+
theme.palette.mode === "dark" ? "grey.50" : "grey.900",
|
| 316 |
+
borderRadius: 1,
|
| 317 |
+
overflowX: "auto",
|
| 318 |
+
"& pre": {
|
| 319 |
+
m: 0,
|
| 320 |
+
p: 0,
|
| 321 |
+
fontFamily: "monospace",
|
| 322 |
+
fontSize: "0.8125rem",
|
| 323 |
+
whiteSpace: "pre",
|
| 324 |
+
color: (theme) =>
|
| 325 |
+
theme.palette.mode === "dark" ? "grey.900" : "grey.50",
|
| 326 |
+
},
|
| 327 |
+
}}
|
| 328 |
+
>
|
| 329 |
+
<pre>{CITATION}</pre>
|
| 330 |
+
</Box>
|
| 331 |
+
</Stack>
|
| 332 |
+
</Box>
|
| 333 |
</Stack>
|
| 334 |
</Box>
|
| 335 |
</Collapse>
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import React, { useState, useEffect } from "react";
|
| 2 |
import { Alert, Box, CircularProgress } from "@mui/material";
|
| 3 |
|
| 4 |
-
const
|
| 5 |
|
| 6 |
function SubmissionLimitChecker({ user, children }) {
|
| 7 |
const [loading, setLoading] = useState(true);
|
|
@@ -17,7 +17,7 @@ function SubmissionLimitChecker({ user, children }) {
|
|
| 17 |
|
| 18 |
try {
|
| 19 |
const response = await fetch(
|
| 20 |
-
`/api/models/organization/${user.username}/submissions?days=
|
| 21 |
);
|
| 22 |
if (!response.ok) {
|
| 23 |
throw new Error("Failed to fetch submission data");
|
|
@@ -25,7 +25,7 @@ function SubmissionLimitChecker({ user, children }) {
|
|
| 25 |
|
| 26 |
const submissions = await response.json();
|
| 27 |
console.log(`Recent submissions for ${user.username}:`, submissions);
|
| 28 |
-
setReachedLimit(submissions.length >=
|
| 29 |
setError(false);
|
| 30 |
} catch (error) {
|
| 31 |
console.error("Error checking submission limit:", error);
|
|
@@ -74,7 +74,8 @@ function SubmissionLimitChecker({ user, children }) {
|
|
| 74 |
}}
|
| 75 |
>
|
| 76 |
For fairness reasons, you cannot submit more than{" "}
|
| 77 |
-
{
|
|
|
|
| 78 |
</Alert>
|
| 79 |
);
|
| 80 |
}
|
|
|
|
| 1 |
import React, { useState, useEffect } from "react";
|
| 2 |
import { Alert, Box, CircularProgress } from "@mui/material";
|
| 3 |
|
| 4 |
+
const MAX_SUBMISSIONS_PER_MONTH = 5;
|
| 5 |
|
| 6 |
function SubmissionLimitChecker({ user, children }) {
|
| 7 |
const [loading, setLoading] = useState(true);
|
|
|
|
| 17 |
|
| 18 |
try {
|
| 19 |
const response = await fetch(
|
| 20 |
+
`/api/models/organization/${user.username}/submissions?days=30`
|
| 21 |
);
|
| 22 |
if (!response.ok) {
|
| 23 |
throw new Error("Failed to fetch submission data");
|
|
|
|
| 25 |
|
| 26 |
const submissions = await response.json();
|
| 27 |
console.log(`Recent submissions for ${user.username}:`, submissions);
|
| 28 |
+
setReachedLimit(submissions.length >= MAX_SUBMISSIONS_PER_MONTH);
|
| 29 |
setError(false);
|
| 30 |
} catch (error) {
|
| 31 |
console.error("Error checking submission limit:", error);
|
|
|
|
| 74 |
}}
|
| 75 |
>
|
| 76 |
For fairness reasons, you cannot submit more than{" "}
|
| 77 |
+
{MAX_SUBMISSIONS_PER_MONTH} submissions per month. Please try again next
|
| 78 |
+
month.
|
| 79 |
</Alert>
|
| 80 |
);
|
| 81 |
}
|
|
@@ -88,3 +88,27 @@ export const getModelTypeOrder = (type) => {
|
|
| 88 |
);
|
| 89 |
return matchedType ? matchedType[1].order : Infinity;
|
| 90 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
);
|
| 89 |
return matchedType ? matchedType[1].order : Infinity;
|
| 90 |
};
|
| 91 |
+
|
| 92 |
+
// OpenEEGBench submission options — fine-tuning strategies, heads, and the
|
| 93 |
+
// classification-only dataset set accepted by oeb.benchmark().
|
| 94 |
+
export const FINETUNING_STRATEGIES = [
|
| 95 |
+
{ value: "frozen", label: "Frozen (linear probing)" },
|
| 96 |
+
{ value: "ridge_probe", label: "Ridge probe" },
|
| 97 |
+
{ value: "lora", label: "LoRA" },
|
| 98 |
+
{ value: "ia3", label: "IA3" },
|
| 99 |
+
{ value: "adalora", label: "AdaLoRA" },
|
| 100 |
+
{ value: "dora", label: "DoRA" },
|
| 101 |
+
{ value: "oft", label: "OFT" },
|
| 102 |
+
{ value: "full_finetune", label: "Full fine-tune" },
|
| 103 |
+
{ value: "two_stages", label: "Two stages" },
|
| 104 |
+
];
|
| 105 |
+
export const HEADS = [
|
| 106 |
+
{ value: "linear_head", label: "Linear head" },
|
| 107 |
+
{ value: "mlp_head", label: "MLP head" },
|
| 108 |
+
{ value: "original_head", label: "Original head" },
|
| 109 |
+
];
|
| 110 |
+
export const PEFT_STRATEGIES = ["lora", "ia3", "adalora", "dora", "oft"];
|
| 111 |
+
export const OEB_DATASETS = [
|
| 112 |
+
"bcic2a", "physionet", "bcic2020_3", "isruc_sleep", "tuab",
|
| 113 |
+
"tuev", "mdd_mumtaz2016", "arithmetic_zyma2019", "faced", "seed_v",
|
| 114 |
+
];
|