rickveloper's picture
Update app.py
0c9c5cf verified
import os, hashlib, textwrap, requests
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
import gradio as gr
# ==============================
# Config / Secrets
# ==============================
HF_TOKEN = os.getenv("HF_TOKEN") # optional
# Try these Inference API model IDs first (will skip on 404/403/5xx)
INFERENCE_CANDIDATES = [
"stabilityai/stable-diffusion-2-1",
"runwayml/stable-diffusion-v1-5",
]
# Public Space fallback (no token). We'll DISCOVER a valid api_name at runtime.
PUBLIC_SPACE_ID = "black-forest-labs/FLUX.1-schnell"
# ==============================
# Fonts
# ==============================
CANDIDATE_FONTS = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
def get_font(size: int):
for p in CANDIDATE_FONTS:
if os.path.exists(p):
return ImageFont.truetype(p, size=int(size))
return ImageFont.load_default()
# ==============================
# Utils
# ==============================
def i(v):
try: return int(round(float(v)))
except Exception: return int(v)
def gradient_from_prompt(prompt: str, w=768, h=768) -> Image.Image:
w, h = i(w), i(h)
hsh = hashlib.sha256((prompt or "meme").encode()).hexdigest()
c1 = tuple(int(hsh[i:i+2], 16) for i in (0, 2, 4))
c2 = tuple(int(hsh[i:i+2], 16) for i in (6, 8, 10))
c1 = tuple(min(255, int(v*1.2)) for v in c1)
c2 = tuple(min(255, int(v*1.1)) for v in c2)
img = Image.new("RGB", (w, h), c1)
px = img.load()
for y in range(h):
t = y / (h - 1)
r = int(c1[0]*(1-t) + c2[0]*t)
g = int(c1[1]*(1-t) + c2[1]*t)
b = int(c1[2]*(1-t) + c2[2]*t)
for x in range(w):
px[x, y] = (r, g, b)
return img
def wrap_lines(draw, text, img_w, font, stroke):
lines = []
max_chars = max(12, min(30, img_w // 30))
for paragraph in (text or "").split("\n"):
wrapped = textwrap.wrap(paragraph, width=max_chars)
lines.extend(wrapped if wrapped else [""])
heights = [draw.textbbox((0, 0), ln, font=font, stroke_width=stroke)[3] for ln in lines]
return lines, heights
def draw_block(draw, text, img_w, y, font, fill, stroke_fill, stroke_width, align="center"):
lines, heights = wrap_lines(draw, text, img_w, font, stroke_width)
curr_y = y
for i, line in enumerate(lines):
bbox = draw.textbbox((0, 0), line, font=font, stroke_width=stroke_width)
w_line = bbox[2] - bbox[0]
if align == "center":
x = (img_w - w_line) / 2
elif align == "left":
x = int(img_w * 0.05)
else:
x = img_w - int(img_w * 0.05) - w_line
draw.text((x, curr_y), line, font=font, fill=fill,
stroke_width=stroke_width, stroke_fill=stroke_fill)
curr_y += heights[i] + int(font.size * 0.25)
return curr_y, sum(heights) + (len(heights)-1) * int(font.size*0.25)
# ==============================
# Styles / text split
# ==============================
PRESETS = {
"None": "",
"Retro Comic": "bold comic outline, grain, high contrast, 35mm scan",
"Vaporwave": "vaporwave, neon pink and cyan, miami sunset, synth grid",
"Game Boy": "pixel art, 4-color green palette, dithering",
"Newspaper Halftone": "b&w halftone dots, newsprint texture",
"Cyberpunk Neon": "neon city at night, purple blue rim light, rain",
"90s Web": "bevel buttons, gradients, clipart stars, lens flare",
"Synthwave Grid": "purple/indigo sky, glowing sun, mountains, grid floor",
}
def smart_split_text(prompt: str):
p = (prompt or "").strip()
if not p: return "TOP TEXT", "BOTTOM TEXT"
for sep in ["|", " - ", " — ", ":", ";"]:
if sep in p:
a, b = p.split(sep, 1)
return a.strip().upper(), b.strip().upper()
words = p.split()
if len(words) > 6:
mid = len(words) // 2
return " ".join(words[:mid]).upper(), " ".join(words[mid:]).upper()
return p.upper(), ""
# ==============================
# Generators (multi-fallback)
# ==============================
def call_inference_api(model_id: str, prompt: str, width: int, height: int) -> Image.Image:
if not HF_TOKEN:
raise RuntimeError("no-token")
url = f"https://api-inference.huggingface.co/models/{model_id}"
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
payload = {"inputs": prompt, "options": {"wait_for_model": True},
"parameters": {"width": int(width), "height": int(height)}}
r = requests.post(url, headers=headers, json=payload, timeout=180)
if r.status_code != 200:
raise RuntimeError(f"{model_id}:{r.status_code}")
return Image.open(BytesIO(r.content)).convert("RGB")
def call_public_space(prompt: str, width: int, height: int) -> Image.Image:
"""Use the FLUX public Space directly via its /infer endpoint."""
from gradio_client import Client
client = Client("black-forest-labs/FLUX.1-schnell")
# order: prompt, seed, randomize_seed, width, height, num_inference_steps
result, _seed = client.predict(
prompt,
0, # seed (0 = let Space choose unless randomize_seed=False)
True, # randomize_seed
int(width),
int(height),
4, # num_inference_steps (keep tiny for speed on mobile)
api_name="/infer"
)
# result is a dict with path/url
path = None
if isinstance(result, dict):
path = result.get("path") or result.get("url")
elif isinstance(result, list) and result:
item = result[0]
if isinstance(item, dict):
path = item.get("path") or item.get("url")
else:
path = item
else:
path = result
if not path:
raise RuntimeError("public-space returned empty result")
from PIL import Image
return Image.open(path).convert("RGB")
def generate_image_auto(prompt: str, width: int, height: int):
tried = []
# 1) Inference API candidates (if token present)
if HF_TOKEN:
for mid in INFERENCE_CANDIDATES:
try:
img = call_inference_api(mid, prompt, width, height)
return img, f"✅ Inference API: **{mid}** (token present)"
except Exception as e:
tried.append(f"{mid}{str(e)}")
continue
# 2) Public Space dynamic
try:
img = call_public_space(prompt, width, height)
return img, "✅ Public Space: FLUX /infer"
except Exception as e:
tried.append(f"{PUBLIC_SPACE_ID}{str(e)}")
# 3) Gradient
return gradient_from_prompt(prompt, w=width, h=height), f"⚠️ Fallback gradient | tried: {', '.join(tried)}"
# ==============================
# Core pipeline (returns image + status)
# ==============================
def generate_and_meme(
prompt, preset_name, use_ai, width, height,
font_size, stroke_width, text_color, outline_color,
align, top_nudge, bottom_nudge, use_prompt_for_text, top_text_manual, bottom_text_manual
):
width, height = i(width), i(height)
top_nudge, bottom_nudge = i(top_nudge), i(bottom_nudge)
stroke_width = i(stroke_width)
base = (prompt or "").strip()
style_suffix = PRESETS.get(preset_name or "None", "")
gen_prompt = (base + " " + style_suffix).strip()
if use_ai:
img, status = generate_image_auto(gen_prompt, width, height)
else:
img, status = gradient_from_prompt(gen_prompt, w=width, h=height), "ℹ️ AI generator is OFF"
# Text
if use_prompt_for_text:
top_text, bottom_text = smart_split_text(base)
else:
top_text = (top_text_manual or "").upper()
bottom_text = (bottom_text_manual or "").upper()
img = img.convert("RGB")
draw = ImageDraw.Draw(img)
w_img, h_img = img.size
base_size = max(12, int((w_img * float(font_size)) / 100))
font = get_font(base_size)
stroke = int(max(0, stroke_width))
top_y = int(h_img * 0.03) + top_nudge
draw_block(draw, top_text, w_img, top_y, font, text_color, outline_color, stroke, align=align)
lines, heights = wrap_lines(draw, bottom_text, w_img, font, stroke)
total_bottom_h = sum(heights) + (len(heights)-1) * int(font.size*0.25)
bottom_y_start = int(h_img - total_bottom_h - h_img*0.03) - bottom_nudge
draw_block(draw, bottom_text, w_img, bottom_y_start, font, text_color, outline_color, stroke, align=align)
return img, status
# ==============================
# Retro theme + CSS
# ==============================
THEME = gr.themes.Soft(primary_hue="indigo", secondary_hue="violet", neutral_hue="slate")
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
:root { --radius: 14px; }
* { -webkit-tap-highlight-color: transparent; }
body { background: radial-gradient(1200px 600px at 50% -10%, #0d1220 10%, #05060b 70%); }
.gradio-container { max-width: 900px; margin: 0 auto; padding: 12px; }
h2, p { text-align: center; color: #cde3ff; text-shadow: 0 0 10px rgba(80,120,255,.25); }
h2 { font-family: 'Press Start 2P', system-ui, sans-serif; letter-spacing: 1px; font-size: 18px; }
.crt { position: relative; border: 2px solid #2a3350; border-radius: 12px; overflow: hidden;
box-shadow: 0 0 0 1px #0f1427 inset, 0 0 40px rgba(60,80,255,.25); }
.crt::before { content: ""; position: absolute; inset: 0; pointer-events: none;
background: repeating-linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.05) 1px, transparent 1px, transparent 3px);
mix-blend-mode: overlay; opacity: .25; }
label { color: #a9b7ff !important; }
.gr-button { font-weight: 800; border-radius: 12px; }
"""
# ==============================
# App
# ==============================
with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
gr.Markdown("<h2>🕹️ MEME LAB — RETRO EDITION</h2>"
"<p>One prompt → generate image → auto meme text. Style presets for instant vibes.</p>")
with gr.Row():
with gr.Column(scale=1, elem_classes=["crt"]):
prompt = gr.Textbox(label="Your idea (one prompt)", value="cat typing on a laptop at midnight")
preset = gr.Dropdown(choices=list(PRESETS.keys()), value="Retro Comic", label="Style preset")
use_ai = gr.Checkbox(label="Use AI image (auto-fallbacks, no key required)", value=True)
with gr.Row():
width = gr.Slider(384, 1024, value=768, step=64, label="Width")
height = gr.Slider(384, 1024, value=768, step=64, label="Height")
gr.Markdown("### Meme Text")
use_prompt_for_text = gr.Checkbox(label="Auto from prompt", value=True)
top_text_manual = gr.Textbox(label="Top text (if not auto)", value="", interactive=True)
bottom_text_manual = gr.Textbox(label="Bottom text (if not auto)", value="", interactive=True)
align = gr.Radio(choices=["left", "center", "right"], value="center", label="Text alignment")
font_size = gr.Slider(8, 24, value=10, step=1, label="Font size (% of width)")
stroke_width = gr.Slider(0, 16, value=4, step=1, label="Outline thickness")
with gr.Row():
text_color = gr.ColorPicker(value="#FFFFFF", label="Text color")
outline_color = gr.ColorPicker(value="#000000", label="Outline color")
with gr.Row():
top_nudge = gr.Slider(-300, 300, value=0, step=1, label="Top nudge (px)")
bottom_nudge = gr.Slider(-300, 300, value=0, step=1, label="Bottom nudge (px)")
with gr.Column(scale=1, elem_classes=["crt"]):
out = gr.Image(type="pil", label="Preview / Download", height=540, show_download_button=True)
status = gr.Markdown("…")
generate = gr.Button("✨ Generate Image + Meme", variant="primary")
inputs = [prompt, preset, use_ai, width, height,
font_size, stroke_width, text_color, outline_color,
align, top_nudge, bottom_nudge, use_prompt_for_text, top_text_manual, bottom_text_manual]
generate.click(fn=generate_and_meme, inputs=inputs, outputs=[out, status])
for comp in [preset, use_prompt_for_text, top_text_manual, bottom_text_manual,
font_size, stroke_width, text_color, outline_color, align, top_nudge, bottom_nudge]:
comp.change(fn=generate_and_meme, inputs=inputs, outputs=[out, status], show_progress=False)
if __name__ == "__main__":
demo.launch()