ysharma HF Staff commited on
Commit
0eacc6f
Β·
verified Β·
1 Parent(s): ec3f75d

Latest template; already-generated detection + recreate/replace flow

Browse files
__pycache__/app.cpython-312.pyc ADDED
Binary file (19.8 kB). View file
 
__pycache__/certificate_upload_module.cpython-312.pyc CHANGED
Binary files a/__pycache__/certificate_upload_module.cpython-312.pyc and b/__pycache__/certificate_upload_module.cpython-312.pyc differ
 
app.py CHANGED
@@ -26,7 +26,7 @@ from PIL import Image
26
  from datasets import load_dataset
27
  from gradio_client import Client, handle_file
28
 
29
- from certificate_upload_module import upload_user_certificate
30
 
31
  HF_TOKEN = os.getenv("HF_TOKEN")
32
 
@@ -117,42 +117,37 @@ def lookup_participant(username: str):
117
 
118
  # ----------------------------------------------------------------------------- on login
119
  def on_load(profile: gr.OAuthProfile | None):
120
- """Runs on page load. Drives which panel is shown and pre-fills the form."""
 
 
 
121
  if profile is None:
122
  return (
123
  "Please sign in with your Hugging Face account to continue.",
124
- gr.update(visible=False), # main_interface
125
- gr.update(visible=False), # contact_box
126
- "", # name
127
  gr.update(value="", visible=True), # project
128
- "", # data_status
 
 
 
129
  )
130
 
131
  username = profile.username
132
  profile_name = profile.name or username
133
  state, full_name, space_name = lookup_participant(username)
134
 
135
- if state == "not_found":
136
- return (
137
- f"Signed in as **{username}**.",
138
- gr.update(visible=False),
139
- gr.update(visible=True),
140
- "",
141
- gr.update(value="", visible=True),
142
- "",
143
- )
144
- if state == "error":
145
  return (
146
  f"Signed in as **{username}**.",
147
  gr.update(visible=False),
148
  gr.update(visible=True),
149
- "",
150
- gr.update(value="", visible=True),
151
- "",
152
  )
153
 
154
  name_value = full_name or profile_name
155
-
156
  if state == "found_project":
157
  status = f"βœ… Found your submission β€” **{space_name}**. Review the details below, then generate."
158
  project_update = gr.update(value=space_name, visible=True)
@@ -163,13 +158,30 @@ def on_load(profile: gr.OAuthProfile | None):
163
  )
164
  project_update = gr.update(value="", visible=True)
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  return (
167
  f"Signed in as **{username}**.",
168
  gr.update(visible=True),
169
  gr.update(visible=False),
170
- name_value,
171
- project_update,
172
- status,
173
  )
174
 
175
 
@@ -188,10 +200,10 @@ def generate_linkedin_url(participant_name, project_name):
188
 
189
 
190
  # ----------------------------------------------------------------------------- generate
191
- def create_certificate(participant_name, project_name,
192
  oauth_token: gr.OAuthToken | None, profile: gr.OAuthProfile | None):
193
  if profile is None:
194
- return None, None, "❌ Please sign in first to generate your certificate.", ""
195
 
196
  username = profile.username
197
  state, _, _ = lookup_participant(username)
