Download app.py from build-small-hackathon/certificate-generator: direct link, hf CLI and curl.
- Browser
- Download file 20.9 kB
-
https://huggingface.co/spaces/build-small-hackathon/certificate-generator/resolve/main/app.py
- Command line
-
hf download hf://spaces/build-small-hackathon/certificate-generator/app.py
-
curl -L -o app.py https://huggingface.co/spaces/build-small-hackathon/certificate-generator/resolve/main/app.py
20.9 kB
| """ | |
| 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 = '<div class="project"><span class="lbl">Project</span> {project_name}</div>' | |
| 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 | |
| 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.""" | |
| reset_cb = gr.update(value=False) # never toggle the checkbox's own visibility (keeps it interactive) | |
| 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 | |
| None, # certificate_file | |
| reset_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, None, reset_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, with guidance up top. | |
| existing = None | |
| try: | |
| existing = get_certificate_image_path(username) | |
| except Exception: | |
| existing = None | |
| if existing: | |
| gen_status = ( | |
| "### π You already have a certificate\n" | |
| "It's shown below β **download it** and use it however you like (LinkedIn, socials, etc.).\n\n" | |
| "- Want a version with **different details**? Edit your name/project and click " | |
| "**Generate my certificate** β that's just a fresh preview to download and **won't change** " | |
| "the copy saved in our records.\n" | |
| "- Want to **replace** your saved copy? Tick **Recreate certificate** below, then generate. " | |
| "There's one certificate per participant, so the new one overwrites the old." | |
| ) | |
| cb = reset_cb | |
| else: | |
| gen_status = "" | |
| cb = reset_cb | |
| return ( | |
| f"Signed in as **{username}**.", | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| name_value, project_update, status, | |
| existing, 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): | |
| hide_cb = gr.update(value=False) | |
| if profile is None: | |
| return None, None, "β Please sign in first to generate your certificate.", "", hide_cb | |
| 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}." | |
| ), "", hide_cb | |
| if state == "error": | |
| return None, None, "β Couldn't reach the hackathon records right now. Please try again shortly.", "", hide_cb | |
| # Does a saved certificate already exist for this user? | |
| existing_path = None | |
| try: | |
| existing_path = get_certificate_image_path(username) | |
| except Exception: | |
| existing_path = None | |
| # Always render a fresh certificate from the CURRENT field values. | |
| 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(value=False) | |
| show_cb = gr.update(value=False) | |
| linkedin_url = generate_linkedin_url(participant_name, project_name) | |
| # Decide what happens to the SAVED copy in the dataset. | |
| if existing_path is None: | |
| # First time β save it. | |
| try: | |
| ok, msg = upload_user_certificate(Image.open(image_path), username, overwrite=False) | |
| note = ("π Your certificate is ready and has been **saved to our records**. Download it " | |
| "below β you can edit your details and regenerate anytime.") if ok else \ | |
| f"β οΈ Certificate generated, but saving hit an issue: {msg}" | |
| except Exception as e: | |
| note = f"β οΈ Certificate generated, but saving was skipped: {e}" | |
| return image_path, image_path, note, linkedin_url, show_cb | |
| if recreate: | |
| # Replace the saved copy. | |
| try: | |
| ok, msg = upload_user_certificate(Image.open(image_path), username, overwrite=True) | |
| note = ("π **Done β your new certificate has replaced the previous one saved in our " | |
| "records.** Download the updated copy below.") if ok else \ | |
| f"β οΈ Generated, but replacing the saved copy hit an issue: {msg}" | |
| except Exception as e: | |
| note = f"β οΈ Generated, but replacing the saved copy was skipped: {e}" | |
| return image_path, image_path, note, linkedin_url, show_cb | |
| # Existing copy + recreate NOT ticked β fresh preview only, dataset untouched. | |
| note = ( | |
| "π **Here's a new preview with your latest details** β download it below. " | |
| "Your **saved** certificate in our records is **unchanged**. " | |
| "To make this version the official one, tick **Recreate certificate** above and generate again." | |
| ) | |
| return image_path, image_path, note, linkedin_url, show_cb | |
| def render_linkedin_button(url): | |
| if not url: | |
| return "<div style='padding:.5rem;color:var(--ink-soft)'>Generate your certificate to enable LinkedIn sharing.</div>" | |
| return f""" | |
| <a href="{url}" target="_blank" class="bs-linkedin">Add to LinkedIn profile β</a> | |
| <p style="margin-top:8px;font-size:.85rem;color:#6b6655"> | |
| Opens LinkedIn with the certification pre-filled β then upload your downloaded image.</p> | |
| """ | |
| # ----------------------------------------------------------------------------- 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; | |
| } | |
| /* Theme-proof: force the light "paper" look for ALL visitors, even on Gradio dark mode. | |
| We override Gradio's theme CSS variables for both :root and .dark so blocks, inputs, | |
| labels and text never go dark-on-dark or light-on-light. */ | |
| .gradio-container, .dark, body, body.dark{ | |
| color-scheme: light !important; | |
| --body-background-fill:#f4eee1 !important; | |
| --background-fill-primary:#ffffff !important; | |
| --background-fill-secondary:#efe7d4 !important; | |
| --block-background-fill:#ffffff !important; | |
| --block-label-background-fill:#efe7d4 !important; | |
| --panel-background-fill:#efe7d4 !important; | |
| --block-label-text-color:#33312b !important; | |
| --block-title-text-color:#33312b !important; | |
| --block-info-text-color:rgba(51,49,43,.62) !important; | |
| --body-text-color:#33312b !important; | |
| --body-text-color-subdued:rgba(51,49,43,.60) !important; | |
| --input-background-fill:#ffffff !important; | |
| --input-text-color:#33312b !important; | |
| --input-placeholder-color:rgba(51,49,43,.45) !important; | |
| --border-color-primary:#33312b !important; | |
| --checkbox-background-color:#ffffff !important; | |
| --checkbox-background-color-selected:#3d6a55 !important; | |
| --checkbox-border-color:#33312b !important; | |
| --link-text-color:#3d6a55 !important; | |
| } | |
| .gradio-container{max-width:880px !important;margin:auto !important; | |
| font-family:'Archivo',system-ui,sans-serif !important;background:var(--paper) !important;color:var(--ink) !important;} | |
| .gradio-container .prose, .gradio-container .prose *, .gradio-container label, | |
| .gradio-container p, .gradio-container span, .gradio-container .md{color:var(--ink) !important;} | |
| .gradio-container .prose strong{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:not([type='checkbox']):not([type='radio']), textarea{border-radius:0 !important; | |
| border:2px solid var(--ink) !important;background:#fff !important;color:var(--ink) !important; | |
| -webkit-text-fill-color:var(--ink) !important; | |
| font-family:'Spline Sans Mono',monospace !important;} | |
| input::placeholder, textarea::placeholder{color:rgba(51,49,43,.45) !important;-webkit-text-fill-color:rgba(51,49,43,.45) !important;} | |
| /* Download (PNG) file row β force light bg + readable ink text in any theme */ | |
| #cert-file, #cert-file *{background-color:#fff !important;color:var(--ink) !important; | |
| -webkit-text-fill-color:var(--ink) !important;border-color:var(--ink) !important;fill:var(--ink) !important;} | |
| #cert-file a, #cert-file a *{color:var(--forest) !important;-webkit-text-fill-color:var(--forest) !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( | |
| """ | |
| <div class="bs-hero"> | |
| <div class="kick">Hugging Face Γ Gradio Β· 2026</div> | |
| <h1>Build Small β Certificate</h1> | |
| <p>Sign in with Hugging Face to claim your certificate of participation. Built something | |
| small, local, and yours? Let's make it official.</p> | |
| </div> | |
| """ | |
| ) | |
| 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""" | |
| <div class="bs-contact"> | |
| <strong>We couldn't find you in the Build Small Hackathon records.</strong><br> | |
| Certificates are issued to registered participants and Build Small org Space contributors. | |
| If you think this is a mistake, reach out on | |
| <a href="{DISCORD_INVITE}" target="_blank">Discord</a> or email | |
| <a href="mailto:{CONTACT_EMAIL}">{CONTACT_EMAIL}</a>. | |
| </div> | |
| """, | |
| visible=False, | |
| ) | |
| with gr.Column(visible=False) as main_interface: | |
| gr.HTML('<div class="bs-section">Your details</div>') | |
| data_status = gr.Markdown("") | |
| # status / "you already have a certificate" notice β kept at the TOP, above the form | |
| generation_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 β replace the copy saved in our records", | |
| info="Only matters once you already have a saved certificate β tick it to overwrite the stored copy.", | |
| value=False, visible=True, interactive=True, | |
| ) | |
| with gr.Row(elem_classes=["bs-generate"]): | |
| generate_btn = gr.Button("Generate my certificate", variant="primary", size="lg") | |
| gr.HTML('<div class="bs-section">Your certificate</div>') | |
| certificate_image = gr.Image(label="Preview", type="filepath", interactive=False) | |
| certificate_file = gr.File(label="Download (PNG)", interactive=False, elem_id="cert-file") | |
| gr.HTML('<div class="bs-section">Share</div>') | |
| 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, certificate_file, 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) | |