""" Build Small Hackathon — Certificate Generator ============================================= Workflow: 1. User signs in with Hugging Face (OAuth). 2. We take their HF username and look it up in the private eligibility dataset `build-small-hackathon/build-small-apps-for-certificates` (registered participants ∪ org Space contributors). • username found WITH a space -> autofill full name + project name • username found WITHOUT a space -> autofill full name, no project • username not found -> show contact message, do not proceed 3. All autofilled fields are editable. 4. A button generates the certificate (HTML -> PNG via the renderer Space) and saves it to the public gallery dataset `build-small-hackathon/build-small-certificates`. """ import os import uuid import inspect import tempfile import urllib.parse from functools import lru_cache import gradio as gr import pandas as pd from PIL import Image from datasets import load_dataset from gradio_client import Client, handle_file from certificate_upload_module import upload_user_certificate, get_certificate_image_path HF_TOKEN = os.getenv("HF_TOKEN") ELIGIBILITY_DATASET = "build-small-hackathon/build-small-apps-for-certificates" RENDERER_SPACE = "https://ysharma-hackathon-certificate-html-to-image.hf.space/" DISCORD_INVITE = "https://discord.gg/92sEPT2Zhv" CONTACT_EMAIL = "hello@gradio.app" _TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "certificate_template.html") with open(_TEMPLATE_PATH, encoding="utf-8") as _f: CERTIFICATE_HTML_TEMPLATE = _f.read() PROJECT_SECTION_WITH_NAME = '
Project {project_name}
' PROJECT_SECTION_EMPTY = "" # ----------------------------------------------------------------------------- renderer client _gradio_client = None def get_gradio_client(): """Lazy init of the HTML->image renderer client (avoids event-loop issues). The auth kwarg for gradio_client.Client changed across versions (`hf_token` vs `token`), so pick whichever the installed version exposes. """ global _gradio_client if _gradio_client is None: params = inspect.signature(Client.__init__).parameters attempts = [] if "hf_token" in params: attempts.append({"hf_token": HF_TOKEN}) if "token" in params: attempts.append({"token": HF_TOKEN}) attempts.append({"headers": {"Authorization": f"Bearer {HF_TOKEN}"}}) last_err = None for kwargs in attempts: try: _gradio_client = Client(RENDERER_SPACE, **kwargs) break except TypeError as e: last_err = e if _gradio_client is None: raise last_err return _gradio_client # ----------------------------------------------------------------------------- eligibility lookup @lru_cache(maxsize=1) def _eligibility_index(): """Load the eligibility dataset once into {username_lower: (full_name, space_name)}.""" ds = load_dataset(ELIGIBILITY_DATASET, split="train", token=HF_TOKEN) df = ds.to_pandas().fillna("") index = {} for _, row in df.iterrows(): uname = str(row.get("hf_username", "")).strip() if not uname: continue index[uname.lower()] = ( str(row.get("full_name", "")).strip(), str(row.get("space_name", "")).strip(), ) return index def lookup_participant(username: str): """Return (state, full_name, space_name). state ∈ {"found_project", "found_no_project", "not_found"}. """ if not username: return "not_found", "", "" try: index = _eligibility_index() except Exception as e: print(f"[ERROR] eligibility load failed: {e}") return "error", "", "" entry = index.get(username.strip().lower()) if entry is None: return "not_found", "", "" full_name, space_name = entry if space_name: return "found_project", full_name, space_name return "found_no_project", full_name, "" # ----------------------------------------------------------------------------- on login def on_load(profile: gr.OAuthProfile | None): """Runs on page load. Drives which panel is shown, pre-fills the form, and — if the user already generated a certificate — shows it and reveals the 'Recreate' control.""" hidden_cb = gr.update(visible=False, value=False) if profile is None: return ( "Please sign in with your Hugging Face account to continue.", gr.update(visible=False), # main_interface gr.update(visible=False), # contact_box "", # name gr.update(value="", visible=True), # project "", # data_status None, # certificate_image hidden_cb, # recreate_checkbox "", # generation_status ) username = profile.username profile_name = profile.name or username state, full_name, space_name = lookup_participant(username) if state in ("not_found", "error"): return ( f"Signed in as **{username}**.", gr.update(visible=False), gr.update(visible=True), "", gr.update(value="", visible=True), "", None, hidden_cb, "", ) name_value = full_name or profile_name if state == "found_project": status = f"✅ Found your submission — **{space_name}**. Review the details below, then generate." project_update = gr.update(value=space_name, visible=True) else: # found_no_project status = ( "✅ You're on the participant list! We couldn't find a Space submission under your " "name — you can add a project below or leave it blank." ) project_update = gr.update(value="", visible=True) # Already generated a certificate before? Fetch and show it. existing = None try: existing = get_certificate_image_path(username) except Exception: existing = None if existing: gen_status = ( "📜 You've **already generated** a certificate (shown below). To change it, edit your " "details, tick **Recreate certificate**, and click generate again — this **replaces** " "your existing one (one certificate per participant)." ) cb = gr.update(visible=True, value=False) else: gen_status = "" cb = gr.update(visible=False, value=False) return ( f"Signed in as **{username}**.", gr.update(visible=True), gr.update(visible=False), name_value, project_update, status, existing, cb, gen_status, ) # ----------------------------------------------------------------------------- LinkedIn helper def generate_linkedin_url(participant_name, project_name): params = { "startTask": "CERTIFICATION_NAME", "name": "Build Small Hackathon 2026", "organizationName": "Hugging Face", "issueYear": "2026", "issueMonth": "6", } return "https://www.linkedin.com/profile/add?" + urllib.parse.urlencode( params, quote_via=urllib.parse.quote ) # ----------------------------------------------------------------------------- generate def create_certificate(participant_name, project_name, recreate, oauth_token: gr.OAuthToken | None, profile: gr.OAuthProfile | None): if profile is None: return None, None, "❌ Please sign in first to generate your certificate.", "", gr.update(visible=False) username = profile.username state, _, _ = lookup_participant(username) if state == "not_found": return None, None, ( "❌ We couldn't find you in the Build Small Hackathon records, so we can't issue a " f"certificate. Please reach out on [Discord]({DISCORD_INVITE}) or email {CONTACT_EMAIL}." ), "", gr.update(visible=False) if state == "error": return None, None, "❌ Couldn't reach the hackathon records right now. Please try again shortly.", "", gr.update(visible=False) # If they already have one and didn't ask to recreate, show it instead of duplicating. existing_path = None try: existing_path = get_certificate_image_path(username) except Exception: existing_path = None if existing_path and not recreate: return ( existing_path, existing_path, "ℹ️ You already have a certificate (shown above). To change it, edit your details, tick " "**Recreate certificate**, and click generate again to replace it.", generate_linkedin_url(participant_name or username, project_name or ""), gr.update(visible=True, value=False), ) participant_name = (participant_name or "").strip() or (profile.name or username) project_name = (project_name or "").strip() project_section = ( PROJECT_SECTION_WITH_NAME.replace("{project_name}", project_name) if project_name else PROJECT_SECTION_EMPTY ) html = CERTIFICATE_HTML_TEMPLATE.replace("{participant_name}", participant_name) html = html.replace("{project_section}", project_section) with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as f: f.write(html) html_path = f.name try: client = get_gradio_client() image_path = client.predict(html_file=handle_file(html_path), api_name="/predict")[0] except Exception as e: return None, None, f"❌ Error generating certificate image: {e}", "", gr.update(visible=bool(existing_path)) # Save (or replace) in the private gallery dataset try: cert_image = Image.open(image_path) ok, msg = upload_user_certificate(cert_image, username, overwrite=bool(existing_path)) if existing_path: save_note = "🔁 This replaces your previous certificate in our records." else: save_note = msg if ok else f"(generated — gallery note: {msg})" except Exception as e: save_note = f"(generated — gallery save skipped: {e})" linkedin_url = generate_linkedin_url(participant_name, project_name) status = f"🎉 Your certificate is ready, {participant_name}! {save_note}" return image_path, image_path, status, linkedin_url, gr.update(visible=True, value=False) def render_linkedin_button(url): if not url: return "
Generate your certificate to enable LinkedIn sharing.
" return f""" Add to LinkedIn profile →