@@ -199,9 +211,25 @@ def create_certificate(participant_name, project_name,
199
  return None, None, (
200
  "❌ We couldn't find you in the Build Small Hackathon records, so we can't issue a "
201
  f"certificate. Please reach out on [Discord]({DISCORD_INVITE}) or email {CONTACT_EMAIL}."
202
- ), ""
203
  if state == "error":
204
- return None, None, "❌ Couldn't reach the hackathon records right now. Please try again shortly.", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  participant_name = (participant_name or "").strip() or (profile.name or username)
207
  project_name = (project_name or "").strip()
@@ -210,7 +238,6 @@ def create_certificate(participant_name, project_name,
210
  PROJECT_SECTION_WITH_NAME.replace("{project_name}", project_name)
211
  if project_name else PROJECT_SECTION_EMPTY
212
  )
213
-
214
  html = CERTIFICATE_HTML_TEMPLATE.replace("{participant_name}", participant_name)
215
  html = html.replace("{project_section}", project_section)
216
 
@@ -222,19 +249,22 @@ def create_certificate(participant_name, project_name,
222
  client = get_gradio_client()
223
  image_path = client.predict(html_file=handle_file(html_path), api_name="/predict")[0]
224
  except Exception as e:
225
- return None, None, f"❌ Error generating certificate image: {e}", ""
226
 
227
- # Save to the public gallery dataset
228
  try:
229
  cert_image = Image.open(image_path)
230
- ok, msg = upload_user_certificate(cert_image, username)
231
- save_note = msg if ok else f"(generated β€” gallery save note: {msg})"
 
 
 
232
  except Exception as e:
233
  save_note = f"(generated β€” gallery save skipped: {e})"
234
 
235
  linkedin_url = generate_linkedin_url(participant_name, project_name)
236
  status = f"πŸŽ‰ Your certificate is ready, {participant_name}! {save_note}"
237
- return image_path, image_path, status, linkedin_url
238
 
239
 
240
  def render_linkedin_button(url):
@@ -335,6 +365,9 @@ with gr.Blocks(title="Build Small β€” Certificate Generator") as demo:
335
  label="Project / Space name (editable, optional)",
336
  info="Auto-filled from your Build Small submission. Edit it, or leave blank for a certificate without a project.",
337
  )
 
 
 
338
  with gr.Row(elem_classes=["bs-generate"]):
339
  generate_btn = gr.Button("Generate my certificate", variant="primary", size="lg")
340
 
@@ -351,13 +384,14 @@ with gr.Blocks(title="Build Small β€” Certificate Generator") as demo:
351
  demo.load(
352
  fn=on_load,
353
  inputs=None,
354
- outputs=[login_status, main_interface, contact_box, participant_name, project_name, data_status],
 
355
  )
356
 
357
  generate_btn.click(
358
  fn=create_certificate,
359
- inputs=[participant_name, project_name],
360
- outputs=[certificate_image, certificate_file, generation_status, linkedin_state],
361
  ).then(
362
  fn=lambda url: render_linkedin_button(url),
363
  inputs=[linkedin_state],
 
26
  from datasets import load_dataset
27
  from gradio_client import Client, handle_file
28
 
29
+ from certificate_upload_module import upload_user_certificate, get_certificate_image_path
30
 
31
  HF_TOKEN = os.getenv("HF_TOKEN")
32
 
 
117
 
118
  # ----------------------------------------------------------------------------- on login
119
  def on_load(profile: gr.OAuthProfile | None):
120
+ """Runs on page load. Drives which panel is shown, pre-fills the form, and β€” if the user
121
+ already generated a certificate β€” shows it and reveals the 'Recreate' control."""
122
+ hidden_cb = gr.update(visible=False, value=False)
123
+
124
  if profile is None:
125
  return (
126
  "Please sign in with your Hugging Face account to continue.",
127
+ gr.update(visible=False), # main_interface
128
+ gr.update(visible=False), # contact_box
129
+ "", # name
130
  gr.update(value="", visible=True), # project
131
+ "", # data_status
132
+ None, # certificate_image
133
+ hidden_cb, # recreate_checkbox
134
+ "", # generation_status
135
  )
136
 
137
  username = profile.username
138
  profile_name = profile.name or username
139
  state, full_name, space_name = lookup_participant(username)
140
 
141
+ if state in ("not_found", "error"):
 
 
 
 
 
 
 
 
 
142
  return (
143
  f"Signed in as **{username}**.",
144
  gr.update(visible=False),
145
  gr.update(visible=True),
146
+ "", gr.update(value="", visible=True), "",
147
+ None, hidden_cb, "",
 
148
  )
149
 
150
  name_value = full_name or profile_name
 
151
  if state == "found_project":
152
  status = f"βœ… Found your submission β€” **{space_name}**. Review the details below, then generate."
153
  project_update = gr.update(value=space_name, visible=True)
 
158
  )
159
  project_update = gr.update(value="", visible=True)
160
 
161
+ # Already generated a certificate before? Fetch and show it.
162
+ existing = None
163
+ try:
164
+ existing = get_certificate_image_path(username)
165
+ except Exception:
166
+ existing = None
167
+
168
+ if existing:
169
+ gen_status = (
170
+ "πŸ“œ You've **already generated** a certificate (shown below). To change it, edit your "
171
+ "details, tick **Recreate certificate**, and click generate again β€” this **replaces** "
172
+ "your existing one (one certificate per participant)."
173
+ )
174
+ cb = gr.update(visible=True, value=False)
175
+ else:
176
+ gen_status = ""
177
+ cb = gr.update(visible=False, value=False)
178
+
179
  return (
180
  f"Signed in as **{username}**.",
181
  gr.update(visible=True),
182
  gr.update(visible=False),
183
+ name_value, project_update, status,
184
+ existing, cb, gen_status,
 
185
  )
186
 
187
 
 
200
 
201
 
202
  # ----------------------------------------------------------------------------- generate
203
+ def create_certificate(participant_name, project_name, recreate,
204
  oauth_token: gr.OAuthToken | None, profile: gr.OAuthProfile | None):
205
  if profile is None:
206
+ return None, None, "❌ Please sign in first to generate your certificate.", "", gr.update(visible=False)
207
 
208
  username = profile.username
209
  state, _, _ = lookup_participant(username)
 
211
  return None, None, (
212
  "❌ We couldn't find you in the Build Small Hackathon records, so we can't issue a "
213
  f"certificate. Please reach out on [Discord]({DISCORD_INVITE}) or email {CONTACT_EMAIL}."
214
+ ), "", gr.update(visible=False)
215
  if state == "error":
216
+ return None, None, "❌ Couldn't reach the hackathon records right now. Please try again shortly.", "", gr.update(visible=False)
217
+
218
+ # If they already have one and didn't ask to recreate, show it instead of duplicating.
219
+ existing_path = None
220
+ try:
221
+ existing_path = get_certificate_image_path(username)
222
+ except Exception:
223
+ existing_path = None
224
+
225
+ if existing_path and not recreate:
226
+ return (
227
+ existing_path, existing_path,
228
+ "ℹ️ You already have a certificate (shown above). To change it, edit your details, tick "
229
+ "**Recreate certificate**, and click generate again to replace it.",
230
+ generate_linkedin_url(participant_name or username, project_name or ""),
231
+ gr.update(visible=True, value=False),
232
+ )
233
 
234
  participant_name = (participant_name or "").strip() or (profile.name or username)
235
  project_name = (project_name or "").strip()
 
238
  PROJECT_SECTION_WITH_NAME.replace("{project_name}", project_name)
239
  if project_name else PROJECT_SECTION_EMPTY
240
  )
 
