verena commited on
Commit
efa3e66
Β·
1 Parent(s): 1112710

8/28: submission + scoring clarity; track 2 quota increase; FAQ updates

Browse files
Files changed (8) hide show
  1. app.py +1 -1
  2. config.py +2 -0
  3. tabs/about.py +8 -2
  4. tabs/faq.py +30 -7
  5. tabs/leaderboard.py +6 -2
  6. tabs/submit_track1.py +29 -11
  7. tabs/submit_track2.py +72 -94
  8. utils.py +32 -14
app.py CHANGED
@@ -107,7 +107,7 @@ with gr.Blocks(title="Rare Disease, Real Kid: The MVA Hackathon 2026") as app:
107
  quota_t1 = submit_track1.render()
108
  status_t2 = submit_track2.render()
109
  app.load(fn=submit_track1._quota_status, outputs=quota_t1)
110
- app.load(fn=submit_track2._submission_status, outputs=status_t2)
111
  faq.render()
112
  rules.render()
113
 
 
107
  quota_t1 = submit_track1.render()
108
  status_t2 = submit_track2.render()
109
  app.load(fn=submit_track1._quota_status, outputs=quota_t1)
110
+ app.load(fn=submit_track2._quota_status, outputs=status_t2)
111
  faq.render()
112
  rules.render()
113
 
config.py CHANGED
@@ -5,6 +5,7 @@ In particular, the following constants are
5
  LEADERBOARD_HEADERS - columns displayed on the leaderboard
6
  CHALLENGE_ACTIVE - set to False to hide leaderboard submission tabs
7
  MAX_TRACK1_SUBMISSIONS - how many Track 1 uploads each participant (a HF user) is allowed
 
8
  """
9
 
10
  from dotenv import load_dotenv
@@ -43,3 +44,4 @@ LEADERBOARD_HEADERS = ["#", "Participant", "Rank Points", "F-max", "Model", "Sub
43
  # Challenge control
44
  CHALLENGE_ACTIVE = True
45
  MAX_TRACK1_SUBMISSIONS = 6
 
 
5
  LEADERBOARD_HEADERS - columns displayed on the leaderboard
6
  CHALLENGE_ACTIVE - set to False to hide leaderboard submission tabs
7
  MAX_TRACK1_SUBMISSIONS - how many Track 1 uploads each participant (a HF user) is allowed
8
+ MAX_TRACK2_SUBMISSIONS - how many Track 2 uploads each participant (a HF user) is allowed
9
  """
10
 
11
  from dotenv import load_dotenv
 
44
  # Challenge control
45
  CHALLENGE_ACTIVE = True
46
  MAX_TRACK1_SUBMISSIONS = 6
47
+ MAX_TRACK2_SUBMISSIONS = 3
tabs/about.py CHANGED
@@ -93,7 +93,9 @@ def _build_html(img_child: str) -> str:
93
  </ul>
94
  <span class="sub-heading">Scoring</span>
95
  <p>
96
- Track 1 is scored automatically on two primary metrics:
 
 
97
  </p>
98
  <ul>
99
  <li><strong>Rank points</strong> - How high did you rank the true variant(s). If your top-ranked
@@ -104,6 +106,10 @@ def _build_html(img_child: str) -> str:
104
  any confidence threshold, rewarding submissions that pinpoint the right answer without burying it
105
  in noise. Secondary or incidental findings won't hurt your automated score.</li>
106
  </ul>
 
 
 
 
107
  </div>
108
 
109
  <div class="info-card track-card">
@@ -124,7 +130,7 @@ def _build_html(img_child: str) -> str:
124
  </p>
125
  <ul class="note-small">
126
  <li>What you submit: A detailed report, a Github link, and a 3-minute pitch video</li>
127
- <li>Submission limit: 1</li>
128
  </ul>
129
  <span class="sub-heading">Scoring</span>
130
  <p>
 
93
  </ul>
94
  <span class="sub-heading">Scoring</span>
95
  <p>
96
+ <strong>This is a foundational track</strong> β€” the goal is to validate that your pipeline can
97
+ recover a clinically confirmed variant from real patient data. Prediction files are scored automatically on
98
+ two primary metrics:
99
  </p>
100
  <ul>
101
  <li><strong>Rank points</strong> - How high did you rank the true variant(s). If your top-ranked
 
106
  any confidence threshold, rewarding submissions that pinpoint the right answer without burying it
107
  in noise. Secondary or incidental findings won't hurt your automated score.</li>
108
  </ul>
109
+ <p>
110
+ Your <strong>methods write-up</strong> is reviewed separately by a judging panel for scientific rigor and
111
+ depth of understanding.
112
+ </p>
113
  </div>
114
 
115
  <div class="info-card track-card">
 
130
  </p>
131
  <ul class="note-small">
132
  <li>What you submit: A detailed report, a Github link, and a 3-minute pitch video</li>
133
+ <li>Submission limit: 3</li>
134
  </ul>
135
  <span class="sub-heading">Scoring</span>
136
  <p>
tabs/faq.py CHANGED
@@ -7,6 +7,7 @@ import gradio as gr
7
  from config import DATASET_URL, DISCUSSIONS_URL
8
 