Opens LinkedIn with the certification pre-filled — then upload your downloaded image.

""" # ----------------------------------------------------------------------------- theming CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800;900&family=Spline+Sans+Mono:wght@400;500;600&display=swap'); :root{ --paper:#f4eee1; --kraft:#e4d5b7; --kraft-deep:#d6c19a; --line:#cdbb95; --ink:#33312b; --ink-soft:rgba(51,49,43,.62); --forest:#3d6a55; --forest-ink:#20392d; --amber:#e0913a; } .gradio-container{max-width:880px !important;margin:auto !important; font-family:'Archivo',system-ui,sans-serif !important;background:var(--paper) !important;} .gradio-container .prose, .gradio-container label, .gradio-container p{color:var(--ink) !important;} .bs-hero{position:relative;overflow:hidden;background:var(--kraft);border:2px solid var(--ink); box-shadow:6px 6px 0 var(--ink);padding:30px 34px;margin-bottom:22px;} .bs-hero .kick{font-family:'Spline Sans Mono',monospace;font-size:12px;letter-spacing:.24em; text-transform:uppercase;color:var(--forest);font-weight:600;} .bs-hero h1{font-family:'Archivo';font-weight:900;font-stretch:120%;font-size:46px;line-height:.95; letter-spacing:-.02em;color:var(--ink);margin:8px 0 6px;} .bs-hero p{color:#5a5446;font-size:15px;margin:0;} .bs-section{font-family:'Spline Sans Mono',monospace;font-size:12px;letter-spacing:.18em; text-transform:uppercase;color:var(--ink-soft);font-weight:600;margin:6px 0 2px;display:flex; align-items:center;gap:9px;} .bs-section::before{content:"";width:20px;height:2px;background:var(--amber);display:inline-block;} button.primary, .bs-generate button{background:var(--forest) !important;border:2px solid var(--forest-ink) !important; color:#fff !important;border-radius:0 !important;font-family:'Archivo' !important;font-weight:800 !important; box-shadow:4px 4px 0 var(--forest-ink) !important;transition:transform .1s,box-shadow .1s !important;} .bs-generate button:hover{transform:translate(2px,2px) !important;box-shadow:2px 2px 0 var(--forest-ink) !important;} .bs-contact{background:#fbeee0;border:2px solid var(--amber);box-shadow:4px 4px 0 var(--ink);padding:18px 22px;} .bs-contact a{color:var(--forest);font-weight:700;} .bs-linkedin{display:inline-block;background:var(--ink);color:var(--paper) !important;text-decoration:none; font-weight:800;padding:11px 20px;border:2px solid var(--ink);box-shadow:3px 3px 0 var(--amber);} input, textarea{border-radius:0 !important;border:2px solid var(--ink) !important; background:#fff !important;font-family:'Spline Sans Mono',monospace !important;} footer{display:none !important;} """ BS_THEME = gr.themes.Base( primary_hue=gr.themes.colors.green, secondary_hue=gr.themes.colors.orange, neutral_hue=gr.themes.colors.stone, font=gr.themes.GoogleFont("Archivo"), ).set(button_large_radius="0px", button_small_radius="0px", block_radius="0px") # ----------------------------------------------------------------------------- UI with gr.Blocks(title="Build Small — Certificate Generator") as demo: gr.HTML( """
Hugging Face × Gradio · 2026

Build Small — Certificate

Sign in with Hugging Face to claim your certificate of participation. Built something small, local, and yours? Let's make it official.

""" ) with gr.Group(): login_btn = gr.LoginButton(value="Sign in with Hugging Face") login_status = gr.Markdown("Please sign in with your Hugging Face account to continue.") contact_box = gr.HTML( f"""
We couldn't find you in the Build Small Hackathon records.
Certificates are issued to registered participants and Build Small org Space contributors. If you think this is a mistake, reach out on Discord or email {CONTACT_EMAIL}.
""", visible=False, ) with gr.Column(visible=False) as main_interface: gr.HTML('
Your details
') data_status = gr.Markdown("") gr.Markdown( "✏️ **These details were auto-filled from the Build Small Hackathon records — " "you can edit both your name and your project/Space name** below before generating " "your certificate." ) participant_name = gr.Textbox( label="Full name (editable)", info="This appears on your certificate — edit if you'd like it shown differently.", ) project_name = gr.Textbox( label="Project / Space name (editable, optional)", info="Auto-filled from your Build Small submission. Edit it, or leave blank for a certificate without a project.", ) recreate_checkbox = gr.Checkbox( label="Recreate certificate (replaces my existing one)", value=False, visible=False, ) with gr.Row(elem_classes=["bs-generate"]): generate_btn = gr.Button("Generate my certificate", variant="primary", size="lg") gr.HTML('
Your certificate
') certificate_image = gr.Image(label="Preview", type="filepath", interactive=False) certificate_file = gr.File(label="Download (PNG)", interactive=False) generation_status = gr.Markdown("") gr.HTML('
Share
') linkedin_html = gr.HTML(render_linkedin_button("")) linkedin_state = gr.State("") demo.load( fn=on_load, inputs=None, outputs=[login_status, main_interface, contact_box, participant_name, project_name, data_status, certificate_image, recreate_checkbox, generation_status], ) generate_btn.click( fn=create_certificate, inputs=[participant_name, project_name, recreate_checkbox], outputs=[certificate_image, certificate_file, generation_status, linkedin_state, recreate_checkbox], ).then( fn=lambda url: render_linkedin_button(url), inputs=[linkedin_state], outputs=[linkedin_html], ) if __name__ == "__main__": demo.launch(theme=BS_THEME, css=CUSTOM_CSS)