Spaces:
Runtime error
Runtime error
| import base64 | |
| import io | |
| import os | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Dict, Any | |
| import gradio as gr | |
| from jinja2 import Template | |
| # ---- Runtime mode flags ------------------------------------------------------ | |
| # ZERO_GPU=1 => running as a Gradio (Python) Space on ZeroGPU (no Docker) | |
| # USE_OSS_MODEL=1 => enable palette suggestions with gpt-oss-20b | |
| ZERO_GPU = os.getenv("ZERO_GPU", "0") == "1" | |
| USE_OSS_MODEL = os.getenv("USE_OSS_MODEL", "0") == "1" | |
| # WeasyPrint is used for HTML->PDF when available (Docker/CPU Basic path). | |
| WEASYPRINT_OK = False | |
| if not ZERO_GPU: | |
| try: | |
| from weasyprint import HTML, CSS | |
| WEASYPRINT_OK = True | |
| except Exception as e: | |
| print(f"[WARN] WeasyPrint unavailable: {e}") | |
| # Optional: OSS text-generation model (palette suggestions) | |
| textgen = None | |
| if USE_OSS_MODEL: | |
| try: | |
| from transformers import pipeline | |
| textgen = pipeline( | |
| "text-generation", | |
| model="openai/gpt-oss-20b", | |
| device_map="auto", | |
| torch_dtype="auto", | |
| ) | |
| except Exception as e: | |
| print(f"[WARN] Could not init gpt-oss-20b: {e}") | |
| # If in ZeroGPU, expose at least one @spaces.GPU function and actually call it. | |
| if ZERO_GPU: | |
| try: | |
| import spaces | |
| # optional: duration=120 | |
| def suggest_palette_with_gpu(prompt: str, base_primary: str, base_secondary: str, base_accent: str): | |
| """Runs on the ZeroGPU worker. Uses OSS model if available, else returns a sane palette.""" | |
| if textgen is not None: | |
| sys = ( | |
| "You are a design assistant. Given a short brief, output JSON with keys: primary, secondary, accent, note. " | |
| "Colors must be hex values." | |
| ) | |
| user = ( | |
| f"Brief: {prompt}\nBase: primary={base_primary}, secondary={base_secondary}, accent={base_accent}" | |
| ) | |
| out = textgen(sys + " | |
| " + user, max_new_tokens=160, do_sample=True, temperature=0.6)[0][ | |
| "generated_text" | |
| ] | |
| import re | |
| hexes = re.findall(r"#(?:[0-9a-fA-F]{3}){1,2}", out) | |
| note = "AI suggested a professional palette with good contrast." | |
| primary = hexes[0] if len(hexes) > 0 else base_primary | |
| secondary = hexes[1] if len(hexes) > 1 else base_secondary | |
| accent = hexes[2] if len(hexes) > 2 else base_accent | |
| return {"primary": primary, "secondary": secondary, "accent": accent, "note": note} | |
| # Fallback without model | |
| return { | |
| "primary": base_primary, | |
| "secondary": base_secondary, | |
| "accent": base_accent, | |
| "note": "Using base colors (GPU style assistant fallback).", | |
| } | |
| except Exception as e: | |
| print(f"[WARN] Could not set up @spaces.GPU: {e}") | |
| # ----------------------------- | |
| # HTML Template (Tailwind via CDN) | |
| # ----------------------------- | |
| TEMPLATE_HTML = r""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>{{ title }}</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Montserrat:wght@300;400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: {{ colors.primary }}; | |
| --secondary: {{ colors.secondary }}; | |
| --accent: {{ colors.accent }}; | |
| } | |
| body { font-family: 'Montserrat', sans-serif; background: #f8f5f2; } | |
| .certificate-border { | |
| border: 20px solid transparent; | |
| border-image: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| border-image-slice: 1; | |
| } | |
| .signature-line { border-bottom: 1px solid #2c3e50; width: 220px; display: inline-block; margin-top: 36px; } | |
| .seal { background: radial-gradient(circle, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 70%); } | |
| .watermark svg { opacity: 0.09; } | |
| .title-font { font-family: 'Playfair Display', serif; } | |
| </style> | |
| </head> | |
| <body class="min-h-screen p-6"> | |
| <div class="certificate-border bg-white w-full max-w-4xl mx-auto p-8 md:p-12 shadow-2xl relative overflow-hidden"> | |
| {% if letterhead_base64 %} | |
| <div class="mb-6 flex justify-center"> | |
| <img src="data:image/{{ letterhead_ext }};base64,{{ letterhead_base64 }}" alt="Letterhead" class="max-h-28 object-contain" /> | |
| </div> | |
| {% endif %} | |
| {% if watermark %} | |
| <div class="watermark absolute inset-0 flex items-center justify-center z-0"> | |
| <svg width="600" height="600" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M300,150 C400,50 500,150 450,250 C500,350 400,450 300,350 C200,450 100,350 150,250 C100,150 200,50 300,150 Z" | |
| fill="none" stroke="var(--primary)" stroke-width="2" /> | |
| </svg> | |
| </div> | |
| {% endif %} | |
| <div class="relative z-10"> | |
| <!-- Header --> | |
| <div class="text-center mb-6"> | |
| <h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-2 title-font tracking-wide">{{ heading }}</h1> | |
| <p class="text-gray-600 uppercase tracking-widest text-sm">This is to certify that</p> | |
| </div> | |
| <!-- Recipient --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-3xl md:text-4xl font-bold text-gray-800 mb-1 title-font" style="color: var(--primary)">{{ recipient_name }}</h2> | |
| {% if recipient_role %}<p class="text-gray-600">{{ recipient_role }}</p>{% endif %} | |
| </div> | |
| <!-- Body --> | |
| <div class="max-w-2xl mx-auto text-gray-700 text-lg leading-relaxed mb-8"> | |
| <p class="mb-3">has successfully completed {{ duration }} at</p> | |
| <p class="font-bold text-xl text-gray-800 mb-4" style="color: var(--secondary)">{{ org_name }}</p> | |
| {% if supervisor %} | |
| <p class="mb-3">under the supervision of <span class="font-semibold">{{ supervisor }}</span>.</p> | |
| {% endif %} | |
| {% if project_title %} | |
| <p class="mb-3"><span class="font-semibold">Project Title:</span> {{ project_title }}</p> | |
| {% endif %} | |
| {% if project_summary %} | |
| <p class="mb-3">{{ project_summary }}</p> | |
| {% endif %} | |
| </div> | |
| <!-- Meta Box --> | |
| <div class="bg-gray-50 border rounded-lg p-6 mb-8 text-left"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div><span class="font-semibold">Duration:</span> {{ duration }}</div> | |
| <div><span class="font-semibold">Issue Date:</span> {{ issue_date }}</div> | |
| {% if cert_id %}<div><span class="font-semibold">Certificate ID:</span> {{ cert_id }}</div>{% endif %} | |
| </div> | |
| </div> | |
| {% if closing_note %} | |
| <p class="text-gray-700 italic mb-8 text-center">{{ closing_note }}</p> | |
| {% endif %} | |
| <!-- Signatures --> | |
| <div class="flex flex-wrap justify-between mt-10"> | |
| <div class="w-full md:w-1/2 text-center md:text-left mb-10 md:mb-0"> | |
| <div class="signature-line"></div> | |
| <p class="text-gray-800 mt-2 font-semibold">{{ signer_name }}</p> | |
| <p class="text-gray-600 text-sm">{{ signer_title }}</p> | |
| <p class="text-gray-600 text-sm">{{ org_name }}</p> | |
| </div> | |
| <div class="w-full md:w-1/2 text-center md:text-right"> | |
| <div class="inline-block"> | |
| <div class="signature-line"></div> | |
| <p class="text-gray-800 mt-2 font-semibold">Date</p> | |
| <p class="text-gray-600 text-sm">{{ issue_date }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Seal --> | |
| {% if show_seal %} | |
| <div class="flex justify-center mt-12"> | |
| <div class="seal w-24 h-24 rounded-full border-4 flex items-center justify-center font-bold text-sm text-center p-2 rotate-12" | |
| style="border-color: var(--accent); color: var(--accent)"> | |
| Official Seal<br>{{ org_short }} | |
| </div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| DEFAULT_COLORS = { | |
| "primary": "#2c3e50", | |
| "secondary": "#4ca1af", | |
| "accent": "#b91c1c", | |
| } | |
| def img_to_base64(file_obj): | |
| if not file_obj: | |
| return None, None | |
| data = file_obj.read() | |
| b64 = base64.b64encode(data).decode("utf-8") | |
| # crude extension guess | |
| ext = "png" | |
| name = getattr(file_obj, "name", "") | |
| if name.lower().endswith(".jpg") or name.lower().endswith(".jpeg"): | |
| ext = "jpeg" | |
| elif name.lower().endswith(".png"): | |
| ext = "png" | |
| return b64, ext | |
| def render_html(values: Dict[str, Any]) -> str: | |
| tpl = Template(TEMPLATE_HTML) | |
| html = tpl.render(**values) | |
| return html | |
| def generate(values: Dict[str, Any]) -> Dict[str, Any]: | |
| """Build HTML and (if supported) PDF. On ZeroGPU or without WeasyPrint, return HTML only.""" | |
| html_str = render_html(values) | |
| html_bytes = html_str.encode("utf-8") | |
| # Save HTML | |
| html_path = "/tmp/certificate.html" | |
| with open(html_path, "wb") as f: | |
| f.write(html_bytes) | |
| pdf_path = None | |
| if WEASYPRINT_OK and not ZERO_GPU: | |
| try: | |
| pdf_io = io.BytesIO() | |
| HTML(string=html_str, base_url=str(Path.cwd())).write_pdf( | |
| pdf_io, stylesheets=[CSS(string="@page { size: A4; margin: 18mm; }")] | |
| ) | |
| pdf_path = "/tmp/certificate.pdf" | |
| with open(pdf_path, "wb") as f: | |
| f.write(pdf_io.getvalue()) | |
| except Exception as e: | |
| print(f"[WARN] PDF generation failed: {e}") | |
| pdf_path = None | |
| return { | |
| "preview_html": html_str, | |
| "html_file": html_path, | |
| "pdf_file": pdf_path, | |
| } | |
| def today_str(): | |
| # Asia/Kolkata-friendly simple date | |
| return datetime.now().strftime("%B %d, %Y") | |
| def build_values( | |
| recipient_name, | |
| recipient_role, | |
| org_name, | |
| org_short, | |
| supervisor, | |
| project_title, | |
| project_summary, | |
| duration, | |
| issue_date, | |
| signer_name, | |
| signer_title, | |
| closing_note, | |
| letterhead_file, | |
| watermark, | |
| show_seal, | |
| primary, | |
| secondary, | |
| accent, | |
| ): | |
| b64, ext = img_to_base64(letterhead_file) | |
| colors = { | |
| "primary": primary or DEFAULT_COLORS["primary"], | |
| "secondary": secondary or DEFAULT_COLORS["secondary"], | |
| "accent": accent or DEFAULT_COLORS["accent"], | |
| } | |
| values = { | |
| "title": "Certificate of Experience", | |
| "heading": "CERTIFICATE OF EXPERIENCE", | |
| "recipient_name": recipient_name, | |
| "recipient_role": recipient_role, | |
| "org_name": org_name, | |
| "org_short": org_short or org_name, | |
| "supervisor": supervisor, | |
| "project_title": project_title, | |
| "project_summary": project_summary, | |
| "duration": duration, | |
| "issue_date": issue_date or today_str(), | |
| "signer_name": signer_name, | |
| "signer_title": signer_title, | |
| "closing_note": closing_note, | |
| "cert_id": f"HFR-{datetime.now().strftime('%Y%m%d')}-{str(abs(hash(recipient_name)))[0:6]}", | |
| "letterhead_base64": b64, | |
| "letterhead_ext": ext, | |
| "watermark": watermark, | |
| "show_seal": show_seal, | |
| "colors": colors, | |
| } | |
| return values | |
| def suggest_style_cpu(prompt, base_primary, base_secondary, base_accent): | |
| """CPU path: use OSS model if available (non-GPU), else fallback to base.""" | |
| if textgen is None: | |
| return ( | |
| base_primary, | |
| base_secondary, | |
| base_accent, | |
| "Keep the formal tone. Use Playfair Display for headings and maintain a calm, professional palette.", | |
| ) | |
| sys = ( | |
| "You are a design assistant. Given a short brief, output JSON with keys: primary, secondary, accent, note. " | |
| "Colors must be hex values." | |
| ) | |
| user = f"Brief: {prompt} | |
| Base: primary={base_primary}, secondary={base_secondary}, accent={base_accent}" | |
| out = textgen(sys + " | |
| " + user, max_new_tokens=160, do_sample=True, temperature=0.6)[0]["generated_text"] | |
| import re | |
| hexes = re.findall(r"#(?:[0-9a-fA-F]{3}){1,2}", out) | |
| note = "Consider a dignified palette with strong contrast." | |
| primary = hexes[0] if len(hexes) > 0 else base_primary | |
| secondary = hexes[1] if len(hexes) > 1 else base_secondary | |
| accent = hexes[2] if len(hexes) > 2 else base_accent | |
| return primary, secondary, accent, note | |
| with gr.Blocks(title="HawkFranklin Certificate Generator", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| f""" | |
| # 🧾 HawkFranklin Certificate Generator (Agent-Ready) | |
| - Fill in the fields, preview the certificate, then export **HTML**{'' if WEASYPRINT_OK and not ZERO_GPU else ''} {'and **PDF**' if WEASYPRINT_OK and not ZERO_GPU else '(PDF disabled: ZeroGPU or missing WeasyPrint)'}. | |
| - Optional **AI Style Assistant** (OSS 20B). {'Runs on ZeroGPU GPU call.' if ZERO_GPU else 'Runs on CPU if enabled.'} | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| recipient_name = gr.Textbox(label="Recipient Name", value="Sonia Bara", autofocus=True) | |
| recipient_role = gr.Textbox(label="Recipient Role (optional)", value="Intern – ML Research") | |
| org_name = gr.Textbox(label="Organization", value="HawkFranklin Research") | |
| org_short = gr.Textbox(label="Org Short (seal)", value="HawkFranklin") | |
| supervisor = gr.Textbox(label="Supervisor", value="Vatsal Pravinbhai Patel, Senior Research Engineer and Manager") | |
| project_title = gr.Textbox(label="Project Title", value="Application of Graph Neural Networks in Drug Discovery") | |
| project_summary = gr.Textbox(label="Project Summary", value=( | |
| "Conducted research on ML models in drug discovery, performed detailed codebase analysis, and demonstrated " | |
| "exceptional independent research capabilities." | |
| )) | |
| duration = gr.Textbox(label="Duration", value="8 weeks (July 2025)") | |
| issue_date = gr.Textbox(label="Issue Date", value=today_str()) | |
| signer_name = gr.Textbox(label="Signer Name", value="Vatsal Pravinbhai Patel") | |
| signer_title = gr.Textbox(label="Signer Title", value="Senior Research Engineer and Manager") | |
| closing_note = gr.Textbox(label="Closing Note (optional)", value="We commend the intern for dedication and wish them the best in future endeavors.") | |
| letterhead = gr.File(label="Upload Letterhead (PNG/JPG)") | |
| watermark = gr.Checkbox(label="Show Watermark", value=True) | |
| show_seal = gr.Checkbox(label="Show Seal", value=True) | |
| with gr.Accordion("Colors", open=False): | |
| primary = gr.ColorPicker(label="Primary", value=DEFAULT_COLORS["primary"]) | |
| secondary = gr.ColorPicker(label="Secondary", value=DEFAULT_COLORS["secondary"]) | |
| accent = gr.ColorPicker(label="Accent (Seal)", value=DEFAULT_COLORS["accent"]) | |
| with gr.Accordion("AI Style Assistant (optional)", open=False): | |
| ai_prompt = gr.Textbox(label="Describe desired vibe / style", placeholder="e.g., Elegant university look with teal accents") | |
| ask_ai = gr.Button("Suggest Palette (AI)") | |
| ai_note = gr.Markdown(visible=False) | |
| with gr.Column(scale=3): | |
| preview = gr.HTML(label="Live Preview") | |
| with gr.Row(): | |
| gen_btn = gr.Button("🔧 Build Preview") | |
| export_btn = gr.Button( | |
| "⬇️ Export HTML{}".format(" & PDF" if WEASYPRINT_OK and not ZERO_GPU else " (PDF disabled)"), | |
| variant="primary", | |
| ) | |
| html_file = gr.File(label="HTML Output") | |
| pdf_file = gr.File(label="PDF Output") | |
| def do_preview(*args): | |
| vals = build_values(*args) | |
| out = generate(vals) | |
| return out["preview_html"] | |
| def do_export(*args): | |
| vals = build_values(*args) | |
| out = generate(vals) | |
| # When PDF is disabled, pdf_file will be None; Gradio handles None -> nothing to download | |
| return out["preview_html"], out["html_file"], out["pdf_file"] | |
| gen_inputs = [ | |
| recipient_name, recipient_role, org_name, org_short, supervisor, | |
| project_title, project_summary, duration, issue_date, signer_name, | |
| signer_title, closing_note, letterhead, watermark, show_seal, | |
| primary, secondary, accent | |
| ] | |
| gen_btn.click(do_preview, inputs=gen_inputs, outputs=preview) | |
| export_btn.click(do_export, inputs=gen_inputs, outputs=[preview, html_file, pdf_file]) | |
| # Style assistant wiring (ZeroGPU uses @spaces.GPU function; CPU uses local suggestor) | |
| def use_ai_cpu(prompt, p, s, a): | |
| P, S, A, note = suggest_style_cpu(prompt or "", p, s, a) | |
| return gr.update(value=P), gr.update(value=S), gr.update(value=A), gr.update(value=f"**AI Note:** {note}", visible=True) | |
| if ZERO_GPU: | |
| def use_ai_gpu(prompt, p, s, a): | |
| res = suggest_palette_with_gpu(prompt or "", p, s, a) | |
| return ( | |
| gr.update(value=res.get("primary", p)), | |
| gr.update(value=res.get("secondary", s)), | |
| gr.update(value=res.get("accent", a)), | |
| gr.update(value=f"**AI Note:** {res.get('note','')}", visible=True), | |
| ) | |
| ask_ai.click(use_ai_gpu, inputs=[ai_prompt, primary, secondary, accent], outputs=[primary, secondary, accent, ai_note]) | |
| else: | |
| ask_ai.click(use_ai_cpu, inputs=[ai_prompt, primary, secondary, accent], outputs=[primary, secondary, accent, ai_note]) | |
| if __name__ == "__main__": | |
| # Disable SSR to avoid Node server quirks in Spaces logs | |
| demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False) | |