9
  FAQ_ITEMS = [
 
10
  (
11
  "Who can participate?",
12
  "Anyone with a Hugging Face account can participate - data scientists, ML engineers, clinicians, "
@@ -24,13 +25,29 @@ FAQ_ITEMS = [
24
  "on the team's behalf - only one Track 2 submission per team is accepted, and additional submissions from "
25
  "other team members will be ignored.",
26
  ),
 
 
 
 
 
 
 
27
  (
28
  "How is Track 1 scored?",
29
  "Submissions are scored the way large-scale rare disease benchmarks (like Stenton et al., 2024) score solved "
30
  "cases applied to this single, clinically confirmed answer.\n\nTwo metrics are computed automatically: **rank points** "
31
  "(based on how high the true variant(s) land in your ranked list, with partial credit if you recover only one of "
32
- "two compound-heterozygous variants) and **F-max** (the best precision/recall balance across your submitted"
33
- "confidence thresholds).",
 
 
 
 
 
 
 
 
 
34
  ),
35
  (
36
  "Are secondary or incidental findings scored?",
@@ -39,24 +56,30 @@ FAQ_ITEMS = [
39
  "since there's no fixed \"correct\" answer for secondary findings the way there is for the primary causal variant.\n\n"
40
  "Use the optional `notes` column to briefly explain why you're flagging it (e.g. \"well-established pathogenic "
41
  "variant, unrelated to primary phenotype, recommend clinical follow-up\")",
42
-
43
  ),
44
  (
45
  "How is Track 2 scored?",
46
  "Reports are reviewed by an independent panel, and will be judged on scientific rigor, potential impact, innovation, "
47
  "and scalability.",
48
  ),
 
49
  (
50
- "How many submissions can I make?",
51
- "Track 1 allows up to **6 submissions** per participant; only your highest-scoring submission appears on the "
52
- "leaderboard. Track 2 accepts **one final submission** per participant. No re-submissions allowed, so make "
53
- "them count.",
54
  ),
55
  (
56
  "What compute resources are available?",
57
  "This Space runs on CPU-basic hardware. You are welcome to use your own compute for training, as only "
58
  "the final submission files need to be uploaded here.",
59
  ),
 
 
 
 
 
 
 
60
  (
61
  "What is the data license?",
62
  "The underlying dataset is **gated** and subject to the terms of the Hackathon Rules and Data Transfer Agreement "
 
7
  from config import DATASET_URL, DISCUSSIONS_URL
8
 
9
  FAQ_ITEMS = [
10
+ # Participation
11
  (
12
  "Who can participate?",
13
  "Anyone with a Hugging Face account can participate - data scientists, ML engineers, clinicians, "
 
25
  "on the team's behalf - only one Track 2 submission per team is accepted, and additional submissions from "
26
  "other team members will be ignored.",
27
  ),
28
+ (
29
+ "How many submissions can I make?",
30
+ "Track 1 allows up to **6 submissions** per participant; only your highest-scoring submission appears on the "
31
+ "leaderboard.\n\nTrack 2 allows up to **3 submissions** per team. The independent expert panel will only review "
32
+ "your latest entry, so save your best for last.",
33
+ ),
34
+ # Scoring
35
  (
36
  "How is Track 1 scored?",
37
  "Submissions are scored the way large-scale rare disease benchmarks (like Stenton et al., 2024) score solved "
38
  "cases applied to this single, clinically confirmed answer.\n\nTwo metrics are computed automatically: **rank points** "
39
  "(based on how high the true variant(s) land in your ranked list, with partial credit if you recover only one of "
40
+ "two compound-heterozygous variants) and **F-max** (the best precision/recall balance across your submitted "
41
+ "confidence thresholds).\n\n"
42
+ "Your methods write-up is also reviewed by a judging panel for scientific rigor and depth of understanding.",
43
+ ),
44
+ (
45
+ "Why are there perfect scores on the Track 1 leaderboard?",
46
+ "Track 1 is a **foundational track** β€” it's designed to be achievable. The leaderboard validates that your "
47
+ "pipeline can recover the clinically confirmed variant(s) from real patient data.\n\n"
48
+ "However, recovering the correct variant(s) is not the finish line for Track 1. All teams must also submit a "
49
+ "methods write-up explaining their approach and interpretation of the variants. This component cannot be scored "
50
+ "automatically and is where the judging panel evaluates scientific rigor and depth of understanding."
51
  ),
52
  (
53
  "Are secondary or incidental findings scored?",
 
56
  "since there's no fixed \"correct\" answer for secondary findings the way there is for the primary causal variant.\n\n"
57
  "Use the optional `notes` column to briefly explain why you're flagging it (e.g. \"well-established pathogenic "
58
  "variant, unrelated to primary phenotype, recommend clinical follow-up\")",
 
59
  ),
60
  (
61
  "How is Track 2 scored?",
62
  "Reports are reviewed by an independent panel, and will be judged on scientific rigor, potential impact, innovation, "
63
  "and scalability.",
64
  ),
65
+ # Technical & Logistics
66
  (
67
+ "Can I keep my GitHub repository private during the Hackathon?",
68
+ "Yes. Your repository may remain private while the Hackathon is running. However, it must be made public "
69
+ "once the Hackathon ends so the expert panel can review your code and methods.",
 
70
  ),
71
  (
72
  "What compute resources are available?",
73
  "This Space runs on CPU-basic hardware. You are welcome to use your own compute for training, as only "
74
  "the final submission files need to be uploaded here.",
75
  ),
76
+ (
77
+ "Are third-party LLMs allowed with this Hackathon's dataset?",
78
+ "Conditions apply. Please see our "
79
+ "[detailed answer in the Community discussion](https://huggingface.co/spaces/SageBio/rare-disease-real-kid-mva-hackathon-2026/discussions/2#6a8f68927f3975b1f61cc18a) "
80
+ "for full guidance on provider terms, data handling, and deletion requirements.",
81
+ ),
82
+ # Data & Timeline
83
  (
84
  "What is the data license?",
85
  "The underlying dataset is **gated** and subject to the terms of the Hackathon Rules and Data Transfer Agreement "
tabs/leaderboard.py CHANGED
@@ -10,10 +10,14 @@ from utils import leaderboard_df
10
  LEADERBOARD_MD = """
11
  ## Track 1 Leaderboard
12
 
13
- Rankings update automatically as valid Track 1 submissions are scored. Only your team's highest-scoring
14
- submission is displayed.
15
 
16
  **Metrics:** Rank Points & F-max (adapted from _Stenton et al., 2024_)
 
 
 
 
17
  """
18
 
19
 
 
10
  LEADERBOARD_MD = """
11
  ## Track 1 Leaderboard
12
 
13
+ Rankings update automatically as valid prediction files are scored. Only your team's highest-scoring prediction file
14
+ is displayed.
15
 
16
  **Metrics:** Rank Points & F-max (adapted from _Stenton et al., 2024_)
17
+
18
+ > **Note:** Final Track 1 evaluation also includes your **methods write-up**, which is not reflected here. Your
19
+ > write-ups will be reviewed by a judging panel for scientific rigor after the Hackathon closes. See the FAQ
20
+ > for details.
21
  """
22
 
23
 
tabs/submit_track1.py CHANGED
@@ -72,6 +72,10 @@ name your predictions file:
72
  Each model must include a brief report (PDF or Markdown) describing your approach, rationale, and any supporting evidence.
73
  This is required for judging, but is not scored automatically.
74
 
 
 
 
 
75
  Not sure what details to include? Please use the provided methods description template to organize your analysis, then
76
  export your report as a PDF or Markdown file for submission.
77
 
@@ -82,7 +86,16 @@ is `jane-doe`, you might name your report file:
82
 
83
  ### 3. Prepare other supporting materials.
84
 
85
- * Push documented, reproducible code to a public GitHub repository.
 
 
 
 
 
 
 
 
 
86
 
87
  ### 4. After submission:
88
 
@@ -116,31 +129,31 @@ def _score_csv_bytes(csv_bytes: bytes) -> tuple:
116
 
117
 
118
  def _handle_submit(display_name_input: str, github_url: str, report_file, csv_file, request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> tuple:
119
- """Gradio callback - returns (feedback_md, quota_md)."""
120
  hf_username = get_hf_username(request, oauth_profile)
121
  if not hf_username:
122
- return "⚠️ Please sign in with your Hugging Face account to submit.", ""
123
 
124
  display_name = (display_name_input or "").strip() or hf_username_to_display_slug(hf_username)
125
 
126
  github_url = (github_url or "").strip()
127
  if not github_url.startswith("https://github.com/"):
128
- return "⚠️ GitHub URL must start with `https://github.com/`.", _quota_status(request)
129
 
130
  if report_file is None:
131
- return "⚠️ Please upload your report (PDF or Markdown).", _quota_status(request)
132
  report_path = report_file if isinstance(report_file, str) else report_file.name
133
  if Path(report_path).suffix.lower() not in {".pdf", ".md"}:
134
- return "⚠️ Report must be a PDF (.pdf) or Markdown (.md) file.", _quota_status(request)
135
  report_filename = Path(report_path).name
136
 
137
  if csv_file is None:
138
- return "⚠️ Please upload a CSV file.", _quota_status(request)
139
 
140
  csv_path = csv_file if isinstance(csv_file, str) else csv_file.name
141
  csv_filename = Path(csv_path).name
142
  if Path(csv_path).suffix.lower() != ".csv":
143
- return "⚠️ File must be a CSV (.csv).", _quota_status(request)
144
 
145
  count = submissions_by_user(hf_username)
146
  if count >= MAX_TRACK1_SUBMISSIONS:
@@ -148,6 +161,8 @@ def _handle_submit(display_name_input: str, github_url: str, report_file, csv_fi
148
  f"⚠️ You have already used all {MAX_TRACK1_SUBMISSIONS} submissions. "
149
  "Only your best-scoring submission counts toward final ranking.",
150
  _quota_status(request),
 
 
151
  )
152
  model_n = count + 1
153
 
@@ -157,7 +172,7 @@ def _handle_submit(display_name_input: str, github_url: str, report_file, csv_fi
157
  else:
158
  csv_bytes = csv_file.read() if hasattr(csv_file, "read") else Path(csv_file.name).read_bytes()
159
  except Exception as e:
160
- return f"⚠️ Could not read uploaded file: {e}", _quota_status(request)
161
 
162
  try:
163
  result, proband_id = _score_csv_bytes(csv_bytes)
@@ -167,6 +182,8 @@ def _handle_submit(display_name_input: str, github_url: str, report_file, csv_fi
167
  f"❌ **Scoring error:** {e}\n\n"
168
  f"<details><summary>Technical details</summary>\n\n```\n{tb}\n```\n</details>",
169
  _quota_status(request),
 
 
170
  )
171
 
172
  entry = {
@@ -211,7 +228,7 @@ def _handle_submit(display_name_input: str, github_url: str, report_file, csv_fi
211
 
212
  {match_desc}
213
  """
214
- return feedback.strip(), _quota_status(request)
215
 
216
 
217
  def _quota_status(request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> str:
@@ -234,6 +251,7 @@ def render() -> None:
234
  quota_md = gr.HTML()
235
  with gr.Accordion("πŸ“‹ Submission Format & Instructions", open=True, elem_classes=["faq-accordion"]):
236
  gr.Markdown("**Templates:**")
 
237
  with gr.Row():
238
  gr.DownloadButton(
239
  label="πŸ“„ Submission template (CSV)",
@@ -272,7 +290,7 @@ def render() -> None:
272
  submit_btn.click(
273
  fn=_handle_submit,
274
  inputs=[team_name_box, github_box, report_file, csv_file],
275
- outputs=[result_md, quota_md],
276
  )
277
  tab.select(fn=_quota_status, inputs=[], outputs=quota_md)
278
  return quota_md
 
72
  Each model must include a brief report (PDF or Markdown) describing your approach, rationale, and any supporting evidence.
73
  This is required for judging, but is not scored automatically.
74
 
75
+ > **Update 28 Aug 2026:** If you used an LLM or AI assistant, please record the provider, the plan or tier, and the
76
+ > relevant data-handling setting in your methods description. A line is enough, for example: *"Anthropic API, Claude
77
+ > Sonnet, commercial terms, no training on customer content."*
78
+
79
  Not sure what details to include? Please use the provided methods description template to organize your analysis, then
80
  export your report as a PDF or Markdown file for submission.
81
 
 
86
 
87
  ### 3. Prepare other supporting materials.
88
 
89
+ **GitHub Repository**
90
+
91
+ Your repository may remain private while the Hackathon is running. However, it must be made public once the
92
+ Hackathon ends so the judging panel can review your code and methods.
93
+
94
+ What to include:
95
+ - Documented, reproducible code (all scripts and configuration files needed to reproduce your results)
96
+ - Submission files *(optional)*
97
+ - Methods description *(optional but recommended)*
98
+
99
 
100
  ### 4. After submission:
101
 
 
129
 
130
 
131
  def _handle_submit(display_name_input: str, github_url: str, report_file, csv_file, request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> tuple:
132
+ """Gradio callback - returns (feedback_md, quota_md, report_file, csv_file)."""
133
  hf_username = get_hf_username(request, oauth_profile)
134
  if not hf_username:
135
+ return "⚠️ Please sign in with your Hugging Face account to submit.", "", gr.update(), gr.update()
136
 
137
  display_name = (display_name_input or "").strip() or hf_username_to_display_slug(hf_username)
138
 
139
  github_url = (github_url or "").strip()
140
  if not github_url.startswith("https://github.com/"):
141
+ return "⚠️ GitHub URL must start with `https://github.com/`.", _quota_status(request), gr.update(), gr.update()
142
 
143
  if report_file is None:
144
+ return "⚠️ Please upload your report (PDF or Markdown).", _quota_status(request), gr.update(), gr.update()
145
  report_path = report_file if isinstance(report_file, str) else report_file.name
146
  if Path(report_path).suffix.lower() not in {".pdf", ".md"}:
147
+ return "⚠️ Report must be a PDF (.pdf) or Markdown (.md) file.", _quota_status(request), gr.update(), gr.update()
148
  report_filename = Path(report_path).name
149
 
150
  if csv_file is None:
151
+ return "⚠️ Please upload a CSV file.", _quota_status(request), gr.update(), gr.update()
152
 
153
  csv_path = csv_file if isinstance(csv_file, str) else csv_file.name
154
  csv_filename = Path(csv_path).name
155
  if Path(csv_path).suffix.lower() != ".csv":
156
+ return "⚠️ File must be a CSV (.csv).", _quota_status(request), gr.update(), gr.update()
157
 
158
  count = submissions_by_user(hf_username)
159
  if count >= MAX_TRACK1_SUBMISSIONS:
 
161
  f"⚠️ You have already used all {MAX_TRACK1_SUBMISSIONS} submissions. "
162
  "Only your best-scoring submission counts toward final ranking.",
163
  _quota_status(request),
164
+ gr.update(),
165
+ gr.update(),
166
  )
167
  model_n = count + 1
168
 
 
172
  else:
173
  csv_bytes = csv_file.read() if hasattr(csv_file, "read") else Path(csv_file.name).read_bytes()
174
  except Exception as e:
175
+ return f"⚠️ Could not read uploaded file: {e}", _quota_status(request), gr.update(), gr.update()
176
 
177
  try:
178
  result, proband_id = _score_csv_bytes(csv_bytes)
 
182
  f"❌ **Scoring error:** {e}\n\n"
183
  f"<details><summary>Technical details</summary>\n\n```\n{tb}\n```\n</details>",
184
  _quota_status(request),
185
+ gr.update(),
186
+ gr.update(),
187
  )
188
 
189
  entry = {
 
228
 
229
  {match_desc}
230
  """
231
+ return feedback.strip(), _quota_status(request), None, None
232
 
233
 
234
  def _quota_status(request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> str:
 
251
  quota_md = gr.HTML()
252
  with gr.Accordion("πŸ“‹ Submission Format & Instructions", open=True, elem_classes=["faq-accordion"]):
253
  gr.Markdown("**Templates:**")
254
+ gr.Markdown("<small>*Last updated 28 Aug 2026*</small>")
255
  with gr.Row():
256
  gr.DownloadButton(
257
  label="πŸ“„ Submission template (CSV)",
 
290
  submit_btn.click(
291
  fn=_handle_submit,
292
  inputs=[team_name_box, github_box, report_file, csv_file],
293
+ outputs=[result_md, quota_md, report_file, csv_file],
294
  )
295
  tab.select(fn=_quota_status, inputs=[], outputs=quota_md)
296
  return quota_md
tabs/submit_track2.py CHANGED
@@ -2,25 +2,29 @@
2
 
3
  from __future__ import annotations
4
 
5
- import io
6
- import json
7
- import os
8
  from datetime import datetime, timezone
9
  from pathlib import Path
10
 
11
  import gradio as gr
12
 
13
- from config import LEADERBOARD_DATASET, TRACK2_SUBMISSIONS_FOLDER
14
- from utils import _LOCAL_DEV_DIR, _sanitize, get_hf_username, hf_username_to_display_slug
 
 
 
 
 
15
 
16
- INTRO_MD = """
17
  Upload your Track 2 proposal here for review by our independent expert judging panel. Unlike Track 1,
18
  this track uses qualitative evaluation rather than automated scoring.
19
 
20
- Only <u>one submission</u> is accepted for this track.
 
 
21
 
22
- **Team Participation:** Please designate a single team member to submit on the team's behalf. Additional
23
- or duplicate submissions from other team members will not be reviewed.
24
  """
25
 
26
  INSTRUCTIONS_MD = """
@@ -30,6 +34,10 @@ Prepare a written report (PDF or Markdown) proposing repositioned drug candidate
30
  analysis. Include a characterization of the variant's mechanism (loss-of-function / gain-of-function,
31
  pathway disrupted, downstream biological consequence) as the basis for your repurposing rationale.
32
 
 
 
 
 
33
  Not sure what details to include? Please use the provided methods description template to organize your
34
  analysis and supporting materials, then export your report as a PDF or Markdown file for submission.
35
 
@@ -40,9 +48,19 @@ HF username is `jane-doe`, you might name your report file:
40
 
41
  ### 2. Prepare other supporting materials.
42
 
43
- * Push documented, reproducible code to a public GitHub repository.
44
- * Record a 3-minute pitch video walking through your reasoning and proposed candidates and upload it to
45
- YouTube or Vimeo.
 
 
 
 
 
 
 
 
 
 
46
 
47
  ### 3. After submission:
48
 
@@ -50,70 +68,6 @@ The independent panel will review all entries over a ~2-3 month window. Results
50
  later date.
51
  """
52
 
53
- def _save_track2_submission(entry: dict, report_path: str) -> None:
54
- timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
55
- safe_user = _sanitize(entry.get("hf_username", "unknown"))
56
- subfolder = f"{TRACK2_SUBMISSIONS_FOLDER}{safe_user}/"
57
- filename = f"sub_{timestamp}.json"
58
- payload = json.dumps(entry, indent=2).encode()
59
- token = os.environ.get("HF_TOKEN")
60
- if token:
61
- from huggingface_hub import upload_file
62
- upload_file(
63
- path_or_fileobj=io.BytesIO(payload),
64
- path_in_repo=f"{subfolder}{filename}",
65
- repo_id=LEADERBOARD_DATASET,
66
- repo_type="dataset",
67
- token=token,
68
- commit_message=f"track2: {entry.get('display_name', entry.get('hf_username', '?'))}",
69
- )
70
- upload_file(
71
- path_or_fileobj=report_path,
72
- path_in_repo=f"{subfolder}{entry['report_filename']}",
73
- repo_id=LEADERBOARD_DATASET,
74
- repo_type="dataset",
75
- token=token,
76
- commit_message=f"track2 report: {entry.get('display_name', entry.get('hf_username', '?'))}",
77
- )
78
- else:
79
- local_dir = _LOCAL_DEV_DIR / "track2_submissions" / safe_user
80
- local_dir.mkdir(parents=True, exist_ok=True)
81
- (local_dir / filename).write_bytes(payload)
82
- import shutil
83
- shutil.copy(report_path, local_dir / entry['report_filename'])
84
-
85
- def _track2_submissions_by_user(hf_username: str) -> int:
86
- """Count Track 2 submissions for a given HF user."""
87
- safe_user = _sanitize(hf_username)
88
- token = os.environ.get("HF_TOKEN")
89
- if token:
90
- from huggingface_hub import HfApi
91
- api = HfApi()
92
- try:
93
- return sum(
94
- 1 for f in api.list_repo_files(
95
- LEADERBOARD_DATASET, repo_type="dataset", token=token
96
- )
97
- if f.startswith(f"{TRACK2_SUBMISSIONS_FOLDER}{safe_user}/sub_") and f.endswith(".json")
98
- )
99
- except Exception:
100
- return 0
101
- # Local dev fallback
102
- local_dir = _LOCAL_DEV_DIR / "track2_submissions" / safe_user
103
- if not local_dir.exists():
104
- return 0
105
- return sum(1 for _ in local_dir.glob("sub_*.json"))
106
-
107
-
108
- def _submission_status(request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> str:
109
- hf_username = get_hf_username(request, oauth_profile)
110
- if not hf_username:
111
- return ""
112
- count = _track2_submissions_by_user(hf_username)
113
- if count >= 1:
114
- return '<div class="info-card quota-card">βœ… <strong>Track 2 submission received.</strong> Only one submission per team is accepted.</div>'
115
- return '<div class="info-card quota-card"><strong>0 / 1</strong> submissions used &nbsp;Β·&nbsp; <strong>1</strong> remaining</div>'
116
-
117
 
118
  def _handle_submit(
119
  display_name_input: str,
@@ -123,29 +77,37 @@ def _handle_submit(
123
  notes: str,
124
  request: gr.Request,
125
  oauth_profile: gr.OAuthProfile | None = None,
126
- ) -> str:
 
127
  hf_username = get_hf_username(request, oauth_profile)
128
  if not hf_username:
129
- return "⚠️ Please sign in with your Hugging Face account to submit."
130
 
131
  display_name = (display_name_input or "").strip() or hf_username_to_display_slug(hf_username)
132
 
133
- count = _track2_submissions_by_user(hf_username)
134
- if count >= 1:
135
- return "⚠️ You have already submitted a Track 2 entry. Only one submission per participant is accepted."
136
-
137
  github_url = (github_url or "").strip()
138
  if not github_url.startswith("https://github.com/"):
139
- return "⚠️ GitHub URL must start with `https://github.com/`."
 
140
  video_url = (video_url or "").strip()
141
  if not video_url:
142
- return "⚠️ A pitch video URL is required. Please upload your 3-minute video to YouTube or Vimeo and paste the link."
 
143
  if report_file is None:
144
- return "⚠️ Please upload your report (PDF or Markdown)."
145
 
146
  report_path = report_file if isinstance(report_file, str) else report_file.name
147
  if Path(report_path).suffix.lower() not in {".pdf", ".md"}:
148
- return "⚠️ Report must be a PDF (.pdf) or Markdown (.md) file."
 
 
 
 
 
 
 
 
 
149
 
150
  report_name = Path(
151
  report_file if isinstance(report_file, str) else report_file.name
@@ -160,14 +122,14 @@ def _handle_submit(
160
  "notes": (notes or "").strip(),
161
  "submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
162
  }
163
- _save_track2_submission(entry, report_path)
164
 
165
  video_line = f"[{entry['video_url']}]({entry['video_url']})" if entry["video_url"] else "_(not provided)_"
166
 
167
- return f"""
168
  ## Track 2 submission received βœ“
169
 
170
- **{display_name}** (`{hf_username}`)
171
 
172
  Your submission has been logged and will be reviewed by the expert judging panel over the
173
  ~2-3 month judging window. Results will be announced after the judging period closes.
@@ -176,15 +138,31 @@ Your submission has been logged and will be reviewed by the expert judging panel
176
  - GitHub repository: [{github_url}]({github_url})
177
  - Video: {video_line}
178
  - Report file: `{report_name}`
179
- """.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
 
182
  def render() -> None:
183
  with gr.Tab("Submit - Track 2") as tab:
184
  gr.Markdown(INTRO_MD)
185
- status_md = gr.HTML()
186
  with gr.Accordion("πŸ“‹ Submission Format & Instructions", open=True, elem_classes=["faq-accordion"]):
187
  gr.Markdown("**Templates:**")
 
188
  with gr.Row():
189
  gr.DownloadButton(
190
  label="πŸ“„ Methods description template (Excel)",
@@ -225,7 +203,7 @@ def render() -> None:
225
  submit_btn.click(
226
  fn=_handle_submit,
227
  inputs=[team_name_box, github_box, video_box, report_file, notes_box],
228
- outputs=result_md,
229
  )
230
- tab.select(fn=_submission_status, inputs=[], outputs=status_md)
231
- return status_md
 
2
 
3
  from __future__ import annotations
4
 
 
 
 
5
  from datetime import datetime, timezone
6
  from pathlib import Path
7
 
8
  import gradio as gr
9
 
10
+ from config import MAX_TRACK2_SUBMISSIONS
11
+ from utils import (
12
+ append_submission,
13
+ get_hf_username,
14
+ hf_username_to_display_slug,
15
+ submissions_by_user,
16
+ )
17
 
18
+ INTRO_MD = f"""
19
  Upload your Track 2 proposal here for review by our independent expert judging panel. Unlike Track 1,
20
  this track uses qualitative evaluation rather than automated scoring.
21
 
22
+ You are allowed up to <u>{MAX_TRACK2_SUBMISSIONS} submissions</u> in case you need to make updates to your
23
+ findings/methods. The panel will only review your latest entry, so make sure your final submission is the one you
24
+ want reviewed.
25
 
26
+ **Team Participation:** Please designate a single team member to submit on the team's behalf. Additional or duplicate
27
+ submissions from other team members will not be reviewed.
28
  """
29
 
30
  INSTRUCTIONS_MD = """
 
34
  analysis. Include a characterization of the variant's mechanism (loss-of-function / gain-of-function,
35
  pathway disrupted, downstream biological consequence) as the basis for your repurposing rationale.
36
 
37
+ > **Update 28 Aug 2026:** If you used an LLM or AI assistant, please record the provider, the plan or tier, and the
38
+ > relevant data-handling setting in your methods description. A line is enough, for example: *"Anthropic API, Claude
39
+ > Sonnet, commercial terms, no training on customer content."*
40
+
41
  Not sure what details to include? Please use the provided methods description template to organize your
42
  analysis and supporting materials, then export your report as a PDF or Markdown file for submission.
43
 
 
48
 
49
  ### 2. Prepare other supporting materials.
50
 
51
+ **GitHub Repository**
52
+
53
+ Your repository may remain private while the Hackathon is running. However, it must be made public once the
54
+ Hackathon ends so the panel can review your code and methods.
55
+
56
+ What to include:
57
+ - Documented, reproducible code (all scripts and configuration files needed to reproduce your results)
58
+ - Methods description *(optional but recommended)*
59
+
60
+ **Pitch Video**
61
+
62
+ Record a 3-minute pitch video walking through your reasoning and proposed candidates, then upload it to
63
+ YouTube or Vimeo.
64
 
65
  ### 3. After submission:
66
 
 
68
  later date.
69
  """
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  def _handle_submit(
73
  display_name_input: str,
 
77
  notes: str,
78
  request: gr.Request,
79
  oauth_profile: gr.OAuthProfile | None = None,
80
+ ) -> tuple:
81
+ """Gradio callback - returns (feedback_md, quota_md, report_file)."""
82
  hf_username = get_hf_username(request, oauth_profile)
83
  if not hf_username:
84
+ return "⚠️ Please sign in with your Hugging Face account to submit.", "", gr.update()
85
 
86
  display_name = (display_name_input or "").strip() or hf_username_to_display_slug(hf_username)
87
 
 
 
 
 
88
  github_url = (github_url or "").strip()
89
  if not github_url.startswith("https://github.com/"):
90
+ return "⚠️ GitHub URL must start with `https://github.com/`.", _quota_status(request), gr.update()
91
+
92
  video_url = (video_url or "").strip()
93
  if not video_url:
94
+ return "⚠️ A pitch video URL is required. Please upload your 3-minute video to YouTube or Vimeo and paste the link.", _quota_status(request), gr.update()
95
+
96
  if report_file is None:
97
+ return "⚠️ Please upload your report (PDF or Markdown).", _quota_status(request), gr.update()
98
 
99
  report_path = report_file if isinstance(report_file, str) else report_file.name
100
  if Path(report_path).suffix.lower() not in {".pdf", ".md"}:
101
+ return "⚠️ Report must be a PDF (.pdf) or Markdown (.md) file.", _quota_status(request), gr.update()
102
+
103
+ count = submissions_by_user(hf_username, track=2)
104
+ if count >= MAX_TRACK2_SUBMISSIONS:
105
+ return (
106
+ f"⚠️ You have already used all {MAX_TRACK2_SUBMISSIONS} submissions.",
107
+ _quota_status(request),
108
+ gr.update(),
109
+ )
110
+ submission_n = count + 1
111
 
112
  report_name = Path(
113
  report_file if isinstance(report_file, str) else report_file.name
 
122
  "notes": (notes or "").strip(),
123
  "submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
124
  }
125
+ append_submission(entry, report_path, track=2)
126
 
127
  video_line = f"[{entry['video_url']}]({entry['video_url']})" if entry["video_url"] else "_(not provided)_"
128
 
129
+ feedback = f"""
130
  ## Track 2 submission received βœ“
131
 
132
+ **{display_name}** (`{hf_username}`) &nbsp;|&nbsp; **Submission:** {submission_n}
133
 
134
  Your submission has been logged and will be reviewed by the expert judging panel over the
135
  ~2-3 month judging window. Results will be announced after the judging period closes.
 
138
  - GitHub repository: [{github_url}]({github_url})
139
  - Video: {video_line}
140
  - Report file: `{report_name}`
141
+ """
142
+ return feedback.strip(), _quota_status(request), None
143
+
144
+
145
+ def _quota_status(request: gr.Request, oauth_profile: gr.OAuthProfile | None = None) -> str:
146
+ hf_username = get_hf_username(request, oauth_profile)
147
+ if not hf_username:
148
+ return ""
149
+ count = submissions_by_user(hf_username, track=2)
150
+ remaining = MAX_TRACK2_SUBMISSIONS - count
151
+ return (
152
+ f'<div class="info-card quota-card">'
153
+ f'<strong>{count} / {MAX_TRACK2_SUBMISSIONS}</strong> submissions used &nbsp;Β·&nbsp; '
154
+ f'<strong>{remaining}</strong> remaining'
155
+ f'</div>'
156
+ )
157
 
158
 
159
  def render() -> None:
160
  with gr.Tab("Submit - Track 2") as tab:
161
  gr.Markdown(INTRO_MD)
162
+ quota_md = gr.HTML()
163
  with gr.Accordion("πŸ“‹ Submission Format & Instructions", open=True, elem_classes=["faq-accordion"]):
164
  gr.Markdown("**Templates:**")
165
+ gr.Markdown("<small>*Last updated 28 Aug 2026*</small>")
166
  with gr.Row():
167
  gr.DownloadButton(
168
  label="πŸ“„ Methods description template (Excel)",
 
203
  submit_btn.click(
204
  fn=_handle_submit,
205
  inputs=[team_name_box, github_box, video_box, report_file, notes_box],
206
+ outputs=[result_md, quota_md, report_file],
207
  )
208
+ tab.select(fn=_quota_status, inputs=[], outputs=quota_md)
209
+ return quota_md
utils.py CHANGED
@@ -17,6 +17,7 @@ from pathlib import Path
17
  from config import (
18
  LEADERBOARD_DATASET,
19
  TRACK1_SUBMISSIONS_FOLDER,
 
20
  )
21
 
22
  _LOCAL_DEV_DIR = Path("local_dev")
@@ -58,18 +59,34 @@ def hf_username_to_display_slug(username: str) -> str:
58
 
59
  # ── Submissions ──────────────────────────────────────────────────────────────
60
 
61
- def _local_submissions_dir() -> Path:
62
- return _LOCAL_DEV_DIR / "track1_submissions"
 
 
 
63
 
64
 
65
- def append_submission(entry: dict, report_path: str | None = None) -> None:
 
 
 
 
 
66
  """
67
  Store a single submission as its own JSON file in the HF dataset (or locally in dev).
68
  Optionally also saves a report file alongside the JSON.
 
 
 
 
 
69
  """
 
 
 
70
  timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
71
  safe_user = _sanitize(entry.get("hf_username", "unknown"))
72
- subfolder = f"{TRACK1_SUBMISSIONS_FOLDER}{safe_user}/"
73
  filename = f"sub_{timestamp}.json"
74
 
75
  # Stamp the report filename with the same timestamp as the metadata.
@@ -89,7 +106,7 @@ def append_submission(entry: dict, report_path: str | None = None) -> None:
89
  repo_id=LEADERBOARD_DATASET,
90
  repo_type="dataset",
91
  token=token,
92
- commit_message=f"track1: {entry.get('display_name', entry.get('hf_username', '?'))}",
93
  )
94
  if report_path and entry.get("report_filename"):
95
  upload_file(
@@ -98,11 +115,11 @@ def append_submission(entry: dict, report_path: str | None = None) -> None:
98
  repo_id=LEADERBOARD_DATASET,
99
  repo_type="dataset",
100
  token=token,
101
- commit_message=f"track1 report: {entry.get('display_name', entry.get('hf_username', '?'))}",
102
  )
103
  else:
104
  import shutil
105
- local_dir = _local_submissions_dir() / safe_user
106
  local_dir.mkdir(parents=True, exist_ok=True)
107
  (local_dir / filename).write_bytes(payload)
108
  if report_path and entry.get("report_filename"):
@@ -150,7 +167,7 @@ def load_leaderboard(force_refresh: bool = False) -> list[dict]:
150
 
151
  # Local dev fallback
152
  rows = []
153
- for f in sorted(_local_submissions_dir().glob("**/sub_*.json")):
154
  try:
155
  rows.append(json.loads(f.read_text()))
156
  except Exception:
@@ -185,9 +202,10 @@ def best_per_team(rows: list[dict]) -> list[dict]:
185
  return [{**r, "place": i} for i, r in enumerate(sorted_rows, 1)]
186
 
187
 
188
- def _list_user_files(folder: str, hf_username: str) -> list[str]:
189
- """Return sorted list of JSON submission paths for a user in the given folder."""
190
  safe_user = _sanitize(hf_username)
 
191
  prefix = f"{folder}{safe_user}/sub_"
192
  token = os.environ.get("HF_TOKEN")
193
  if token:
@@ -199,15 +217,15 @@ def _list_user_files(folder: str, hf_username: str) -> list[str]:
199
  )
200
  except Exception:
201
  return []
202
- user_dir = _local_submissions_dir() / safe_user
203
  if not user_dir.exists():
204
  return []
205
  return sorted(str(f) for f in user_dir.glob("sub_*.json"))
206
 
207
 
208
- def submissions_by_user(hf_username: str) -> int:
209
- """Return the number of Track 1 submissions made by a given HF user."""
210
- return len(_list_user_files(TRACK1_SUBMISSIONS_FOLDER, hf_username))
211
 
212
 
213
  def leaderboard_df() -> list[list]:
 
17
  from config import (
18
  LEADERBOARD_DATASET,
19
  TRACK1_SUBMISSIONS_FOLDER,
20
+ TRACK2_SUBMISSIONS_FOLDER,
21
  )
22
 
23
  _LOCAL_DEV_DIR = Path("local_dev")
 
59
 
60
  # ── Submissions ──────────────────────────────────────────────────────────────
61
 
62
+ # Track folder mapping
63
+ _TRACK_FOLDERS = {
64
+ 1: TRACK1_SUBMISSIONS_FOLDER,
65
+ 2: TRACK2_SUBMISSIONS_FOLDER,
66
+ }
67
 
68
 
69
+ def _local_submissions_dir(track: int = 1) -> Path:
70
+ folder_name = f"track{track}_submissions"
71
+ return _LOCAL_DEV_DIR / folder_name
72
+
73
+
74
+ def append_submission(entry: dict, report_path: str | None = None, *, track: int = 1) -> None:
75
  """
76
  Store a single submission as its own JSON file in the HF dataset (or locally in dev).
77
  Optionally also saves a report file alongside the JSON.
78
+
79
+ Args:
80
+ entry: Submission metadata dict
81
+ report_path: Optional path to a report file to upload
82
+ track: Track number (1 or 2)
83
  """
84
+ submissions_folder = _TRACK_FOLDERS[track]
85
+ track_label = f"track{track}"
86
+
87
  timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
88
  safe_user = _sanitize(entry.get("hf_username", "unknown"))
89
+ subfolder = f"{submissions_folder}{safe_user}/"
90
  filename = f"sub_{timestamp}.json"
91
 
92
  # Stamp the report filename with the same timestamp as the metadata.
 
106
  repo_id=LEADERBOARD_DATASET,
107
  repo_type="dataset",
108
  token=token,
109
+ commit_message=f"{track_label}: {entry.get('display_name', entry.get('hf_username', '?'))}",
110
  )
111
  if report_path and entry.get("report_filename"):
112
  upload_file(
 
115
  repo_id=LEADERBOARD_DATASET,
116
  repo_type="dataset",
117
  token=token,
118
+ commit_message=f"{track_label} report: {entry.get('display_name', entry.get('hf_username', '?'))}",
119
  )
120
  else:
121
  import shutil
122
+ local_dir = _local_submissions_dir(track) / safe_user
123
  local_dir.mkdir(parents=True, exist_ok=True)
124
  (local_dir / filename).write_bytes(payload)
125
  if report_path and entry.get("report_filename"):
 
167
 
168
  # Local dev fallback
169
  rows = []
170
+ for f in sorted(_local_submissions_dir(track=1).glob("**/sub_*.json")):
171
  try:
172
  rows.append(json.loads(f.read_text()))
173
  except Exception:
 
202
  return [{**r, "place": i} for i, r in enumerate(sorted_rows, 1)]
203
 
204
 
205
+ def _list_user_files(hf_username: str, *, track: int = 1) -> list[str]:
206
+ """Return sorted list of JSON submission paths for a user in the given track."""
207
  safe_user = _sanitize(hf_username)
208
+ folder = _TRACK_FOLDERS[track]
209
  prefix = f"{folder}{safe_user}/sub_"
210
  token = os.environ.get("HF_TOKEN")
211
  if token:
 
217
  )
218
  except Exception:
219
  return []
220
+ user_dir = _local_submissions_dir(track) / safe_user
221
  if not user_dir.exists():
222
  return []
223
  return sorted(str(f) for f in user_dir.glob("sub_*.json"))
224
 
225
 
226
+ def submissions_by_user(hf_username: str, *, track: int = 1) -> int:
227
+ """Return the number of submissions made by a given HF user for the specified track."""
228
+ return len(_list_user_files(hf_username, track=track))
229
 
230
 
231
  def leaderboard_df() -> list[list]: