whojavumusic commited on
Commit
a5fbf6b
·
1 Parent(s): b60e9d6

added email section

Browse files
Files changed (4) hide show
  1. app.py +15 -2
  2. constants.py +1 -0
  3. init.py +10 -1
  4. job_queue.py +64 -13
app.py CHANGED
@@ -213,6 +213,7 @@ def _model_hub_page_link(model_id: str) -> str:
213
  def submit_model(
214
  model_id: str,
215
  submission_notes: str,
 
216
  extra_requirements: str,
217
  setup_script: str,
218
  custom_script: str,
@@ -255,6 +256,11 @@ def submit_model(
255
  if not on_hub:
256
  return styled_error(f"Model '{model_id}' {err_msg}")
257
 
 
 
 
 
 
258
  script_hint = ""
259
  if (custom_script or "").strip():
260
  dep_warn = job_queue.custom_script_deprecated_api_warning(custom_script)
@@ -275,6 +281,7 @@ def submit_model(
275
  model_id,
276
  family_id,
277
  submission_notes=submission_notes or "",
 
278
  extra_requirements=extra_requirements or "",
279
  setup_script=setup_script or "",
280
  custom_script=custom_script or "",
@@ -613,6 +620,11 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
613
  lines=2,
614
  max_length=4000,
615
  )
 
 
 
 
 
616
 
617
  status_output = gr.HTML()
618
  gr.Markdown(
@@ -635,8 +647,8 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
635
  )
636
  return extra, setup_out, custom
637
 
638
- def _submit_and_refresh(model_id, notes, reqs, setup, script, recipe, gated):
639
- status = submit_model(model_id, notes, reqs, setup, script, recipe, gated)
640
  try:
641
  nxt = job_queue.next_up_html()
642
  except Exception as exc:
@@ -660,6 +672,7 @@ with gr.Blocks(title=APP_TITLE, theme=_theme, css=LEADERBOARD_CSS) as demo:
660
  inputs=[
661
  model_input,
662
  notes_input,
 
663
  requirements_input,
664
  setup_script_input,
665
  script_input,
 
213
  def submit_model(
214
  model_id: str,
215
  submission_notes: str,
216
+ contact_email: str,
217
  extra_requirements: str,
218
  setup_script: str,
219
  custom_script: str,
 
256
  if not on_hub:
257
  return styled_error(f"Model '{model_id}' {err_msg}")
258
 
259
+ try:
260
+ job_queue.sanitize_contact_email(contact_email)
261
+ except ValueError:
262
+ return styled_error("Please enter a valid email address.")
263
+
264
  script_hint = ""
265
  if (custom_script or "").strip():
266
  dep_warn = job_queue.custom_script_deprecated_api_warning(custom_script)
 
281
  model_id,
282
  family_id,
283
  submission_notes=submission_notes or "",
284
+ contact_email=contact_email or "",
285
  extra_requirements=extra_requirements or "",
286
  setup_script=setup_script or "",
287
  custom_script=custom_script or "",
 
620
  lines=2,
621
  max_length=4000,
622
  )
623
+ email_input = gr.Textbox(
624
+ label="Contact email (optional)",
625
+ placeholder="you@example.com",
626
+ max_length=254,
627
+ )
628
 
629
  status_output = gr.HTML()
630
  gr.Markdown(
 
647
  )
648
  return extra, setup_out, custom
649
 
650
+ def _submit_and_refresh(model_id, notes, email, reqs, setup, script, recipe, gated):
651
+ status = submit_model(model_id, notes, email, reqs, setup, script, recipe, gated)
652
  try:
653
  nxt = job_queue.next_up_html()
654
  except Exception as exc:
 
672
  inputs=[
673
  model_input,
674
  notes_input,
675
+ email_input,
676
  requirements_input,
677
  setup_script_input,
678
  script_input,
constants.py CHANGED
@@ -248,6 +248,7 @@ client = Client("__SPACE_ID__")
248
  result = client.predict(
249
  "openai/whisper-tiny", # model id
250
  "", # optional notes
 
251
  "", # extra requirements
252
  "", # setup script
253
  "", # custom evaluator
 
248
  result = client.predict(
249
  "openai/whisper-tiny", # model id
250
  "", # optional notes
251
+ "", # contact email
252
  "", # extra requirements
253
  "", # setup script
254
  "", # custom evaluator
init.py CHANGED
@@ -63,6 +63,7 @@ CSV_FIELDS = [
63
  "eval_rtf",
64
  "num_params",
65
  "submission_notes",
 
66
  "submitted_at",
67
  ]
68
 
@@ -111,6 +112,7 @@ def leaderboard_row_from_eval_result(
111
  result: dict,
112
  submitted_at_iso: str,
113
  submission_notes: str = "",
 
114
  ) -> dict:
115
  """Build a full CSV row; scenario keys missing from `result` are left empty (future metrics)."""
116
  row = {k: "" for k in CSV_FIELDS}
@@ -156,6 +158,7 @@ def leaderboard_row_from_eval_result(
156
  if k in result and result[k] is not None and result[k] != "":
157
  row[k] = str(result[k])
158
  row["submission_notes"] = (submission_notes or "").strip()[:4000]
 
159
  row["submitted_at"] = submitted_at_iso
160
  return row
161
 
@@ -164,6 +167,7 @@ def merge_leaderboard_row_from_eval_result(
164
  result: dict,
165
  submitted_at_iso: str,
166
  submission_notes: str = "",
 
167
  ) -> None:
168
  """
169
  Merge evaluation output into an existing leaderboard row (partial re-eval).
@@ -186,7 +190,7 @@ def merge_leaderboard_row_from_eval_result(
186
  break
187
 
188
  patch = leaderboard_row_from_eval_result(
189
- result, submitted_at_iso, submission_notes=submission_notes
190
  )
191
 
192
  if match_idx is None:
@@ -204,6 +208,8 @@ def merge_leaderboard_row_from_eval_result(
204
  row["num_params"] = patch["num_params"]
205
  if (submission_notes or "").strip():
206
  row["submission_notes"] = patch["submission_notes"]
 
 
207
  normalize_legacy_csv_row(row)
208
  rows[match_idx] = row
209
 
@@ -286,6 +292,7 @@ def load_raw_results(*, refresh: bool = False) -> list[dict]:
286
  if not row.get("eval_family"):
287
  row["eval_family"] = "N/A"
288
  row.setdefault("submission_notes", "")
 
289
  normalize_legacy_csv_row(row)
290
  _raw_results_cache = results
291
  return [dict(r) for r in results]
@@ -482,6 +489,7 @@ def raw_rows_for_version(version: str) -> list[dict]:
482
  if not row.get("eval_family"):
483
  row["eval_family"] = "N/A"
484
  row.setdefault("submission_notes", "")
 
485
  normalize_legacy_csv_row(row)
486
  out.append(row)
487
  return out
@@ -636,6 +644,7 @@ def raw_rows_for_version(version: str) -> list[dict]:
636
  if not row.get("eval_family"):
637
  row["eval_family"] = "N/A"
638
  row.setdefault("submission_notes", "")
 
639
  normalize_legacy_csv_row(row)
640
  out.append(row)
641
  return out
 
63
  "eval_rtf",
64
  "num_params",
65
  "submission_notes",
66
+ "contact_email",
67
  "submitted_at",
68
  ]
69
 
 
112
  result: dict,
113
  submitted_at_iso: str,
114
  submission_notes: str = "",
115
+ contact_email: str = "",
116
  ) -> dict:
117
  """Build a full CSV row; scenario keys missing from `result` are left empty (future metrics)."""
118
  row = {k: "" for k in CSV_FIELDS}
 
158
  if k in result and result[k] is not None and result[k] != "":
159
  row[k] = str(result[k])
160
  row["submission_notes"] = (submission_notes or "").strip()[:4000]
161
+ row["contact_email"] = (contact_email or "").strip()[:254]
162
  row["submitted_at"] = submitted_at_iso
163
  return row
164
 
 
167
  result: dict,
168
  submitted_at_iso: str,
169
  submission_notes: str = "",
170
+ contact_email: str = "",
171
  ) -> None:
172
  """
173
  Merge evaluation output into an existing leaderboard row (partial re-eval).
 
190
  break
191
 
192
  patch = leaderboard_row_from_eval_result(
193
+ result, submitted_at_iso, submission_notes=submission_notes, contact_email=contact_email
194
  )
195
 
196
  if match_idx is None:
 
208
  row["num_params"] = patch["num_params"]
209
  if (submission_notes or "").strip():
210
  row["submission_notes"] = patch["submission_notes"]
211
+ if (contact_email or "").strip():
212
+ row["contact_email"] = patch["contact_email"]
213
  normalize_legacy_csv_row(row)
214
  rows[match_idx] = row
215
 
 
292
  if not row.get("eval_family"):
293
  row["eval_family"] = "N/A"
294
  row.setdefault("submission_notes", "")
295
+ row.setdefault("contact_email", "")
296
  normalize_legacy_csv_row(row)
297
  _raw_results_cache = results
298
  return [dict(r) for r in results]
 
489
  if not row.get("eval_family"):
490
  row["eval_family"] = "N/A"
491
  row.setdefault("submission_notes", "")
492
+ row.setdefault("contact_email", "")
493
  normalize_legacy_csv_row(row)
494
  out.append(row)
495
  return out
 
644
  if not row.get("eval_family"):
645
  row["eval_family"] = "N/A"
646
  row.setdefault("submission_notes", "")
647
+ row.setdefault("contact_email", "")
648
  normalize_legacy_csv_row(row)
649
  out.append(row)
650
  return out
job_queue.py CHANGED
@@ -62,6 +62,7 @@ class Job:
62
  error: str | None = None
63
  result: dict[str, Any] | None = None
64
  submission_notes: str = ""
 
65
  extra_requirements: str = ""
66
  setup_script: str = ""
67
  custom_script: str = ""
@@ -108,6 +109,7 @@ _JOBS_CSV_FIELDS = [
108
  "updated_at",
109
  "error",
110
  "submission_notes",
 
111
  "extra_requirements",
112
  "setup_script_b64",
113
  "custom_script_b64",
@@ -149,6 +151,19 @@ def sanitize_custom_script(text: str) -> str:
149
  return s
150
 
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  def sanitize_setup_script(text: str) -> str:
153
  """Trim and cap one-time setup script (shell or Python) for storage."""
154
  s = (text or "").strip()
@@ -327,6 +342,7 @@ def _persist_jobs() -> None:
327
  "updated_at": j.updated_at or j.created_at,
328
  "error": (j.error or "").replace("\n", " ")[:2000],
329
  "submission_notes": (j.submission_notes or "").replace("\n", " ")[:4000],
 
330
  "extra_requirements": (j.extra_requirements or "").replace("\r\n", "\n")[:8000],
331
  "setup_script_b64": _encode_script_b64(j.setup_script or ""),
332
  "custom_script_b64": _encode_script_b64(j.custom_script or ""),
@@ -399,6 +415,7 @@ def _load_persisted_jobs_once() -> None:
399
  continue
400
  err = (row.get("error") or "").strip() or None
401
  notes = (row.get("submission_notes") or "").strip()
 
402
  extra_req = (row.get("extra_requirements") or "").strip()
403
  setup = _decode_script_b64(row.get("setup_script_b64") or "")
404
  script = _decode_script_b64(row.get("custom_script_b64") or "")
@@ -434,6 +451,7 @@ def _load_persisted_jobs_once() -> None:
434
  updated_at=updated,
435
  error=err,
436
  submission_notes=notes,
 
437
  extra_requirements=extra_req,
438
  setup_script=setup,
439
  custom_script=script,
@@ -580,6 +598,7 @@ def _merge_eval_result_to_leaderboard(
580
  result: dict,
581
  submitted_at_iso: str,
582
  submission_notes: str,
 
583
  *,
584
  replace_existing: bool = False,
585
  merge_partial: bool = False,
@@ -595,7 +614,10 @@ def _merge_eval_result_to_leaderboard(
595
 
596
  if merge_partial:
597
  merge_leaderboard_row_from_eval_result(
598
- result, submitted_at_iso, submission_notes=submission_notes
 
 
 
599
  )
600
  return
601
 
@@ -607,7 +629,10 @@ def _merge_eval_result_to_leaderboard(
607
  rows.pop(i)
608
 
609
  new_row = leaderboard_row_from_eval_result(
610
- result, submitted_at_iso, submission_notes=submission_notes
 
 
 
611
  )
612
  normalize_legacy_csv_row(new_row)
613
  rows.append(new_row)
@@ -682,20 +707,36 @@ def _load_artifact_json_from_bucket(artifact_path: str) -> dict:
682
  pass
683
 
684
 
685
- def _submission_notes_for_artifact_import(
686
- data: dict, artifact_path: str, override: str
687
- ) -> str:
688
- notes = (override or "").strip()
689
- if notes:
690
- return notes[:4000]
 
691
  space_job_id = str(data.get("job_id") or "").strip()
692
  with _jobs_lock:
693
  for j in _jobs.values():
694
  if space_job_id and j.id == space_job_id:
695
- return (j.submission_notes or "").strip()[:4000]
 
 
 
 
696
  if (j.remote_artifact_path or "").strip() == artifact_path:
697
- return (j.submission_notes or "").strip()[:4000]
698
- return ""
 
 
 
 
 
 
 
 
 
 
 
699
 
700
 
701
  def _mark_job_done_for_artifact(
@@ -770,7 +811,9 @@ def import_artifact_to_leaderboard(
770
  if not model_id:
771
  return False, "Artifact result is missing model_id."
772
 
773
- notes = _submission_notes_for_artifact_import(data, artifact_path, submission_notes)
 
 
774
  rows = load_raw_results()
775
  existing = [i for i, r in enumerate(rows) if (r.get("model_id") or "").strip() == model_id]
776
 
@@ -790,7 +833,9 @@ def import_artifact_to_leaderboard(
790
  rows.pop(i)
791
 
792
  submitted_at = _now_iso()
793
- new_row = leaderboard_row_from_eval_result(result, submitted_at, submission_notes=notes)
 
 
794
  normalize_legacy_csv_row(new_row)
795
  rows.append(new_row)
796
  _leaderboard_sort_rows_inplace(rows)
@@ -950,6 +995,7 @@ def _remote_start_queued_job(job_id: str, jobs_token: str) -> None:
950
  mid = j.model_id
951
  fid = j.family_id
952
  notes = j.submission_notes or ""
 
953
  j.status = JobStatus.running
954
  j.progress_done = 0
955
  j.progress_total = 0
@@ -994,6 +1040,7 @@ def _remote_tick_job(job_id: str, jobs_token: str) -> None:
994
  mid = j.model_id
995
  fid = j.family_id
996
  notes = j.submission_notes or ""
 
997
 
998
  # Grace window absorbs the brief gap between the worker script returning
999
  # rc=0 (and uploading the JSON artifact) and the Hub flipping the job's
@@ -1035,6 +1082,7 @@ def _remote_tick_job(job_id: str, jobs_token: str) -> None:
1035
  result,
1036
  _now_iso(),
1037
  notes,
 
1038
  replace_existing=replace_lb and not partial,
1039
  merge_partial=partial,
1040
  )
@@ -1157,6 +1205,7 @@ def enqueue(
1157
  family_id: str,
1158
  submission_notes: str = "",
1159
  *,
 
1160
  extra_requirements: str = "",
1161
  setup_script: str = "",
1162
  custom_script: str = "",
@@ -1199,6 +1248,7 @@ def enqueue(
1199
  created = _now_iso()
1200
  notes_clean = (submission_notes or "").strip()[:4000]
1201
  try:
 
1202
  from recipes.registry import apply_recipe_to_submission
1203
 
1204
  req_lines = parse_requirements_lines(extra_requirements)
@@ -1231,6 +1281,7 @@ def enqueue(
1231
  created_at=created,
1232
  updated_at=created,
1233
  submission_notes=notes_clean,
 
1234
  extra_requirements=extra_req_clean,
1235
  setup_script=setup_clean,
1236
  custom_script=script_clean,
 
62
  error: str | None = None
63
  result: dict[str, Any] | None = None
64
  submission_notes: str = ""
65
+ contact_email: str = ""
66
  extra_requirements: str = ""
67
  setup_script: str = ""
68
  custom_script: str = ""
 
109
  "updated_at",
110
  "error",
111
  "submission_notes",
112
+ "contact_email",
113
  "extra_requirements",
114
  "setup_script_b64",
115
  "custom_script_b64",
 
151
  return s
152
 
153
 
154
+ def sanitize_contact_email(text: str) -> str:
155
+ """Trim and validate an optional submitter email address."""
156
+ s = (text or "").strip()[:254]
157
+ if not s:
158
+ return ""
159
+ if s.count("@") != 1:
160
+ raise ValueError("Invalid email address.")
161
+ local, domain = s.split("@", 1)
162
+ if not local or not domain or "." not in domain:
163
+ raise ValueError("Invalid email address.")
164
+ return s
165
+
166
+
167
  def sanitize_setup_script(text: str) -> str:
168
  """Trim and cap one-time setup script (shell or Python) for storage."""
169
  s = (text or "").strip()
 
342
  "updated_at": j.updated_at or j.created_at,
343
  "error": (j.error or "").replace("\n", " ")[:2000],
344
  "submission_notes": (j.submission_notes or "").replace("\n", " ")[:4000],
345
+ "contact_email": (j.contact_email or "").replace("\n", " ")[:254],
346
  "extra_requirements": (j.extra_requirements or "").replace("\r\n", "\n")[:8000],
347
  "setup_script_b64": _encode_script_b64(j.setup_script or ""),
348
  "custom_script_b64": _encode_script_b64(j.custom_script or ""),
 
415
  continue
416
  err = (row.get("error") or "").strip() or None
417
  notes = (row.get("submission_notes") or "").strip()
418
+ email = (row.get("contact_email") or "").strip()[:254]
419
  extra_req = (row.get("extra_requirements") or "").strip()
420
  setup = _decode_script_b64(row.get("setup_script_b64") or "")
421
  script = _decode_script_b64(row.get("custom_script_b64") or "")
 
451
  updated_at=updated,
452
  error=err,
453
  submission_notes=notes,
454
+ contact_email=email,
455
  extra_requirements=extra_req,
456
  setup_script=setup,
457
  custom_script=script,
 
598
  result: dict,
599
  submitted_at_iso: str,
600
  submission_notes: str,
601
+ contact_email: str = "",
602
  *,
603
  replace_existing: bool = False,
604
  merge_partial: bool = False,
 
614
 
615
  if merge_partial:
616
  merge_leaderboard_row_from_eval_result(
617
+ result,
618
+ submitted_at_iso,
619
+ submission_notes=submission_notes,
620
+ contact_email=contact_email,
621
  )
622
  return
623
 
 
629
  rows.pop(i)
630
 
631
  new_row = leaderboard_row_from_eval_result(
632
+ result,
633
+ submitted_at_iso,
634
+ submission_notes=submission_notes,
635
+ contact_email=contact_email,
636
  )
637
  normalize_legacy_csv_row(new_row)
638
  rows.append(new_row)
 
707
  pass
708
 
709
 
710
+ def _submission_fields_for_artifact_import(
711
+ data: dict, artifact_path: str, notes_override: str, email_override: str
712
+ ) -> tuple[str, str]:
713
+ notes = (notes_override or "").strip()[:4000]
714
+ email = (email_override or "").strip()[:254]
715
+ if notes and email:
716
+ return notes, email
717
  space_job_id = str(data.get("job_id") or "").strip()
718
  with _jobs_lock:
719
  for j in _jobs.values():
720
  if space_job_id and j.id == space_job_id:
721
+ if not notes:
722
+ notes = (j.submission_notes or "").strip()[:4000]
723
+ if not email:
724
+ email = (j.contact_email or "").strip()[:254]
725
+ return notes, email
726
  if (j.remote_artifact_path or "").strip() == artifact_path:
727
+ if not notes:
728
+ notes = (j.submission_notes or "").strip()[:4000]
729
+ if not email:
730
+ email = (j.contact_email or "").strip()[:254]
731
+ return notes, email
732
+ return notes, email
733
+
734
+
735
+ def _submission_notes_for_artifact_import(
736
+ data: dict, artifact_path: str, override: str
737
+ ) -> str:
738
+ notes, _ = _submission_fields_for_artifact_import(data, artifact_path, override, "")
739
+ return notes
740
 
741
 
742
  def _mark_job_done_for_artifact(
 
811
  if not model_id:
812
  return False, "Artifact result is missing model_id."
813
 
814
+ notes, email = _submission_fields_for_artifact_import(
815
+ data, artifact_path, submission_notes, ""
816
+ )
817
  rows = load_raw_results()
818
  existing = [i for i, r in enumerate(rows) if (r.get("model_id") or "").strip() == model_id]
819
 
 
833
  rows.pop(i)
834
 
835
  submitted_at = _now_iso()
836
+ new_row = leaderboard_row_from_eval_result(
837
+ result, submitted_at, submission_notes=notes, contact_email=email
838
+ )
839
  normalize_legacy_csv_row(new_row)
840
  rows.append(new_row)
841
  _leaderboard_sort_rows_inplace(rows)
 
995
  mid = j.model_id
996
  fid = j.family_id
997
  notes = j.submission_notes or ""
998
+ email = j.contact_email or ""
999
  j.status = JobStatus.running
1000
  j.progress_done = 0
1001
  j.progress_total = 0
 
1040
  mid = j.model_id
1041
  fid = j.family_id
1042
  notes = j.submission_notes or ""
1043
+ email = j.contact_email or ""
1044
 
1045
  # Grace window absorbs the brief gap between the worker script returning
1046
  # rc=0 (and uploading the JSON artifact) and the Hub flipping the job's
 
1082
  result,
1083
  _now_iso(),
1084
  notes,
1085
+ email,
1086
  replace_existing=replace_lb and not partial,
1087
  merge_partial=partial,
1088
  )
 
1205
  family_id: str,
1206
  submission_notes: str = "",
1207
  *,
1208
+ contact_email: str = "",
1209
  extra_requirements: str = "",
1210
  setup_script: str = "",
1211
  custom_script: str = "",
 
1248
  created = _now_iso()
1249
  notes_clean = (submission_notes or "").strip()[:4000]
1250
  try:
1251
+ email_clean = sanitize_contact_email(contact_email)
1252
  from recipes.registry import apply_recipe_to_submission
1253
 
1254
  req_lines = parse_requirements_lines(extra_requirements)
 
1281
  created_at=created,
1282
  updated_at=created,
1283
  submission_notes=notes_clean,
1284
+ contact_email=email_clean,
1285
  extra_requirements=extra_req_clean,
1286
  setup_script=setup_clean,
1287
  custom_script=script_clean,