|
|
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_dotenv() |
|
|
|
|
|
|
|
|
dataset = load_dataset("tejasashinde/birthday_quotes_1_to_100") |
|
|
|
|
|
|
|
|
df = dataset["train"].to_pandas() |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
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 = """ |
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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}" |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
image_data = base64.b64decode(final_image_b64) |
|
|
image = Image.open(BytesIO(image_data)) |
|
|
|
|
|
|
|
|
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 base_wish, personalized_wish, str(file_name) |
|
|
|
|
|
|
|
|
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('<p style="color: white;">This space uses <a href="https://huggingface.co/datasets/tejasashinde/birthday_quotes_1_to_100" target="blank">tejasashinde/birthday_quotes_1_to_100</a> dataset for generating wish quote and Flux [Dev] to generate background image of greeting card via <a href="https://nebius.com" target="blank">Nebius API</a></p>') |
|
|
|
|
|
|
|
|
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 <a href="https://huggingface.co/tejasashinde" target="blank">tejasashinde</a>') |
|
|
|
|
|
|
|
|
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(""" |
|
|
<div style='display: flex; flex-direction: row; justify-content: space-evenly; align-items: flex-start; width: 100%; padding: 10px; box-sizing: border-box;'> |
|
|
<div id='fabric-canvas-container' style='display: flex; flex-direction: column; align-items: start;'> |
|
|
<h3>Generated Card</h3> |
|
|
<small>Use the tools on the right panel to interact</small> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
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) |
|
|
|