VatsalPatel18's picture
Update app.py
a852a36 verified
raw
history blame
17.9 kB
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
@spaces.GPU() # 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)