import os import json import uuid import base64 import tempfile import pandas as pd import gradio as gr import pandas as pd from io import BytesIO from pathlib import Path from openai import OpenAI from user_agents import parse from dotenv import load_dotenv from datasets import load_dataset from PIL import Image, ImageDraw, ImageFont # Load .env file load_dotenv() # Load entire Hugging Face dataset dataset = load_dataset("tejasashinde/birthday_quotes_1_to_100") # Convert HF dataset β†’ Pandas DataFrame df = dataset["train"].to_pandas() # ---------------------- DATA HELPERS ------------------------- def get_age_group(age): """ Determine the age group category based on the given age. Args: age (int): The age of the person. Returns: str: The age group as a string. One of: "Toddler", "Child", "Teenager", "Adult", "Senior". """ if 1 <= age <= 3: return "Toddler" elif 4 <= age <= 12: return "Child" elif 13 <= age <= 19: return "Teenager" elif 20 <= age <= 60: return "Adult" else: return "Senior" def get_base_wish(age, tone, theme, recipient): """ Retrieve a base birthday wish text based on age, tone, theme, and recipient type. Steps: 1. Filter the dataset by exact age if available. 2. If no exact age match, use the age group (Toddler, Child, Teenager, Adult, Senior). 3. Further filter by tone, theme, and recipient type (case-insensitive). 4. If no matches are found, return a default generic birthday wish. Args: age (int): Age of the recipient. tone (str): Desired tone of the wish (e.g., Cheerful, Heartfelt). theme (str): Theme of the greeting (e.g., Nature, Celebration). recipient (str): Type of recipient (e.g., Friend, Parent). Returns: str: Selected birthday wish text. """ filtered = df[df["id"].astype(int) == age] if filtered.empty: age_group = get_age_group(age) filtered = df[df["age_group"] == age_group] filtered2 = filtered[ (filtered["tone"].str.lower() == tone.lower()) & (filtered["theme"].str.lower() == theme.lower()) & (filtered["recipient_type"].str.lower() == recipient.lower()) ] if not filtered2.empty: filtered = filtered2 if filtered.empty: return "πŸŽ‰ Happy Birthday! πŸŽ‚" return filtered.iloc[0]["wish_text"] def personalize_wish(base_wish, name, age, recipient, tone, theme, hobbies="", pop_culture=""): """ Personalize a base birthday wish with recipient details and optionally generate a themed background image. Args: base_wish (str): The base birthday wish text. name (str): Name of the recipient. age (int): Age of the recipient. recipient (str): Recipient type (e.g., Friend, Parent). tone (str): Tone of the greeting (e.g., Cheerful, Heartfelt). theme (str): Theme of the greeting card (e.g., Nature, Humor). hobbies (str, optional): Recipient hobbies or interests. Default is "". pop_culture (str, optional): Pop culture references to include. Default is "". Returns: tuple: personalized_wish (str): The personalized wish text. base64_image (str or None): Base64 string of generated background image. """ details = [] if hobbies.strip(): details.append(f"elements inspired by hobbies: [{hobbies}],") if pop_culture.strip(): details.append(f"subtle references to pop culture: {pop_culture}") details_text = ", ".join(details) if details_text: image_prompt = ( f"Minimal beautiful wallpaper background for a greeting card with copy space in middle " f"using {theme} theme and {tone} tone, {details_text}" ) else: image_prompt = ( f"Minimal beautiful wallpaper background for a greeting card with copy space in middle " f"using {theme} theme and {tone} tone" ) client = OpenAI( base_url="https://api.tokenfactory.nebius.com/v1/", api_key= os.getenv("NEBIUS_API_KEY") ) try: # Request a background image from the Flux-Dev model (returns base64-encoded JPG) response = client.images.generate( model="black-forest-labs/flux-dev", response_format="b64_json", extra_body={ "response_extension": "jpg", "width": 768, "height": 1024, "num_inference_steps": 4, "negative_prompt": "text, logo, watermark, handwriting, typography, words, letters, labels, captions, subtitles, signs, graffiti", "seed": -1, "loras": None }, prompt=image_prompt ) # Extract the base64_image wish from the response base64_image = response.data[0].b64_json.replace("\n", "").strip() messages = [ { "role": "system", "content": "You are a helpful assistant that generates warm and fun birthday wishes." }, { "role": "user", "content": ( f"Write a single, personalized birthday greeting message for {name}, who is becoming {age} years old. " f"Who is a {recipient}, with {tone} tone and {theme} theme. " f"{'Include hobbies: ' + hobbies + '.' if hobbies else ''} " f"{'Include pop culture references: ' + pop_culture + '.' if pop_culture else ''}. " f"Make it sound natural, warm, and specific to the details. Keep it under 120 words and do not add any extra commentary or formatting." ) } ] response = client.chat.completions.create( model="openai/gpt-oss-120b", response_format={"type": "json_object"}, messages=messages ) # Extract the personalized wish from the response raw_json = response.choices[0].message.content data = json.loads(raw_json) personalized_wish = data.get("message", "") except Exception as e: print(f"Nebius Error ({type(e).__name__}): {e}") raise gr.Error(f"❗Nebius Error ({type(e).__name__}): {e}. Please try again.") return personalized_wish, base64_image def generate_birthday_card(name, age, recipient, tone, theme, hobbies, pop_culture): """ Generate a personalized birthday card with a custom text message and a base64-encoded image. Args: name : str The name of the person for whom the birthday card is being generated. age : int The age of the recipient. recipient : str How the person is related to you tone : str The emotional tone or style of the birthday message theme : str The overall theme or aesthetic for the message hobbies : str A list or comma-separated string of the recipient’s hobbies pop_culture : str Pop-culture preferences or favorites Returns: tuple(str, str) A tuple containing: - The generated personalized message (str) - A base64-encoded image (str) """ if not name or name.strip() == "": raise gr.Error("❗ Please enter name of recipient before generating the card.") base = get_base_wish(age, tone, theme, recipient) text, img64 = personalize_wish(base, name, age, recipient, tone, theme, hobbies, pop_culture) return base, text, img64 def show_completion_message(): return gr.Success("Birthday card created successfully!") # -------------------- FABRIC.JS + CANVAS ---------------------- fabric_js = """ async function createFabricCanvas() { // Load Fabric.js if (typeof fabric === 'undefined') { await new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } /* ------------- MAIN LAYOUT ----------------- */ const container = document.createElement("div"); container.style.display = "flex"; container.style.alignItems = "flex-start"; // center canvas wrapper const canvasWrap = document.createElement("div"); canvasWrap.style.flexGrow = "0"; // prevent it from stretching canvasWrap.style.display = "flex"; canvasWrap.style.justifyContent = "center"; canvasWrap.style.alignItems = "center"; const canvasElem = document.createElement("canvas"); canvasElem.id = "fabric-canvas"; canvasElem.width = 538; canvasElem.height = 710; canvasElem.style.border = "2px solid black"; canvasWrap.appendChild(canvasElem); // right panel const rightPanel = document.createElement("div"); rightPanel.style.width = "250px"; rightPanel.style.display = "flex"; rightPanel.style.flexDirection = "column"; rightPanel.style.gap = "7px"; rightPanel.style.padding = "10px"; rightPanel.style.border = "1px solid #ccc"; container.append(canvasWrap, rightPanel); document.getElementById("fabric-canvas-container").appendChild(container); /* --------------- FABRIC CANVAS ------------------ */ const canvas = new fabric.Canvas("fabric-canvas", { backgroundColor:"#ffffff", preserveObjectStacking: true }); window.fabricCanvas = canvas; /* Undo / Redo */ let undoStack = []; let redoStack = []; let restoring = false; function saveState(){ if(restoring) return; undoStack.push(JSON.stringify(canvas.toJSON(["selectable","hasControls","shadow"]))); redoStack.length = 0; } function restoreState(json){ restoring = true; canvas.loadFromJSON(json, ()=>{ canvas.renderAll(); restoring=false; }); } canvas.on("object:added", saveState); canvas.on("object:modified", saveState); canvas.on("object:removed", saveState); saveState(); /* -------------- RIGHT PANEL EXISTING CONTROLS ---------------- */ function labeled(label, el){ const wrap=document.createElement("div"); const l=document.createElement("label"); l.innerText=label; wrap.style.display="flex"; wrap.style.flexDirection="column"; wrap.append(l, el); return wrap; } const fontSize = document.createElement("input"); fontSize.type="number"; fontSize.value=40; fontSize.style.color="#000" const fontFamily = document.createElement("select"); ["Arial","Helvetica","Times New Roman","Georgia","Verdana","Impact"] .forEach(f=>{ const o=document.createElement("option"); o.value=f; o.innerText=f; fontFamily.appendChild(o); }); fontFamily.style.color="#000" const alignSel = document.createElement("select"); ["left","center","right","justify"].forEach(a=>{ const o=document.createElement("option"); o.value=a; o.innerText=a; alignSel.appendChild(o); }); alignSel.style.color="#000" const bold = document.createElement("button"); bold.innerText="B"; bold.style.fontWeight="bold"; const italic = document.createElement("button"); italic.innerText="I"; italic.style.fontStyle="italic"; const underline = document.createElement("button"); underline.innerText="U"; underline.style.textDecoration="underline"; const caseBtn = document.createElement("button"); caseBtn.innerText="aA"; const shadowBtn=document.createElement("button"); shadowBtn.innerText="Text Shadow: OFF"; shadowBtn.dataset.active="0"; const styleRow=document.createElement("div"); styleRow.style.display="flex"; styleRow.style.gap="5px"; styleRow.append(bold, italic, underline, caseBtn, shadowBtn); const fillColor=document.createElement("input"); fillColor.type="color"; fillColor.value="#000000"; const strokeColor=document.createElement("input"); strokeColor.type="color"; strokeColor.value="#000000"; const strokeWidth=document.createElement("input"); strokeWidth.type="number"; strokeWidth.value=1; strokeWidth.min=0.1; strokeWidth.step=0.1; strokeWidth.style.color="#000" const colorRow=document.createElement("div"); colorRow.style.display="flex"; colorRow.style.gap="10px"; colorRow.append(labeled("Fill", fillColor), labeled("Stroke", strokeColor)); const letterSpacing=document.createElement("input"); letterSpacing.type="number"; letterSpacing.value=0; letterSpacing.min=0; letterSpacing.step=0.1; letterSpacing.style.color="#000" const lineHeight=document.createElement("input"); lineHeight.type="number"; lineHeight.step="0.1"; lineHeight.value=1.2; lineHeight.min=0.1; lineHeight.style.color="#000" rightPanel.append( labeled("Font Size", fontSize), labeled("Font Family", fontFamily), labeled("Alignment", alignSel), styleRow, colorRow, labeled("Stroke Width", strokeWidth), labeled("Letter Spacing", letterSpacing), labeled("Line Height", lineHeight) ); /* -------------- TOOLBAR ------------------ */ const toolbarRow = document.createElement("div"); toolbarRow.style.display = "flex"; toolbarRow.style.gap = "5px"; toolbarRow.style.flexWrap = "wrap"; toolbarRow.style.marginTop = "10px"; // spacing from controls toolbarRow.style.borderTop = "1px solid #ccc"; // optional subtle separator toolbarRow.style.paddingTop = "5px"; function addIconButtonToToolbar(icon, tooltip, fn){ const btn = document.createElement("button"); btn.innerText = icon; btn.title = tooltip; btn.style.width="40px"; btn.style.height="40px"; btn.style.fontSize="18px"; btn.style.cursor="pointer"; btn.addEventListener("click", fn); toolbarRow.appendChild(btn); return btn; } // Add all buttons addIconButtonToToolbar("T","Add Text",()=>{ const t = new fabric.Textbox("New Text",{ left:100, top:100, fontSize:40, fill:"#000" }); canvas.add(t); canvas.setActiveObject(t); }); addIconButtonToToolbar("β–¬","Rectangle",()=>{ const r = new fabric.Rect({ left:150, top:150, width:200, height:100, fill:"red" }); canvas.add(r); canvas.setActiveObject(r); }); addIconButtonToToolbar("⬀","Circle",()=>{ const c = new fabric.Circle({ left:150, top:150, radius:60, fill:"blue" }); canvas.add(c); canvas.setActiveObject(c); }); /* addIconButtonToToolbar("β–²","Triangle",()=>{ const t = new fabric.Triangle({ left:150, top:150, width:120, height:120, fill:"green" }); canvas.add(t); canvas.setActiveObject(t); }); */ const uploadInput = document.createElement("input"); uploadInput.type="file"; uploadInput.accept="image/*"; uploadInput.style.display="none"; uploadInput.addEventListener("change",(e)=>{ const file=e.target.files[0]; if(!file) return; fabric.Image.fromURL(URL.createObjectURL(file),(img)=>{ img.set({ left:100, top:100, scaleX:0.5, scaleY:0.5 }); canvas.add(img); canvas.setActiveObject(img); }); }); document.body.appendChild(uploadInput); addIconButtonToToolbar("πŸ–ΌοΈ","Upload Image",()=> uploadInput.click()); function clearCanvasLogic() { canvas.clear(); canvas.setBackgroundColor("#ffffff"); saveState(); canvas.renderAll(); } addIconButtonToToolbar("🧹", "Clear Canvas", () => { if (confirm("Clear entire canvas?")) { clearCanvasLogic(); } }); window.clearFabricCanvas = clearCanvasLogic; addIconButtonToToolbar("β†Ί","Undo",()=>{ if(undoStack.length>1){ redoStack.push(undoStack.pop()); restoreState(undoStack[undoStack.length-1]); } }); addIconButtonToToolbar("↻","Redo",()=>{ if(redoStack.length>0){ const state=redoStack.pop(); undoStack.push(state); restoreState(state); } }); addIconButtonToToolbar("⬆️", "Bring Selected Object Forward", () => { const obj = canvas.getActiveObject(); if (!obj) return; canvas.bringForward(obj); canvas.requestRenderAll(); saveState(); }); addIconButtonToToolbar("⬇️", "Send Selected Object Backward", () => { const obj = canvas.getActiveObject(); if (!obj) return; canvas.sendBackwards(obj); canvas.requestRenderAll(); saveState(); }); /* addIconButtonToToolbar("πŸ“₯","Download",()=>{ const dataURL = canvas.toDataURL({ format:"png" }); const link=document.createElement("a"); link.href=dataURL; link.download="canvas.png"; link.click(); }); */ // Customize the pencil brush canvas.freeDrawingBrush.color = "#ff0000"; // stroke color canvas.freeDrawingBrush.width = 5; // stroke width canvas.freeDrawingBrush.decimate = 0.4; // simplify path (smoother) canvas.freeDrawingBrush.drawStraightLine = false; // straight lines only with modifier canvas.freeDrawingBrush.limitedToCanvasSize = true; // stay inside canvas bounds addIconButtonToToolbar("✏️", "Pencil Brush", () => { canvas.isDrawingMode = !canvas.isDrawingMode; if(canvas.isDrawingMode){ canvas.freeDrawingBrush = new fabric.PencilBrush(canvas); canvas.freeDrawingBrush.color = fillColor.value; // use existing fill color picker canvas.freeDrawingBrush.width = parseFloat(strokeWidth.value) || 5; } }); addIconButtonToToolbar("πŸ”„", "Flip Horizontal", ()=>{ const obj = canvas.getActiveObject(); if(obj) obj.toggle("flipX"); canvas.requestRenderAll(); saveState(); }); addIconButtonToToolbar("πŸ”ƒ", "Flip Vertical", ()=>{ const obj = canvas.getActiveObject(); if(obj) obj.toggle("flipX"); canvas.requestRenderAll(); saveState(); }); // Append the toolbar row to right panel rightPanel.appendChild(toolbarRow); // Create Download button separately const downloadBtn = document.createElement("button"); downloadBtn.innerText = "πŸ“₯ Download"; downloadBtn.title = "Click to download greeting card"; downloadBtn.style.cursor = "pointer"; downloadBtn.style.background = "#4f46e5"; downloadBtn.style.borderRadius = "15px"; downloadBtn.addEventListener("click", () => { const dataURL = canvas.toDataURL({ format: "png" }); const link = document.createElement("a"); link.href = dataURL; link.download = "canvas.png"; link.click(); }); // Append below the main toolbar rightPanel.appendChild(downloadBtn); /* --- Handlers for dynamic property updates --- */ fontSize.addEventListener("input", ()=>{ let val = parseFloat(fontSize.value); if(val < 0.1) val = 0.1; getSelectedObjects().forEach(o => o.set("fontSize", val)); fontSize.value = val; canvas.requestRenderAll(); }); lineHeight.addEventListener("input", ()=>{ let val = parseFloat(lineHeight.value); if(val < 0.1) val = 0.1; getSelectedObjects().forEach(o => o.type==="textbox" && o.set("lineHeight", val)); lineHeight.value = val; canvas.requestRenderAll(); }); strokeWidth.addEventListener("input", ()=>{ let val = parseFloat(strokeWidth.value); if(val < 0.1) val = 0.1; getSelectedObjects().forEach(o => o.set("strokeWidth", val)); strokeWidth.value = val; canvas.requestRenderAll(); }); letterSpacing.addEventListener("input", ()=>{ let val = parseFloat(letterSpacing.value); if(val < 0) val = 0; getSelectedObjects().forEach(o => o.type==="textbox" && o.set("charSpacing", val)); letterSpacing.value = val; canvas.requestRenderAll(); }); /* --------------- PROPERTY SYNCING ------------------ */ function current() { return canvas.getActiveObject(); } function getSelectedObjects() { const active = current(); if (!active) return []; return active.type === "activeSelection" ? active.getObjects() : [active]; } function updateUI(e) { const o = e.selected[0]; if (!o || o.type !== "textbox") return; fontSize.value = o.fontSize; fontFamily.value = o.fontFamily; fillColor.value = o.fill || "#000000"; strokeColor.value = o.stroke || "#000000"; strokeWidth.value = o.strokeWidth; alignSel.value = o.textAlign; letterSpacing.value = o.charSpacing || 0; lineHeight.value = o.lineHeight || 1.2; bold.dataset.active = o.fontWeight === "bold" ? "1" : "0"; italic.dataset.active = o.fontStyle === "italic" ? "1" : "0"; underline.dataset.active = o.underline ? "1" : "0"; shadowBtn.dataset.active = o.shadow ? "1" : "0"; shadowBtn.innerText = o.shadow ? "Shadow: ON" : "Shadow: OFF"; } /* --- Handlers for dynamic property updates --- */ fontSize.addEventListener("input", ()=>{ getSelectedObjects().forEach(o => o.set("fontSize", parseInt(fontSize.value))); canvas.requestRenderAll(); }); fontFamily.addEventListener("change", ()=>{ getSelectedObjects().forEach(o => o.set("fontFamily", fontFamily.value)); canvas.requestRenderAll(); }); alignSel.addEventListener("change", ()=>{ getSelectedObjects().forEach(o => o.set("textAlign", alignSel.value)); canvas.requestRenderAll(); }); fillColor.addEventListener("input", ()=>{ if (canvas.isDrawingMode) canvas.freeDrawingBrush.color = fillColor.value; getSelectedObjects().forEach(o => o.set("fill", fillColor.value)); canvas.requestRenderAll(); }); strokeColor.addEventListener("input", ()=>{ if (canvas.isDrawingMode) canvas.freeDrawingBrush.width = parseFloat(strokeWidth.value); getSelectedObjects().forEach(o => o.set("stroke", strokeColor.value)); canvas.requestRenderAll(); }); strokeWidth.addEventListener("input", ()=>{ getSelectedObjects().forEach(o => o.set("strokeWidth", parseInt(strokeWidth.value))); canvas.requestRenderAll(); }); letterSpacing.addEventListener("input", ()=>{ getSelectedObjects().forEach(o => o.type==="textbox" && o.set("charSpacing", parseInt(letterSpacing.value))); canvas.requestRenderAll(); }); lineHeight.addEventListener("input", ()=>{ getSelectedObjects().forEach(o => o.type==="textbox" && o.set("lineHeight", parseFloat(lineHeight.value))); canvas.requestRenderAll(); }); bold.addEventListener("click", ()=>{ getSelectedObjects().forEach(o => { if(o.type==="textbox") o.set("fontWeight", o.fontWeight==="bold"?"normal":"bold"); }); canvas.requestRenderAll(); }); italic.addEventListener("click", ()=>{ getSelectedObjects().forEach(o => { if(o.type==="textbox") o.set("fontStyle", o.fontStyle==="italic"?"normal":"italic"); }); canvas.requestRenderAll(); }); underline.addEventListener("click", ()=>{ getSelectedObjects().forEach(o => { if(o.type==="textbox") o.set("underline", !o.underline); }); canvas.requestRenderAll(); }); caseBtn.addEventListener("click", ()=>{ getSelectedObjects().forEach(o => { if(o.type==="textbox"){ o.set("text", /[a-z]/.test(o.text) ? o.text.toUpperCase() : o.text.toLowerCase()); } }); canvas.requestRenderAll(); }); shadowBtn.addEventListener("click", ()=>{ getSelectedObjects().forEach(o => { if(o.type==="textbox"){ if(!o.shadow){ o.set("shadow", new fabric.Shadow({color:"#000", blur:5, offsetX:2, offsetY:2})); } else { o.set("shadow", null); } } }); canvas.requestRenderAll(); }); /* Sync UI whenever selection changes */ canvas.on("selection:created", updateUI); canvas.on("selection:updated", updateUI); canvas.on("selection:cleared", ()=>{ /* optionally reset controls */ }); /* Delete Key */ document.addEventListener("keydown",(e)=>{ if(e.key==="Delete"){ const o=current(); if(o){ canvas.remove(o); canvas.renderAll(); saveState(); } } }); document.addEventListener("keydown", (e) => { if(e.ctrlKey && e.key === "c") { const active = current(); if (active) { active.clone((cloned) => { clipboard = cloned; }); } e.preventDefault(); } if(e.ctrlKey && e.key === "v") { if (clipboard) { clipboard.clone((cloned) => { canvas.discardActiveObject(); cloned.set({ left: cloned.left + 10, top: cloned.top + 10 }); // offset pasted canvas.add(cloned); canvas.setActiveObject(cloned); canvas.requestRenderAll(); saveState(); }); } e.preventDefault(); } }); /* Add base64 Images */ window.addBase64ImageWithText = function(base64, base_wish, name) { // clear canvas first if (window.clearFabricCanvas) { window.clearFabricCanvas(); } if (!window.fabricCanvas || !base64) return; let url = base64.startsWith("data:") ? base64 : ("data:image/jpeg;base64," + base64); fabric.Image.fromURL(url, function(img){ img.set({ left: 0, top: 0, scaleX: 0.7, scaleY: 0.7, selectable: true, hasControls: true }); window.fabricCanvas.add(img); window.fabricCanvas.sendToBack(img); window.fabricCanvas.renderAll(); }); addTextToCanvas(base_wish,'wish') addTextToCanvas(name,'name') }; /* Add text */ window.addTextToCanvas = function(text, field) { if (!window.fabricCanvas) return; let left = 50; let top = 50; switch (field) { case "name": left = 100; top = 100; break; case "wish": left = 100; top = 300; break; default: left = 50; top = 200 + (window.fabricCanvas._objects.length * 40); } const tb = new fabric.Textbox(text, { left, top, fontSize: 24, fontFamily: "Arial", fill: "#000", width: 300 }); window.fabricCanvas.add(tb); window.fabricCanvas.renderAll(); }; } """ def compose_birthday_card(image_b64, base_wish, name): """ Convert base64 β†’ PIL Image β†’ add text β†’ return final JPEG as base64. Works in MCP because no JS/browser is required. """ # 1. Decode base64 input image if ',' in image_b64: image_b64 = image_b64.split(",")[1] img_bytes = base64.b64decode(image_b64) img = Image.open(BytesIO(img_bytes)).convert("RGB") draw = ImageDraw.Draw(img) # 2. Load font (fallback to default if custom not available) try: font_large = ImageFont.truetype("arial.ttf", 28) font_small = ImageFont.truetype("arial.ttf", 20) except: font_large = ImageFont.load_default() font_small = ImageFont.load_default() # Wrap text to fit within max_width def wrap_text(text, font, max_width): lines = [] for paragraph in text.split("\n"): words = paragraph.split() if not words: lines.append("") continue line = words[0] for word in words[1:]: test_line = f"{line} {word}" # Use draw.textbbox to get width bbox = draw.textbbox((0, 0), test_line, font=font) width = bbox[2] - bbox[0] if width <= max_width: line = test_line else: lines.append(line) line = word lines.append(line) return lines # 4. Draw name text if name: name_lines = wrap_text(f"Dear {name}", font_large, 500) y_text = 300 line_spacing = 5 for line in name_lines: draw.text((50, y_text), line, fill="black", font=font_large) bbox = draw.textbbox((0, 0), line, font=font_large) line_height = bbox[3] - bbox[1] y_text += line_height + line_spacing # 5. Draw base_wish text if base_wish: wish_lines = wrap_text(base_wish, font_small, 500) y_text = 600 line_spacing = 5 for line in wish_lines: draw.text((50, y_text), line, fill="black", font=font_small) bbox = draw.textbbox((0, 0), line, font=font_small) line_height = bbox[3] - bbox[1] y_text += line_height + line_spacing # Convert back to base64 JPEG buffer = BytesIO() img.save(buffer, format="JPEG", quality=95) final_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8") return final_b64 def generate_card(name, age, recipient, tone, theme, hobbies, pop, request: gr.Request = None): base_wish, personalized_wish, image_b64 = generate_birthday_card( name, age, recipient, tone, theme, hobbies, pop ) # Detect caller type is_browser = False is_mcp = False if request is not None: user_agent_str = request.headers.get("user-agent", "") ua_string = request.headers.get("user-agent", "") ua = parse(ua_string) if ua.browser.family != "Other": return base_wish, personalized_wish, image_b64 else: final_image_b64 = compose_birthday_card(image_b64, base_wish, name) # Convert base64 to bytes image_data = base64.b64decode(final_image_b64) image = Image.open(BytesIO(image_data)) # Save to a temp file with unique name out_dir = Path("generated_cards") out_dir.mkdir(exist_ok=True) file_name = out_dir / f"{uuid.uuid4()}.jpeg" image.save(file_name, "JPEG") # Return wishes + file path return base_wish, personalized_wish, str(file_name) # --------------------- UI --------------------- with gr.Blocks(theme=gr.themes.Soft(), js=fabric_js, css=""" .gradio-container { display: flex !important; flex-direction: column-reverse !important; } #canvas-group { display: none; } """ ) as demo: gr.Markdown("## πŸ₯³πŸŽ‚ Birthday Greeting Card Generator πŸŽˆπŸŽ‰") gr.Markdown("> _Create beautifully personalized cards for ages **1** πŸ‘Ά to **100** πŸ‘΄._") gr.HTML('