241
  html = CERTIFICATE_HTML_TEMPLATE.replace("{participant_name}", participant_name)
242
  html = html.replace("{project_section}", project_section)
243
 
 
249
  client = get_gradio_client()
250
  image_path = client.predict(html_file=handle_file(html_path), api_name="/predict")[0]
251
  except Exception as e:
252
+ return None, None, f"❌ Error generating certificate image: {e}", "", gr.update(visible=bool(existing_path))
253
 
254
+ # Save (or replace) in the private gallery dataset
255
  try:
256
  cert_image = Image.open(image_path)
257
+ ok, msg = upload_user_certificate(cert_image, username, overwrite=bool(existing_path))
258
+ if existing_path:
259
+ save_note = "πŸ” This replaces your previous certificate in our records."
260
+ else:
261
+ save_note = msg if ok else f"(generated β€” gallery note: {msg})"
262
  except Exception as e:
263
  save_note = f"(generated β€” gallery save skipped: {e})"
264
 
265
  linkedin_url = generate_linkedin_url(participant_name, project_name)
266
  status = f"πŸŽ‰ Your certificate is ready, {participant_name}! {save_note}"
267
+ return image_path, image_path, status, linkedin_url, gr.update(visible=True, value=False)
268
 
269
 
270
  def render_linkedin_button(url):
 
365
  label="Project / Space name (editable, optional)",
366
  info="Auto-filled from your Build Small submission. Edit it, or leave blank for a certificate without a project.",
367
  )
368
+ recreate_checkbox = gr.Checkbox(
369
+ label="Recreate certificate (replaces my existing one)", value=False, visible=False,
370
+ )
371
  with gr.Row(elem_classes=["bs-generate"]):
372
  generate_btn = gr.Button("Generate my certificate", variant="primary", size="lg")
373
 
 
384
  demo.load(
385
  fn=on_load,
386
  inputs=None,
387
+ outputs=[login_status, main_interface, contact_box, participant_name, project_name,
388
+ data_status, certificate_image, recreate_checkbox, generation_status],
389
  )
390
 
391
  generate_btn.click(
392
  fn=create_certificate,
393
+ inputs=[participant_name, project_name, recreate_checkbox],
394
+ outputs=[certificate_image, certificate_file, generation_status, linkedin_state, recreate_checkbox],
395
  ).then(
396
  fn=lambda url: render_linkedin_button(url),
397
  inputs=[linkedin_state],
certificate_template.html CHANGED
@@ -48,10 +48,7 @@ body{width:2000px;height:1414px;overflow:hidden;}
48
  display:flex;flex-direction:column;text-align:center;}
