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) | |