VatsalPatel18 commited on
Commit
8976314
·
verified ·
1 Parent(s): eaea0b0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +365 -0
app.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+
8
+ import gradio as gr
9
+ from jinja2 import Template
10
+ from weasyprint import HTML, CSS
11
+
12
+ # Optional: OSS text-generation model (can be disabled if not available)
13
+ USE_OSS_MODEL = os.getenv("USE_OSS_MODEL", "0") == "1"
14
+
15
+ if USE_OSS_MODEL:
16
+ try:
17
+ from transformers import pipeline
18
+ textgen = pipeline(
19
+ "text-generation",
20
+ model="openai/gpt-oss-20b",
21
+ device_map="auto",
22
+ torch_dtype="auto",
23
+ )
24
+ except Exception as e:
25
+ print(f"[WARN] Could not init gpt-oss-20b: {e}")
26
+ textgen = None
27
+ else:
28
+ textgen = None
29
+
30
+ # -----------------------------
31
+ # HTML Template (Tailwind via CDN)
32
+ # -----------------------------
33
+ TEMPLATE_HTML = r"""
34
+ <!DOCTYPE html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="UTF-8" />
38
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
39
+ <title>{{ title }}</title>
40
+ <script src="https://cdn.tailwindcss.com"></script>
41
+ <link rel="preconnect" href="https://fonts.googleapis.com">
42
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
43
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Montserrat:wght@300;400;500;700&display=swap" rel="stylesheet">
44
+ <style>
45
+ :root {
46
+ --primary: {{ colors.primary }};
47
+ --secondary: {{ colors.secondary }};
48
+ --accent: {{ colors.accent }};
49
+ }
50
+ body { font-family: 'Montserrat', sans-serif; background: #f8f5f2; }
51
+ .certificate-border {
52
+ border: 20px solid transparent;
53
+ border-image: linear-gradient(135deg, var(--primary), var(--secondary));
54
+ border-image-slice: 1;
55
+ }
56
+ .signature-line { border-bottom: 1px solid #2c3e50; width: 220px; display: inline-block; margin-top: 36px; }
57
+ .seal { background: radial-gradient(circle, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 70%); }
58
+ .watermark svg { opacity: 0.09; }
59
+ .title-font { font-family: 'Playfair Display', serif; }
60
+ </style>
61
+ </head>
62
+ <body class="min-h-screen p-6">
63
+ <div class="certificate-border bg-white w-full max-w-4xl mx-auto p-8 md:p-12 shadow-2xl relative overflow-hidden">
64
+
65
+ {% if letterhead_base64 %}
66
+ <div class="mb-6 flex justify-center">
67
+ <img src="data:image/{{ letterhead_ext }};base64,{{ letterhead_base64 }}" alt="Letterhead" class="max-h-28 object-contain" />
68
+ </div>
69
+ {% endif %}
70
+
71
+ {% if watermark %}
72
+ <div class="watermark absolute inset-0 flex items-center justify-center z-0">
73
+ <svg width="600" height="600" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
74
+ <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"
75
+ fill="none" stroke="var(--primary)" stroke-width="2" />
76
+ </svg>
77
+ </div>
78
+ {% endif %}
79
+
80
+ <div class="relative z-10">
81
+ <!-- Header -->
82
+ <div class="text-center mb-6">
83
+ <h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-2 title-font tracking-wide">{{ heading }}</h1>
84
+ <p class="text-gray-600 uppercase tracking-widest text-sm">This is to certify that</p>
85
+ </div>
86
+
87
+ <!-- Recipient -->
88
+ <div class="text-center mb-8">
89
+ <h2 class="text-3xl md:text-4xl font-bold text-gray-800 mb-1 title-font" style="color: var(--primary)">{{ recipient_name }}</h2>
90
+ {% if recipient_role %}<p class="text-gray-600">{{ recipient_role }}</p>{% endif %}
91
+ </div>
92
+
93
+ <!-- Body -->
94
+ <div class="max-w-2xl mx-auto text-gray-700 text-lg leading-relaxed mb-8">
95
+ <p class="mb-3">has successfully completed {{ duration }} at</p>
96
+ <p class="font-bold text-xl text-gray-800 mb-4" style="color: var(--secondary)">{{ org_name }}</p>
97
+ {% if supervisor %}
98
+ <p class="mb-3">under the supervision of <span class="font-semibold">{{ supervisor }}</span>.</p>
99
+ {% endif %}
100
+ {% if project_title %}
101
+ <p class="mb-3"><span class="font-semibold">Project Title:</span> {{ project_title }}</p>
102
+ {% endif %}
103
+ {% if project_summary %}
104
+ <p class="mb-3">{{ project_summary }}</p>
105
+ {% endif %}
106
+ </div>
107
+
108
+ <!-- Meta Box -->
109
+ <div class="bg-gray-50 border rounded-lg p-6 mb-8 text-left">
110
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
111
+ <div><span class="font-semibold">Duration:</span> {{ duration }}</div>
112
+ <div><span class="font-semibold">Issue Date:</span> {{ issue_date }}</div>
113
+ {% if cert_id %}<div><span class="font-semibold">Certificate ID:</span> {{ cert_id }}</div>{% endif %}
114
+ </div>
115
+ </div>
116
+
117
+ {% if closing_note %}
118
+ <p class="text-gray-700 italic mb-8 text-center">{{ closing_note }}</p>
119
+ {% endif %}
120
+
121
+ <!-- Signatures -->
122
+ <div class="flex flex-wrap justify-between mt-10">
123
+ <div class="w-full md:w-1/2 text-center md:text-left mb-10 md:mb-0">
124
+ <div class="signature-line"></div>
125
+ <p class="text-gray-800 mt-2 font-semibold">{{ signer_name }}</p>
126
+ <p class="text-gray-600 text-sm">{{ signer_title }}</p>
127
+ <p class="text-gray-600 text-sm">{{ org_name }}</p>
128
+ </div>
129
+ <div class="w-full md:w-1/2 text-center md:text-right">
130
+ <div class="inline-block">
131
+ <div class="signature-line"></div>
132
+ <p class="text-gray-800 mt-2 font-semibold">Date</p>
133
+ <p class="text-gray-600 text-sm">{{ issue_date }}</p>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- Seal -->
139
+ {% if show_seal %}
140
+ <div class="flex justify-center mt-12">
141
+ <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"
142
+ style="border-color: var(--accent); color: var(--accent)">
143
+ Official Seal<br>{{ org_short }}
144
+ </div>
145
+ </div>
146
+ {% endif %}
147
+ </div>
148
+ </div>
149
+ </body>
150
+ </html>
151
+ """
152
+
153
+ DEFAULT_COLORS = {
154
+ "primary": "#2c3e50",
155
+ "secondary": "#4ca1af",
156
+ "accent": "#b91c1c",
157
+ }
158
+
159
+
160
+ def img_to_base64(file_obj):
161
+ if not file_obj:
162
+ return None, None
163
+ data = file_obj.read()
164
+ b64 = base64.b64encode(data).decode("utf-8")
165
+ # crude extension guess
166
+ ext = "png"
167
+ name = getattr(file_obj, "name", "")
168
+ if name.lower().endswith(".jpg") or name.lower().endswith(".jpeg"):
169
+ ext = "jpeg"
170
+ elif name.lower().endswith(".png"):
171
+ ext = "png"
172
+ return b64, ext
173
+
174
+
175
+ def render_html(values: Dict[str, Any]) -> str:
176
+ tpl = Template(TEMPLATE_HTML)
177
+ html = tpl.render(**values)
178
+ return html
179
+
180
+
181
+ def generate(values: Dict[str, Any]) -> Dict[str, Any]:
182
+ html_str = render_html(values)
183
+ html_bytes = html_str.encode("utf-8")
184
+
185
+ # Save HTML to a temp file-like
186
+ html_io = io.BytesIO(html_bytes)
187
+
188
+ # Render to PDF with WeasyPrint
189
+ pdf_io = io.BytesIO()
190
+ HTML(string=html_str, base_url=str(Path.cwd())).write_pdf(pdf_io, stylesheets=[CSS(string="@page { size: A4; margin: 18mm; }")])
191
+
192
+ # Return files
193
+ html_path = "/tmp/certificate.html"
194
+ pdf_path = "/tmp/certificate.pdf"
195
+ with open(html_path, "wb") as f:
196
+ f.write(html_bytes)
197
+ with open(pdf_path, "wb") as f:
198
+ f.write(pdf_io.getvalue())
199
+
200
+ return {
201
+ "preview_html": html_str,
202
+ "html_file": html_path,
203
+ "pdf_file": pdf_path,
204
+ }
205
+
206
+
207
+ def today_str():
208
+ # Asia/Kolkata-friendly simple date
209
+ return datetime.now().strftime("%B %d, %Y")
210
+
211
+
212
+ def build_values(
213
+ recipient_name,
214
+ recipient_role,
215
+ org_name,
216
+ org_short,
217
+ supervisor,
218
+ project_title,
219
+ project_summary,
220
+ duration,
221
+ issue_date,
222
+ signer_name,
223
+ signer_title,
224
+ closing_note,
225
+ letterhead_file,
226
+ watermark,
227
+ show_seal,
228
+ primary,
229
+ secondary,
230
+ accent,
231
+ ):
232
+ b64, ext = img_to_base64(letterhead_file)
233
+ colors = {
234
+ "primary": primary or DEFAULT_COLORS["primary"],
235
+ "secondary": secondary or DEFAULT_COLORS["secondary"],
236
+ "accent": accent or DEFAULT_COLORS["accent"],
237
+ }
238
+
239
+ values = {
240
+ "title": "Certificate of Experience",
241
+ "heading": "CERTIFICATE OF EXPERIENCE",
242
+ "recipient_name": recipient_name,
243
+ "recipient_role": recipient_role,
244
+ "org_name": org_name,
245
+ "org_short": org_short or org_name,
246
+ "supervisor": supervisor,
247
+ "project_title": project_title,
248
+ "project_summary": project_summary,
249
+ "duration": duration,
250
+ "issue_date": issue_date or today_str(),
251
+ "signer_name": signer_name,
252
+ "signer_title": signer_title,
253
+ "closing_note": closing_note,
254
+ "cert_id": f"HFR-{datetime.now().strftime('%Y%m%d')}-{str(abs(hash(recipient_name)))[0:6]}",
255
+ "letterhead_base64": b64,
256
+ "letterhead_ext": ext,
257
+ "watermark": watermark,
258
+ "show_seal": show_seal,
259
+ "colors": colors,
260
+ }
261
+ return values
262
+
263
+
264
+ def suggest_style(prompt, base_primary, base_secondary, base_accent):
265
+ """Ask the OSS model to suggest palette & copy tweaks. Falls back to defaults."""
266
+ if not textgen:
267
+ return (
268
+ base_primary,
269
+ base_secondary,
270
+ base_accent,
271
+ "Keep the formal tone. Use Playfair Display for headings and maintain a calm, professional palette.",
272
+ )
273
+
274
+ sys = (
275
+ "You are a design assistant. Given a short brief, output JSON with keys: primary, secondary, accent, note. "
276
+ "Colors must be hex values."
277
+ )
278
+ user = f"Brief: {prompt}\nBase: primary={base_primary}, secondary={base_secondary}, accent={base_accent}"
279
+ out = textgen(sys + "\n" + user, max_new_tokens=256, do_sample=True, temperature=0.6)[0]["generated_text"]
280
+
281
+ # naive parse: search for #hex and a note
282
+ import re, json
283
+ hexes = re.findall(r"#(?:[0-9a-fA-F]{3}){1,2}", out)
284
+ note = "Consider a dignified palette with strong contrast."
285
+ primary = (hexes[0] if len(hexes) > 0 else base_primary)
286
+ secondary = (hexes[1] if len(hexes) > 1 else base_secondary)
287
+ accent = (hexes[2] if len(hexes) > 2 else base_accent)
288
+ return primary, secondary, accent, note
289
+
290
+
291
+ with gr.Blocks(title="HawkFranklin Certificate Generator", theme=gr.themes.Soft()) as demo:
292
+ gr.Markdown("""
293
+ # 🧾 HawkFranklin Certificate Generator (Agent-Ready)
294
+ - Fill in the fields, preview the certificate, then export **HTML** and **PDF**.
295
+ - Optionally, use the **AI Style Assistant** (OSS 20B) to suggest color palettes & tone.
296
+ """)
297
+
298
+ with gr.Row():
299
+ with gr.Column(scale=2):
300
+ recipient_name = gr.Textbox(label="Recipient Name", value="Sonia Bara", autofocus=True)
301
+ recipient_role = gr.Textbox(label="Recipient Role (optional)", value="Intern – ML Research")
302
+ org_name = gr.Textbox(label="Organization", value="HawkFranklin Research")
303
+ org_short = gr.Textbox(label="Org Short (seal)", value="HawkFranklin")
304
+ supervisor = gr.Textbox(label="Supervisor", value="Vatsal Pravinbhai Patel, Senior Research Engineer and Manager")
305
+ project_title = gr.Textbox(label="Project Title", value="Application of Graph Neural Networks in Drug Discovery")
306
+ project_summary = gr.Textbox(label="Project Summary", value=(
307
+ "Conducted research on ML models in drug discovery, performed detailed codebase analysis, and demonstrated "
308
+ "exceptional independent research capabilities."
309
+ ))
310
+ duration = gr.Textbox(label="Duration", value="8 weeks (July 2025)")
311
+ issue_date = gr.Textbox(label="Issue Date", value=today_str())
312
+ signer_name = gr.Textbox(label="Signer Name", value="Vatsal Pravinbhai Patel")
313
+ signer_title = gr.Textbox(label="Signer Title", value="Senior Research Engineer and Manager")
314
+ closing_note = gr.Textbox(label="Closing Note (optional)", value="We commend the intern for dedication and wish them the best in future endeavors.")
315
+
316
+ letterhead = gr.File(label="Upload Letterhead (PNG/JPG)")
317
+ watermark = gr.Checkbox(label="Show Watermark", value=True)
318
+ show_seal = gr.Checkbox(label="Show Seal", value=True)
319
+
320
+ with gr.Accordion("Colors", open=False):
321
+ primary = gr.ColorPicker(label="Primary", value=DEFAULT_COLORS["primary"])
322
+ secondary = gr.ColorPicker(label="Secondary", value=DEFAULT_COLORS["secondary"])
323
+ accent = gr.ColorPicker(label="Accent (Seal)", value=DEFAULT_COLORS["accent"])
324
+
325
+ with gr.Accordion("AI Style Assistant (optional)", open=False):
326
+ ai_prompt = gr.Textbox(label="Describe desired vibe / style", placeholder="e.g., Elegant university look with teal accents")
327
+ ask_ai = gr.Button("Suggest Palette with GPT-OSS-20B")
328
+ ai_note = gr.Markdown(visible=False)
329
+
330
+ with gr.Column(scale=3):
331
+ preview = gr.HTML(label="Live Preview")
332
+ with gr.Row():
333
+ gen_btn = gr.Button("🔧 Build Preview")
334
+ export_btn = gr.Button("⬇️ Export HTML & PDF", variant="primary")
335
+ html_file = gr.File(label="HTML Output")
336
+ pdf_file = gr.File(label="PDF Output")
337
+
338
+ def do_preview(*args):
339
+ vals = build_values(*args)
340
+ out = generate(vals)
341
+ return out["preview_html"]
342
+
343
+ def do_export(*args):
344
+ vals = build_values(*args)
345
+ out = generate(vals)
346
+ return out["preview_html"], out["html_file"], out["pdf_file"]
347
+
348
+ gen_inputs = [
349
+ recipient_name, recipient_role, org_name, org_short, supervisor,
350
+ project_title, project_summary, duration, issue_date, signer_name,
351
+ signer_title, closing_note, letterhead, watermark, show_seal,
352
+ primary, secondary, accent
353
+ ]
354
+
355
+ gen_btn.click(do_preview, inputs=gen_inputs, outputs=preview)
356
+ export_btn.click(do_export, inputs=gen_inputs, outputs=[preview, html_file, pdf_file])
357
+
358
+ def use_ai(prompt, p, s, a):
359
+ P, S, A, note = suggest_style(prompt or "", p, s, a)
360
+ return gr.update(value=P), gr.update(value=S), gr.update(value=A), gr.update(value=f"**AI Note:** {note}", visible=True)
361
+
362
+ ask_ai.click(use_ai, inputs=[ai_prompt, primary, secondary, accent], outputs=[primary, secondary, accent, ai_note])
363
+
364
+ if __name__ == "__main__":
365
+ demo.launch()