49
 
50
  /* ---- top bar ---- */
51
- .topbar{display:flex;justify-content:space-between;align-items:center;}
52
- .eyebrow{font-family:var(--font-mono);font-size:18px;font-weight:500;letter-spacing:.30em;
53
- text-transform:uppercase;color:var(--ink-soft);display:inline-flex;align-items:center;gap:14px;}
54
- .eyebrow::before{content:"";width:34px;height:2px;background:var(--amber);display:inline-block;}
55
  .coord{font-family:var(--font-mono);font-size:16px;letter-spacing:.10em;color:var(--ink-faint);}
56
 
57
  /* ---- lockup ---- */
@@ -66,8 +63,10 @@ body{width:2000px;height:1414px;overflow:hidden;}
66
  text-transform:uppercase;color:var(--forest);margin-top:30px;}
67
  .title{font-weight:900;font-stretch:122%;font-size:142px;line-height:.92;letter-spacing:-.02em;
68
  margin-top:12px;color:var(--ink);}
69
- .subtitle{font-family:var(--font-mono);font-size:21px;font-weight:500;letter-spacing:.42em;
70
- text-transform:uppercase;color:var(--ink-2);margin-top:16px;}
 
 
71
 
72
  /* ---- body ---- */
73
  .body{flex-grow:1;display:flex;flex-direction:column;justify-content:center;}
@@ -139,8 +138,7 @@ body{width:2000px;height:1414px;overflow:hidden;}
139
 
140
  <div class="content">
141
  <div class="topbar">
142
- <span class="eyebrow">Certificate of Participation</span>
143
- <span class="coord">June 15, 2026</span>
144
  </div>
145
 
146
  <div class="lockup">
@@ -155,7 +153,7 @@ body{width:2000px;height:1414px;overflow:hidden;}
155
  <div>
156
  <div class="kicker">Hugging Face Γ— Gradio Hackathon</div>
157
  <div class="title">Build Small</div>
158
- <div class="subtitle">Small Β· Local Β· Yours</div>
159
  </div>
160
 
161
  <div class="body">
 
48
  display:flex;flex-direction:column;text-align:center;}
49
 
50
  /* ---- top bar ---- */
51
+ .topbar{display:flex;justify-content:flex-end;align-items:center;}
 
 
 
52
  .coord{font-family:var(--font-mono);font-size:16px;letter-spacing:.10em;color:var(--ink-faint);}
53
 
54
  /* ---- lockup ---- */
 
63
  text-transform:uppercase;color:var(--forest);margin-top:30px;}
64
  .title{font-weight:900;font-stretch:122%;font-size:142px;line-height:.92;letter-spacing:-.02em;
65
  margin-top:12px;color:var(--ink);}
66
+ .doctype{display:inline-flex;align-items:center;justify-content:center;gap:24px;margin-top:22px;
67
+ font-family:var(--font-mono);font-size:32px;font-weight:600;letter-spacing:.28em;
68
+ text-transform:uppercase;color:var(--forest);}
69
+ .doctype::before,.doctype::after{content:"";width:70px;height:2px;background:var(--amber);display:inline-block;}
70
 
71
  /* ---- body ---- */
72
  .body{flex-grow:1;display:flex;flex-direction:column;justify-content:center;}
 
138
 
139
  <div class="content">
140
  <div class="topbar">
141
+ <span class="coord">June 5–15, 2026</span>
 
142
  </div>
143
 
144
  <div class="lockup">
 
153
  <div>
154
  <div class="kicker">Hugging Face Γ— Gradio Hackathon</div>
155
  <div class="title">Build Small</div>
156
+ <div class="doctype">Certificate of Participation</div>
157
  </div>
158
 
159
  <div class="body">
certificate_upload_module.py CHANGED
@@ -17,8 +17,31 @@ CERTIFICATE_DATASET_NAME = "build-small-hackathon/build-small-certificates"
17
  HF_TOKEN = os.getenv("HF_TOKEN")
18
 
19
 
