import gradio as gr from PIL import Image, ImageDraw, ImageFont, ImageFilter import numpy as np import os import random import base64 import requests from io import BytesIO # ------------------------- # FREE HF IMG2IMG MODEL # ------------------------- HF_MODEL = "timbrooks/instruct-pix2pix" HF_API_KEY = os.environ.get("HF_API_KEY") def hf_img2img(prompt, image_b64): response = requests.post( f"https://api-inference.huggingface.co/models/{HF_MODEL}", headers={"Authorization": f"Bearer {HF_API_KEY}"}, json={ "inputs": prompt, "image": image_b64, "parameters": {"guidance_scale": 7.5} }, timeout=60 ) result = response.json() if isinstance(result, list) and "image" in result[0]: img_bytes = base64.b64decode(result[0]["image"]) return Image.open(BytesIO(img_bytes)).convert("RGBA") return None # ------------------------- # FILES & CONSTANTS # ------------------------- FRAME_FILES = ["frame_1.png", "frame_2.png", "frame_3.png", "frame_4.png"] SAINT_FILES = ["saint_base_1.png", "saint_base_2.png", "saint_base_3.png", "saint_base_4.png"] FRAME_LABELS = ["Frame 1", "Frame 2", "Frame 3", "Frame 4"] BACKGROUND_MAP = { "Saint of Drama": "bg_drama.png", "Saint of Gay Chaos": "bg_chaos.png", "Saint of Delulu": "bg_delulu.png", "Divine Gold": "bg_gold.png", "Infernal Red": "bg_red.png" } FONT_PATH = "font.ttf" BASE_FRAMES = [Image.open(p).convert("RGBA") for p in FRAME_FILES] BASE_SAINTS = [Image.open(p).convert("RGBA") for p in SAINT_FILES] FRAME_W, FRAME_H = BASE_FRAMES[0].size CANVAS_W, CANVAS_H = FRAME_W, FRAME_H # ------------------------- # CROPPING # ------------------------- def detect_person_bbox_simple(img): w, h = img.size return (int(w * 0.18), int(h * 0.05), int(w * 0.82), int(h * 0.75)) def auto_crop_and_scale(img): bbox = detect_person_bbox_simple(img) cropped = img.crop(bbox) target_h = int(CANVAS_H * 0.62) scale = target_h / cropped.height new_w = int(cropped.width * scale) return cropped.resize((new_w, target_h), Image.LANCZOS) # ------------------------- # BACKGROUND REMOVAL (improved) # ------------------------- def remove_background_simple(img): img = img.convert("RGBA") arr = np.array(img).astype(np.int16) h, w, _ = arr.shape samples = [ arr[5, 5, :3], arr[5, w-6, :3], arr[h-6, 5, :3], arr[h-6, w-6, :3], arr[h//2, 5, :3], arr[h//2, w-6, :3] ] bg = np.mean(samples, axis=0) diff = np.linalg.norm(arr[:, :, :3] - bg, axis=2) mask = diff > 28 alpha = np.where(mask, 255, 0).astype(np.uint8) out = np.dstack((arr[:, :, 0].clip(0, 255).astype(np.uint8), arr[:, :, 1].clip(0, 255).astype(np.uint8), arr[:, :, 2].clip(0, 255).astype(np.uint8), alpha)) return Image.fromarray(out, mode="RGBA") # ------------------------- # BACKGROUND # ------------------------- def load_background(vibe, color, use_vibe=True): key = vibe if vibe in BACKGROUND_MAP else color f = BACKGROUND_MAP.get(key) if use_vibe and f and os.path.exists(f): return Image.open(f).convert("RGBA").resize((CANVAS_W, CANVAS_H)) return Image.new("RGBA", (CANVAS_W, CANVAS_H), (210, 185, 160, 255)) # ------------------------- # CARTOONIFY (clean, soft) # ------------------------- def cartoonify(img): img = img.convert("RGB") smooth = img.filter(ImageFilter.SMOOTH_MORE) edges = smooth.filter(ImageFilter.EDGE_ENHANCE) arr = np.array(edges) arr = (arr // 24) * 24 return Image.fromarray(arr).convert("RGBA") # ------------------------- # TEXT # ------------------------- def generate_saint_name(name, sin): clean = f"SAINT {name.upper()}" if name.strip() else "THE UNNAMED" return clean, f"PATRON SAINT OF {sin.upper() or 'SIN'}" def load_font(size): try: return ImageFont.truetype(FONT_PATH, size) except: return ImageFont.load_default() def fit_text(draw, text, max_w, base=90): size = base while size > 24: f = load_font(size) w = draw.textbbox((0, 0), text, font=f)[2] if w <= max_w - 40: return f size -= 4 return load_font(24) # ------------------------- # PREVIEW # ------------------------- def compose_preview(img, name, vibe, color, prop, sin, personality, frame_label, remove_bg, cartoon): frame_idx = FRAME_LABELS.index(frame_label) frame = BASE_FRAMES[frame_idx] bg = load_background(vibe, color, use_vibe=remove_bg) if img: if remove_bg: img_no_bg = remove_background_simple(img) person = auto_crop_and_scale(img_no_bg) else: person = auto_crop_and_scale(img) if cartoon: person = cartoonify(person) px = (CANVAS_W - person.width) // 2 py = int(CANVAS_H * 0.18) bg.paste(person, (px, py), person) else: saint = BASE_SAINTS[frame_idx].copy() bg.paste(saint, (0, 0), saint) bg.paste(frame, (0, 0), frame) saint_name, saint_title = generate_saint_name(name, sin) d = ImageDraw.Draw(bg) f1 = fit_text(d, saint_name, CANVAS_W, 80) f2 = fit_text(d, saint_title, CANVAS_W, 70) d.text((CANVAS_W // 2, int(CANVAS_H * 0.135)), saint_name, fill="white", anchor="mm", font=f1) d.text((CANVAS_W // 2, int(CANVAS_H * 0.865)), saint_title, fill="white", anchor="mm", font=f2) return bg # ------------------------- # AI CANONIZER (FREE, WORKING) # ------------------------- def canonize_ai(preview_img, vibe, sin, personality): if preview_img is None: return None prompt = ( f"Make this person look like a glowing saint candle illustration. " f"Vibe: {vibe}. Sin: {sin}. Personality: {personality}. " f"Golden halo, divine light, ornate, baroque, dramatic shadows." ) buffered = BytesIO() preview_img.save(buffered, format="PNG") img_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8") result = hf_img2img(prompt, img_b64) return result if result else preview_img # ------------------------- # RANDOMIZER # ------------------------- VIBE_OPTIONS = ["Saint of Drama", "Saint of Gay Chaos", "Saint of Delulu"] COLOR_OPTIONS = ["Divine Gold", "Infernal Red", "Celestial Blue", "Pastel Angel", "Neon Drag", "Baroque Sepia"] PROP_OPTIONS = ["Candles & Incense", "Money & Bills", "Phones & Screens", "Crowns & Halos", "Flowers & Thorns"] def randomize_settings(img, name, vibe, color, prop, sin, personality, frame_label, remove_bg_flag, cartoon_flag): new_frame = random.choice(FRAME_LABELS) new_vibe = random.choice(VIBE_OPTIONS) new_color = random.choice(COLOR_OPTIONS) new_prop = random.choice(PROP_OPTIONS) new_preview = compose_preview( img, name, new_vibe, new_color, new_prop, sin, personality, new_frame, remove_bg_flag, cartoon_flag ) return new_frame, new_vibe, new_color, new_prop, new_preview # ------------------------- # UI # ------------------------- with gr.Blocks() as demo: with gr.Column(): gr.Image(value="LOGO.png", interactive=False, show_label=False, width=80) gr.Markdown("## 🔥 Holy Smokes™ Saintifier — AI Edition") with gr.Row(): with gr.Column(scale=3): preview = gr.Image(type="pil", label="Preview") with gr.Column(scale=2): uploader = gr.Image(type="pil", label="📸 Upload Photo") frame_choice = gr.Dropdown(FRAME_LABELS, value="Frame 1", label="🖼 Frame") remove_bg = gr.Checkbox(label="🧼 Remove Background", value=True) cartoon = gr.Checkbox(label="🎨 Cartoonify (soft)", value=False) name = gr.Textbox(label="Name", value="") sin = gr.Textbox(label="Sin", value="") personality = gr.Textbox(label="Personality", value="") vibe = gr.Dropdown(VIBE_OPTIONS, value="Saint of Drama", label="Vibe") color = gr.Dropdown(COLOR_OPTIONS, value="Divine Gold", label="Color") prop = gr.Dropdown(PROP_OPTIONS, value="Candles & Incense", label="Prop") with gr.Row(): random_btn = gr.Button("🎲 Randomize") canonize_btn = gr.Button("✨ Canonize with AI") def update_preview(img, name, vibe, color, prop, sin, personality, frame_label, remove_bg_flag, cartoon_flag): return compose_preview(img, name, vibe, color, prop, sin, personality, frame_label, remove_bg_flag, cartoon_flag) for comp in [uploader, name, vibe, color, prop, sin, personality, frame_choice, remove_bg, cartoon]: comp.change( update_preview, [uploader, name, vibe, color, prop, sin, personality, frame_choice, remove_bg, cartoon], preview ) canonize_btn.click( canonize_ai, [preview, vibe, sin, personality], preview ) random_btn.click( randomize_settings, [uploader, name, vibe, color, prop, sin, personality, frame_choice, remove_bg, cartoon], [frame_choice, vibe, color, prop, preview] ) if __name__ == "__main__": demo.launch()