This space uses tejasashinde/birthday_quotes_1_to_100 dataset for generating wish quote and Flux [Dev] to generate background image of greeting card via Nebius API

') # INPUTS with gr.Row(): with gr.Column(scale=1): with gr.Row(): name = gr.Textbox(label="Recepient Name (required)", placeholder="Enter name here...") with gr.Row(): age_input = gr.Slider(1, 100, step=1, label="Recipient Age") with gr.Row(): recipient_input = gr.Dropdown(["Parent", "Grandparent", "Friend", "Child", "Sibling", "Classmate", "Mentor", "Spouse", "Colleague"], label="Recipient Type:") tone_input = gr.Dropdown(["Heartfelt", "Cheerful", "Funny", "Inspirational", "Magical", "Trendy", "Poetic", "Witty", "Classy"], label="Greeting Tone:") theme_input = gr.Dropdown(["Playtime","Stardom","Anime","Nature","Art","Humor","Celebration","Treasure","Nostalgia"], label="Greeting Theme:") with gr.Row(): hobbies_input = gr.Textbox(label="Hobbies/ Interests (Optional):", placeholder="Singing...") with gr.Row(): pop_culture_input = gr.Textbox(label="Pop Culture Reference (Optional):", placeholder="Bon Jovi...") generate_btn = gr.Button("Create it for me πŸš€") examples = gr.Examples( examples=[ [ "Alice", 25, "Friend", "Cheerful", "Celebration", "Painting, singing", "Maroon 5" ], [ "Grandpa Joe", 72, "Grandparent", "Heartfelt", "Nostalgia", "Gardening, chess", "The Beatles" ] ], inputs=[ name, age_input, recipient_input, tone_input, theme_input, hobbies_input, pop_culture_input ], label="πŸ§ͺ Try an Example" ) gr.Markdown(" πŸ€– Created for **MCP-1st-Birthday Hackathon** 2025 (Winter) πŸ€—") gr.HTML('Made with ❀️ by tejasashinde') # CANVAS with gr.Column(scale=2): with gr.Row(): base_wish = gr.Textbox(label="Base Wish", visible=False) personalized_wish = gr.TextArea(label="Personalized Wish", lines="2") with gr.Group(elem_id="canvas-group"): with gr.Row(): gr.HTML("""

Generated Card

Use the tools on the right panel to interact
""") generated_image_b64 = gr.Textbox(label="Generated Image Base64", visible=False) generate_btn.click( fn=generate_card, inputs=[name, age_input, recipient_input, tone_input, theme_input, hobbies_input, pop_culture_input], outputs=[base_wish, personalized_wish, generated_image_b64] ).then( fn=None, inputs=[generated_image_b64, base_wish, name], outputs=[], js="(img,base_wish,name)=>{ document.getElementById('canvas-group').style.display = 'block'; addBase64ImageWithText(img,base_wish,name); }" ).then( fn=show_completion_message ) demo.load(js="createFabricCanvas()") if __name__ == "__main__": demo.launch(mcp_server=True, share=True)