20
- def safe_add_certificate_to_dataset(certificate_image, hf_username, max_retries=5, retry_delay=3):
21
- """Append a certificate image to the dataset, handling empty/existing datasets and dedup."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  try:
23
  if not hf_username or not hf_username.strip():
24
  return False, "❌ Error: HF username is required"
@@ -63,8 +86,13 @@ def safe_add_certificate_to_dataset(certificate_image, hf_username, max_retries=
63
 
64
  # Dedup by username (stored in the 'label' column)
65
  if existing_dataset is not None:
66
- if hf_username in existing_dataset["label"]:
 
67
  return True, f"a certificate for '{hf_username}' already exists in the gallery."
 
 
 
 
68
 
69
  with tempfile.TemporaryDirectory() as temp_dir:
70
  if isinstance(certificate_image, PILImage.Image):
@@ -79,7 +107,7 @@ def safe_add_certificate_to_dataset(certificate_image, hf_username, max_retries=
79
  {"image": [temp_image_path], "label": [hf_username]}
80
  ).cast_column("image", Image())
81
 
82
- if existing_dataset is not None and not is_empty_dataset:
83
  combined_dataset = concatenate_datasets([existing_dataset, new_dataset])
84
  else:
85
  combined_dataset = new_dataset
@@ -99,10 +127,10 @@ def safe_add_certificate_to_dataset(certificate_image, hf_username, max_retries=
99
  return False, f"❌ Certificate upload failed: {str(e)}"
100
 
101
 
102
- def upload_user_certificate(certificate_image, hf_username):
103
  """Public entry point β€” returns (success, message)."""
104
  if not certificate_image:
105
  return False, "❌ No certificate image provided"
106
  if not hf_username or not hf_username.strip():
107
  return False, "❌ HF username is required"
108
- return safe_add_certificate_to_dataset(certificate_image, hf_username)
 
17
  HF_TOKEN = os.getenv("HF_TOKEN")
18
 
19
 
20
+ def get_certificate_image_path(hf_username):
21
+ """Return a local PNG path of this user's existing certificate, or None if they have none."""
22
+ if not hf_username or not hf_username.strip():
23
+ return None
24
+ uname = hf_username.strip().lower()
25
+ try:
26
+ ds = load_dataset(CERTIFICATE_DATASET_NAME, split="train", token=HF_TOKEN)
27
+ except Exception:
28
+ return None
29
+ try:
30
+ labels = [str(x).strip().lower() for x in ds["label"]] # no image decode
31
+ if uname not in labels:
32
+ return None
33
+ img = ds[labels.index(uname)]["image"] # decode only the match
34
+ out = os.path.join(tempfile.gettempdir(), f"existing_cert_{uname}.png")
35
+ img.save(out, "PNG")
36
+ return out
37
+ except Exception as e:
38
+ logger.warning(f"get_certificate_image_path failed: {e}")
39
+ return None
40
+
41
+
42
+ def safe_add_certificate_to_dataset(certificate_image, hf_username, overwrite=False, max_retries=5, retry_delay=3):
43
+ """Add a certificate image to the dataset. With overwrite=True, replace any existing
44
+ row for this username; otherwise refuse to duplicate."""
45
  try:
46
  if not hf_username or not hf_username.strip():
47
  return False, "❌ Error: HF username is required"
 
86
 
87
  # Dedup by username (stored in the 'label' column)
88
  if existing_dataset is not None:
89
+ present = hf_username in existing_dataset["label"]
90
+ if present and not overwrite:
91
  return True, f"a certificate for '{hf_username}' already exists in the gallery."
92
+ if present and overwrite:
93
+ existing_dataset = existing_dataset.filter(
94
+ lambda ex: str(ex.get("label", "")).strip().lower() != hf_username.lower()
95
+ )
96
 
97
  with tempfile.TemporaryDirectory() as temp_dir:
98
  if isinstance(certificate_image, PILImage.Image):
 
107
  {"image": [temp_image_path], "label": [hf_username]}
108
  ).cast_column("image", Image())
109
 
110
+ if existing_dataset is not None and not is_empty_dataset and len(existing_dataset) > 0:
111
  combined_dataset = concatenate_datasets([existing_dataset, new_dataset])
112
  else:
113
  combined_dataset = new_dataset
 
127
  return False, f"❌ Certificate upload failed: {str(e)}"
128
 
129
 
130
+ def upload_user_certificate(certificate_image, hf_username, overwrite=False):
131
  """Public entry point β€” returns (success, message)."""
132
  if not certificate_image:
133
  return False, "❌ No certificate image provided"
134
  if not hf_username or not hf_username.strip():
135
  return False, "❌ HF username is required"
136
+ return safe_add_certificate_to_dataset(certificate_image, hf_username, overwrite=overwrite)