import os import gc import random import gradio as gr import numpy as np import torch from PIL import Image, ImageDraw, ImageFont from gradio.themes import Soft from gradio.themes.utils import colors, fonts, sizes import glob import spaces from diffusers import AutoPipelineForImage2Image from saint_oracle import generate_saint_text # ──────────────────────────────────────────────── # THEME # ──────────────────────────────────────────────── colors.fire_red = colors.Color( name="fire_red", c50="#FFF5F0", c100="#FFE8DB", c200="#FFD0B5", c300="#FFB088", c400="#FF8C5A", c500="#FF6B35", c600="#E8531F", c700="#CC4317", c800="#A63812", c900="#80300F", c950="#5C220A", ) class HolySmokesTheme(Soft): def __init__(self, *, primary_hue=colors.fire_red, secondary_hue=colors.fire_red, neutral_hue=colors.slate, **kwargs): super().__init__(primary_hue=primary_hue, secondary_hue=secondary_hue, neutral_hue=neutral_hue, **kwargs) super().set( body_background_fill="#f7f3ea", button_primary_background_fill="linear-gradient(135deg,#b48c2c,#ffd700)", button_primary_background_fill_hover="linear-gradient(135deg,#c89b34,#ffeb3b)", button_primary_shadow="0 6px 20px rgba(180,140,44,0.5)", ) theme = HolySmokesTheme() # ──────────────────────────────────────────────── # MODEL – back to sd-turbo (your original fast & reliable model) # ──────────────────────────────────────────────── device = torch.device("cuda" if torch.cuda.is_available() else "cpu") dtype = torch.float16 if torch.cuda.is_available() else torch.float32 pipe = AutoPipelineForImage2Image.from_pretrained( "stabilityai/sd-turbo", torch_dtype=dtype, ).to(device) MAX_SEED = np.iinfo(np.int32).max # ──────────────────────────────────────────────── # DYNAMIC ASSETS # ──────────────────────────────────────────────── FRAME_FILES = sorted( glob.glob("frame_*.png"), key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x)))) ) FRAME_LABELS = [f"Frame {i+1}" for i in range(len(FRAME_FILES))] BASE_FRAMES = [Image.open(f).convert("RGBA") for f in FRAME_FILES] SAINT_BASE_FILES = sorted( glob.glob("saint_base_*.png"), key=lambda x: int(''.join(filter(str.isdigit, os.path.basename(x)))) ) SAINT_BASES = [Image.open(f).convert("RGBA") for f in SAINT_BASE_FILES] if FRAME_FILES: FRAME_W, FRAME_H = BASE_FRAMES[0].size else: raise RuntimeError("No frame_*.png files found!") # ──────────────────────────────────────────────── # OPTIONS # ──────────────────────────────────────────────── 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"] SINS = ["Weaponized Gossip", "Luxury Without Income", "Texting The Ex", "Chronic Delusion", "Main Character Syndrome", "Gay Chaos at Brunch", "Financial Delusion"] RANDOM_NAMES = ["Santa Delulu", "Gloria del Caos", "Our Lady of Brunch", "Saint Overthinker", "Madre de la Drama", "San Ex-Toxic"] # ──────────────────────────────────────────────── # CARTOON SAINT PROMPT – optimized for clear illustration # ──────────────────────────────────────────────── def generate_prompt(name, sin, personality, vibe, color, prop): return f""" Make this person a ridiculously funny Saint illustration with large glowing golden halo. Exact face preserved, cartoon style, vibrant colors, exaggerated campy baroque details. Robes in {color}, holding {prop}, embodying {vibe} energy and the sin of {sin}. Funny theatrical pose, queer-coded drama, painterly illustration style. Clearly cartoon/illustration, not realistic photo. Preserve real identity perfectly. """ # ──────────────────────────────────────────────── # IMAGE HELPERS – ribbon fixed to your frame # ──────────────────────────────────────────────── def resize_and_center(img: Image.Image) -> Image.Image: if img is None: return None img = img.convert("RGB") target_h = int(FRAME_H * 0.95) target_w = int(target_h * img.width / img.height) resized = img.resize((target_w, target_h), Image.LANCZOS) canvas = Image.new("RGB", (FRAME_W, FRAME_H), (20, 20, 30)) canvas.paste(resized, ((FRAME_W - target_w) // 2, (FRAME_H - target_h) // 2)) return canvas def apply_frame(img: Image.Image, frame_label: str) -> Image.Image: idx = FRAME_LABELS.index(frame_label) frame = BASE_FRAMES[idx] out = img.convert("RGBA").resize((FRAME_W, FRAME_H), Image.LANCZOS) out.paste(frame, (0, 0), frame) return out def draw_name_on_ribbon(img: Image.Image, name: str) -> Image.Image: if not name: return img img = img.convert("RGBA") draw = ImageDraw.Draw(img) w, h = img.size # ALIGNED TO YOUR FRAME'S LOWER RIBBON BANNER x1 = int(w * 0.02) x2 = int(w * 0.98) y1 = int(h * 0.68) y2 = int(h * 0.95) zone_width = x2 - x1 zone_height = y2 - y1 text = f"SAINT {name.upper()}" font_path = "font.ttf" base_font_size = 92 if os.path.isfile(font_path) else 40 font_size = base_font_size font = None while font_size >= 26: try: font = ImageFont.truetype(font_path, font_size) if os.path.isfile(font_path) else ImageFont.load_default() except: font = ImageFont.load_default() bbox = draw.textbbox((0, 0), text, font=font) tw = bbox[2] - bbox[0] th = bbox[3] - bbox[1] if tw <= zone_width * 0.88 and th <= zone_height * 0.78: break font_size -= 3 x = x1 + (zone_width - tw) // 2 y = y1 + (zone_height - th) // 2 for dx, dy in [(-3,-3), (-3,3), (3,-3), (3,3)]: draw.text((x + dx, y + dy), text, fill="black", font=font) draw.text((x, y), text, fill="black", font=font) return img def apply_frame_and_ribbon(base_img: Image.Image, frame_label: str, name: str) -> Image.Image: if base_img is None: return None framed = apply_frame(base_img, frame_label) return draw_name_on_ribbon(framed, name) # ──────────────────────────────────────────────── # ORACLE CARD – dynamic + auto-size + dark text # ──────────────────────────────────────────────── def generate_oracle(name, sin=None, personality=None, vibe=None): name_clean = name.strip() if name else "Unnamed Saint" title = f"Saint {name_clean}" bio = f"Born from glitter, gossip, and deeply questionable decisions, this chaotic nun wanders the mortal realm spreading drama, delusion, and weaponized camp. Their sin is {sin or 'chaos'}." roast = "Their holiness is as real as their lashes: heavy, synthetic, and absolutely non-refundable." prayer = f"May your sins be glamorous, your chaos be blessed, and your enemies stay forever pressed. {vibe or ''}" return f"""
Upload your chaos or summon a random saint. Either way, you're getting canonized.
📤 Upload Photo + Chaos References
') images = gr.Gallery( label="Upload main photo + optional extras", type="filepath", columns=2, rows=1, height=260, allow_preview=True, object_fit="contain", ) gr.HTML('📝 Who is this saint?
') name = gr.Textbox(label="Name", placeholder="Your name or alter ego") sin = gr.Textbox(label="Sin", placeholder="Weaponized gossip, texting the ex, etc.") personality = gr.Textbox(label="Personality", placeholder="Delulu, dramatic, soft life, etc.") vibe = gr.Dropdown(VIBE_OPTIONS, value="Saint of Drama", label="Vibe") color = gr.Dropdown(COLOR_OPTIONS, value="Divine Gold", label="Palette") prop = gr.Dropdown(PROP_OPTIONS, value="Candles & Incense", label="Prop") frame_choice = gr.Dropdown(FRAME_LABELS, value="Frame 1", label="Frame") with gr.Row(): random_saint_btn = gr.Button("🔥 Random Saint", variant="secondary") random_btn = gr.Button("🎲 Random Chaos (fields only)", variant="secondary") with gr.Row(): canonize_btn = gr.Button("✨ Canonize Me", variant="primary", elem_id="gen-btn") clear_button = gr.Button("🗑️ Clear", variant="secondary") reveal_oracle_btn = gr.Button("🔮 Reveal Oracle", variant="secondary") with gr.Accordion("⚙️ Advanced (FireRed)", open=False): seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0) randomize_seed = gr.Checkbox(label="🎲 Randomize seed", value=True) guidance_scale = gr.Slider( label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0, ) steps = gr.Slider( label="Inference Steps", minimum=1, maximum=30, step=1, value=4, ) with gr.Column(scale=1): gr.HTML('🖼️ Your Saint
') output_image = gr.Image( show_label=False, interactive=False, format="png", width="100%", height=FRAME_H, elem_id="output-img", ) oracle_md = gr.HTML() info_box = gr.Markdown( value="*Generate or summon a saint to see details here.*", elem_id="info-box", ) download_btn = gr.DownloadButton( "📥 Download Saint + Oracle JPG", visible=True, ) # ──────────────────────────────────────────────── # EVENTS # ──────────────────────────────────────────────── frame_choice.change(update_preview_frame, inputs=[frame_choice, current_unframed, name], outputs=output_image) images.change(auto_preview_upload, inputs=[images, frame_choice, name], outputs=output_image) canonize_btn.click( run_canonizer, inputs=[images, name, sin, personality, vibe, color, prop, frame_choice, seed, randomize_seed, guidance_scale, steps, current_unframed], outputs=[output_image, oracle_md, seed, download_btn, current_unframed], ).then(format_info, inputs=[seed, images], outputs=info_box) reveal_oracle_btn.click( generate_oracle, inputs=[name], outputs=[oracle_md], ) random_saint_btn.click(random_saint, outputs=[output_image, oracle_md, name, sin, personality, vibe, color, prop, frame_choice, download_btn, current_unframed]) random_btn.click(randomize_fields, outputs=[sin, vibe, color, prop, frame_choice]) clear_button.click( lambda: (None, "", "", "", "Saint of Drama", "Divine Gold", "Candles & Incense", "Frame 1", None, "*Generate or summon a saint to see details here.*", "", None, None), outputs=[images, name, sin, personality, vibe, color, prop, frame_choice, output_image, info_box, oracle_md, download_btn, current_unframed], ) demo.load(random_saint, outputs=[output_image, oracle_md, name, sin, personality, vibe, color, prop, frame_choice, download_btn, current_unframed]) if __name__ == "__main__": demo.launch(allowed_paths=["."])