Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -10,7 +10,7 @@ CANDIDATE_FONTS = [
|
|
| 10 |
def get_font(size: int):
|
| 11 |
for p in CANDIDATE_FONTS:
|
| 12 |
if os.path.exists(p):
|
| 13 |
-
return ImageFont.truetype(p, size=size)
|
| 14 |
return ImageFont.load_default()
|
| 15 |
|
| 16 |
# ---------------- Style Presets ----------------
|
|
@@ -25,8 +25,13 @@ PRESETS = {
|
|
| 25 |
"Synthwave Grid": "purple/indigo gradient sky, mountain silhouette, glowing sun, grid floor",
|
| 26 |
}
|
| 27 |
|
| 28 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def gradient_from_prompt(prompt: str, w=768, h=768):
|
|
|
|
| 30 |
hsh = hashlib.sha256((prompt or "meme").encode()).hexdigest()
|
| 31 |
c1 = tuple(int(hsh[i:i+2], 16) for i in (0, 2, 4))
|
| 32 |
c2 = tuple(int(hsh[i:i+2], 16) for i in (6, 8, 10))
|
|
@@ -43,7 +48,6 @@ def gradient_from_prompt(prompt: str, w=768, h=768):
|
|
| 43 |
px[x, y] = (r, g, b)
|
| 44 |
return img
|
| 45 |
|
| 46 |
-
# ---------------- Text helpers ----------------
|
| 47 |
def wrap_lines(draw, text, img_w, font, stroke):
|
| 48 |
lines = []
|
| 49 |
max_chars = max(12, min(30, img_w // 30))
|
|
@@ -84,17 +88,16 @@ def smart_split_text(prompt: str):
|
|
| 84 |
return " ".join(words[:mid]).upper(), " ".join(words[mid:]).upper()
|
| 85 |
return p.upper(), ""
|
| 86 |
|
| 87 |
-
#
|
| 88 |
def try_generate_with_flux(prompt: str, width: int, height: int):
|
| 89 |
from gradio_client import Client
|
| 90 |
-
# Using a popular public Space; if its API changes or rate-limits, we'll fallback.
|
| 91 |
client = Client("black-forest-labs/FLUX.1-schnell")
|
|
|
|
| 92 |
try:
|
| 93 |
result = client.predict(prompt, width, height, api_name="/predict")
|
| 94 |
if isinstance(result, list): result = result[0]
|
| 95 |
return Image.open(result)
|
| 96 |
except Exception as e:
|
| 97 |
-
# last-chance alternate endpoint some Spaces expose
|
| 98 |
try:
|
| 99 |
result = client.predict(prompt, api_name="/run")
|
| 100 |
if isinstance(result, list): result = result[0]
|
|
@@ -102,17 +105,21 @@ def try_generate_with_flux(prompt: str, width: int, height: int):
|
|
| 102 |
except Exception:
|
| 103 |
raise e
|
| 104 |
|
| 105 |
-
#
|
| 106 |
def generate_and_meme(
|
| 107 |
prompt, preset_name, use_ai, width, height,
|
| 108 |
font_size, stroke_width, text_color, outline_color,
|
| 109 |
align, top_nudge, bottom_nudge, use_prompt_for_text, top_text_manual, bottom_text_manual
|
| 110 |
):
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
style_suffix = PRESETS.get(preset_name or "None", "")
|
| 113 |
gen_prompt = (base + " " + style_suffix).strip()
|
| 114 |
|
| 115 |
-
# 1)
|
| 116 |
if use_ai:
|
| 117 |
try:
|
| 118 |
img = try_generate_with_flux(gen_prompt, width, height)
|
|
@@ -125,57 +132,48 @@ def generate_and_meme(
|
|
| 125 |
draw = ImageDraw.Draw(img)
|
| 126 |
w, h = img.size
|
| 127 |
|
| 128 |
-
# 2)
|
| 129 |
if use_prompt_for_text:
|
| 130 |
top_text, bottom_text = smart_split_text(base)
|
| 131 |
else:
|
| 132 |
top_text, bottom_text = (top_text_manual or "").upper(), (bottom_text_manual or "").upper()
|
| 133 |
|
| 134 |
-
# 3)
|
| 135 |
-
base_size = max(12, int((w * font_size) / 100))
|
| 136 |
font = get_font(base_size)
|
| 137 |
stroke = int(max(0, stroke_width))
|
| 138 |
|
| 139 |
-
top_y = int(h * 0.03) +
|
| 140 |
_, _ = draw_block(draw, top_text, w, top_y, font, text_color, outline_color, stroke, align=align)
|
| 141 |
|
| 142 |
lines, heights = wrap_lines(draw, bottom_text, w, font, stroke)
|
| 143 |
total_bottom_h = sum(heights) + (len(heights)-1) * int(font.size*0.25)
|
| 144 |
-
bottom_y_start = int(h - total_bottom_h - h*0.03) -
|
| 145 |
draw_block(draw, bottom_text, w, bottom_y_start, font, text_color, outline_color, stroke, align=align)
|
| 146 |
|
| 147 |
return img
|
| 148 |
|
| 149 |
-
#
|
| 150 |
-
THEME = gr.themes.Soft(
|
| 151 |
-
primary_hue="indigo",
|
| 152 |
-
secondary_hue="violet",
|
| 153 |
-
neutral_hue="slate"
|
| 154 |
-
)
|
| 155 |
|
| 156 |
CUSTOM_CSS = """
|
| 157 |
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
| 158 |
-
|
| 159 |
:root { --radius: 14px; }
|
| 160 |
* { -webkit-tap-highlight-color: transparent; }
|
| 161 |
body { background: radial-gradient(1200px 600px at 50% -10%, #0d1220 10%, #05060b 70%); }
|
| 162 |
.gradio-container { max-width: 900px; margin: 0 auto; padding: 12px; }
|
| 163 |
h2, p { text-align: center; color: #cde3ff; text-shadow: 0 0 10px rgba(80,120,255,.25); }
|
| 164 |
h2 { font-family: 'Press Start 2P', system-ui, sans-serif; letter-spacing: 1px; font-size: 18px; }
|
| 165 |
-
.crt {
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
}
|
| 169 |
-
.crt::before {
|
| 170 |
-
content: ""; position: absolute; inset: 0; pointer-events: none;
|
| 171 |
background: repeating-linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.05) 1px, transparent 1px, transparent 3px);
|
| 172 |
-
mix-blend-mode: overlay; opacity: .25;
|
| 173 |
-
}
|
| 174 |
label { color: #a9b7ff !important; }
|
| 175 |
.gr-button { font-weight: 800; border-radius: 12px; }
|
| 176 |
"""
|
| 177 |
|
| 178 |
-
#
|
| 179 |
with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
|
| 180 |
gr.Markdown("<h2>🕹️ MEME LAB — RETRO EDITION</h2>"
|
| 181 |
"<p>One prompt → generate image → auto meme text. Style presets for instant vibes.</p>")
|
|
@@ -185,7 +183,6 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
|
|
| 185 |
prompt = gr.Textbox(label="Your idea (one prompt)", value="cat typing on a laptop at midnight")
|
| 186 |
preset = gr.Dropdown(choices=list(PRESETS.keys()), value="Retro Comic", label="Style preset")
|
| 187 |
use_ai = gr.Checkbox(label="Use AI generator (public FLUX Space)", value=False)
|
| 188 |
-
|
| 189 |
with gr.Row():
|
| 190 |
width = gr.Slider(384, 1024, value=768, step=64, label="Width")
|
| 191 |
height = gr.Slider(384, 1024, value=768, step=64, label="Height")
|
|
@@ -198,11 +195,9 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
|
|
| 198 |
align = gr.Radio(choices=["left", "center", "right"], value="center", label="Text alignment")
|
| 199 |
font_size = gr.Slider(8, 24, value=12, step=1, label="Font size (% of width)")
|
| 200 |
stroke_width = gr.Slider(0, 16, value=4, step=1, label="Outline thickness")
|
| 201 |
-
|
| 202 |
with gr.Row():
|
| 203 |
text_color = gr.ColorPicker(value="#FFFFFF", label="Text color")
|
| 204 |
outline_color = gr.ColorPicker(value="#000000", label="Outline color")
|
| 205 |
-
|
| 206 |
with gr.Row():
|
| 207 |
top_nudge = gr.Slider(-300, 300, value=0, step=1, label="Top nudge (px)")
|
| 208 |
bottom_nudge = gr.Slider(-300, 300, value=0, step=1, label="Bottom nudge (px)")
|
|
@@ -217,7 +212,6 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
|
|
| 217 |
|
| 218 |
generate.click(fn=generate_and_meme, inputs=inputs, outputs=out)
|
| 219 |
|
| 220 |
-
# Quick-refresh when only style/text settings change
|
| 221 |
for comp in [preset, use_prompt_for_text, top_text_manual, bottom_text_manual,
|
| 222 |
font_size, stroke_width, text_color, outline_color, align, top_nudge, bottom_nudge]:
|
| 223 |
comp.change(fn=generate_and_meme, inputs=inputs, outputs=out, show_progress=False)
|
|
|
|
| 10 |
def get_font(size: int):
|
| 11 |
for p in CANDIDATE_FONTS:
|
| 12 |
if os.path.exists(p):
|
| 13 |
+
return ImageFont.truetype(p, size=int(size))
|
| 14 |
return ImageFont.load_default()
|
| 15 |
|
| 16 |
# ---------------- Style Presets ----------------
|
|
|
|
| 25 |
"Synthwave Grid": "purple/indigo gradient sky, mountain silhouette, glowing sun, grid floor",
|
| 26 |
}
|
| 27 |
|
| 28 |
+
# -------- helpers --------
|
| 29 |
+
def i(v): # safe int
|
| 30 |
+
try: return int(round(float(v)))
|
| 31 |
+
except: return int(v)
|
| 32 |
+
|
| 33 |
def gradient_from_prompt(prompt: str, w=768, h=768):
|
| 34 |
+
w, h = i(w), i(h)
|
| 35 |
hsh = hashlib.sha256((prompt or "meme").encode()).hexdigest()
|
| 36 |
c1 = tuple(int(hsh[i:i+2], 16) for i in (0, 2, 4))
|
| 37 |
c2 = tuple(int(hsh[i:i+2], 16) for i in (6, 8, 10))
|
|
|
|
| 48 |
px[x, y] = (r, g, b)
|
| 49 |
return img
|
| 50 |
|
|
|
|
| 51 |
def wrap_lines(draw, text, img_w, font, stroke):
|
| 52 |
lines = []
|
| 53 |
max_chars = max(12, min(30, img_w // 30))
|
|
|
|
| 88 |
return " ".join(words[:mid]).upper(), " ".join(words[mid:]).upper()
|
| 89 |
return p.upper(), ""
|
| 90 |
|
| 91 |
+
# -------- Optional public Space call --------
|
| 92 |
def try_generate_with_flux(prompt: str, width: int, height: int):
|
| 93 |
from gradio_client import Client
|
|
|
|
| 94 |
client = Client("black-forest-labs/FLUX.1-schnell")
|
| 95 |
+
width, height = i(width), i(height)
|
| 96 |
try:
|
| 97 |
result = client.predict(prompt, width, height, api_name="/predict")
|
| 98 |
if isinstance(result, list): result = result[0]
|
| 99 |
return Image.open(result)
|
| 100 |
except Exception as e:
|
|
|
|
| 101 |
try:
|
| 102 |
result = client.predict(prompt, api_name="/run")
|
| 103 |
if isinstance(result, list): result = result[0]
|
|
|
|
| 105 |
except Exception:
|
| 106 |
raise e
|
| 107 |
|
| 108 |
+
# -------- Main pipeline --------
|
| 109 |
def generate_and_meme(
|
| 110 |
prompt, preset_name, use_ai, width, height,
|
| 111 |
font_size, stroke_width, text_color, outline_color,
|
| 112 |
align, top_nudge, bottom_nudge, use_prompt_for_text, top_text_manual, bottom_text_manual
|
| 113 |
):
|
| 114 |
+
width, height = i(width), i(height)
|
| 115 |
+
top_nudge, bottom_nudge = i(top_nudge), i(bottom_nudge)
|
| 116 |
+
stroke_width = i(stroke_width)
|
| 117 |
+
|
| 118 |
+
base = (prompt or "").trim() if hasattr(str, "trim") else (prompt or "").strip()
|
| 119 |
style_suffix = PRESETS.get(preset_name or "None", "")
|
| 120 |
gen_prompt = (base + " " + style_suffix).strip()
|
| 121 |
|
| 122 |
+
# 1) image
|
| 123 |
if use_ai:
|
| 124 |
try:
|
| 125 |
img = try_generate_with_flux(gen_prompt, width, height)
|
|
|
|
| 132 |
draw = ImageDraw.Draw(img)
|
| 133 |
w, h = img.size
|
| 134 |
|
| 135 |
+
# 2) text
|
| 136 |
if use_prompt_for_text:
|
| 137 |
top_text, bottom_text = smart_split_text(base)
|
| 138 |
else:
|
| 139 |
top_text, bottom_text = (top_text_manual or "").upper(), (bottom_text_manual or "").upper()
|
| 140 |
|
| 141 |
+
# 3) draw
|
| 142 |
+
base_size = max(12, int((w * float(font_size)) / 100))
|
| 143 |
font = get_font(base_size)
|
| 144 |
stroke = int(max(0, stroke_width))
|
| 145 |
|
| 146 |
+
top_y = int(h * 0.03) + top_nudge
|
| 147 |
_, _ = draw_block(draw, top_text, w, top_y, font, text_color, outline_color, stroke, align=align)
|
| 148 |
|
| 149 |
lines, heights = wrap_lines(draw, bottom_text, w, font, stroke)
|
| 150 |
total_bottom_h = sum(heights) + (len(heights)-1) * int(font.size*0.25)
|
| 151 |
+
bottom_y_start = int(h - total_bottom_h - h*0.03) - bottom_nudge
|
| 152 |
draw_block(draw, bottom_text, w, bottom_y_start, font, text_color, outline_color, stroke, align=align)
|
| 153 |
|
| 154 |
return img
|
| 155 |
|
| 156 |
+
# -------- Theme + Retro CSS --------
|
| 157 |
+
THEME = gr.themes.Soft(primary_hue="indigo", secondary_hue="violet", neutral_hue="slate")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
CUSTOM_CSS = """
|
| 160 |
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
|
|
|
| 161 |
:root { --radius: 14px; }
|
| 162 |
* { -webkit-tap-highlight-color: transparent; }
|
| 163 |
body { background: radial-gradient(1200px 600px at 50% -10%, #0d1220 10%, #05060b 70%); }
|
| 164 |
.gradio-container { max-width: 900px; margin: 0 auto; padding: 12px; }
|
| 165 |
h2, p { text-align: center; color: #cde3ff; text-shadow: 0 0 10px rgba(80,120,255,.25); }
|
| 166 |
h2 { font-family: 'Press Start 2P', system-ui, sans-serif; letter-spacing: 1px; font-size: 18px; }
|
| 167 |
+
.crt { position: relative; border: 2px solid #2a3350; border-radius: 12px; overflow: hidden;
|
| 168 |
+
box-shadow: 0 0 0 1px #0f1427 inset, 0 0 40px rgba(60,80,255,.25); }
|
| 169 |
+
.crt::before { content: ""; position: absolute; inset: 0; pointer-events: none;
|
|
|
|
|
|
|
|
|
|
| 170 |
background: repeating-linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.05) 1px, transparent 1px, transparent 3px);
|
| 171 |
+
mix-blend-mode: overlay; opacity: .25; }
|
|
|
|
| 172 |
label { color: #a9b7ff !important; }
|
| 173 |
.gr-button { font-weight: 800; border-radius: 12px; }
|
| 174 |
"""
|
| 175 |
|
| 176 |
+
# -------- App --------
|
| 177 |
with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
|
| 178 |
gr.Markdown("<h2>🕹️ MEME LAB — RETRO EDITION</h2>"
|
| 179 |
"<p>One prompt → generate image → auto meme text. Style presets for instant vibes.</p>")
|
|
|
|
| 183 |
prompt = gr.Textbox(label="Your idea (one prompt)", value="cat typing on a laptop at midnight")
|
| 184 |
preset = gr.Dropdown(choices=list(PRESETS.keys()), value="Retro Comic", label="Style preset")
|
| 185 |
use_ai = gr.Checkbox(label="Use AI generator (public FLUX Space)", value=False)
|
|
|
|
| 186 |
with gr.Row():
|
| 187 |
width = gr.Slider(384, 1024, value=768, step=64, label="Width")
|
| 188 |
height = gr.Slider(384, 1024, value=768, step=64, label="Height")
|
|
|
|
| 195 |
align = gr.Radio(choices=["left", "center", "right"], value="center", label="Text alignment")
|
| 196 |
font_size = gr.Slider(8, 24, value=12, step=1, label="Font size (% of width)")
|
| 197 |
stroke_width = gr.Slider(0, 16, value=4, step=1, label="Outline thickness")
|
|
|
|
| 198 |
with gr.Row():
|
| 199 |
text_color = gr.ColorPicker(value="#FFFFFF", label="Text color")
|
| 200 |
outline_color = gr.ColorPicker(value="#000000", label="Outline color")
|
|
|
|
| 201 |
with gr.Row():
|
| 202 |
top_nudge = gr.Slider(-300, 300, value=0, step=1, label="Top nudge (px)")
|
| 203 |
bottom_nudge = gr.Slider(-300, 300, value=0, step=1, label="Bottom nudge (px)")
|
|
|
|
| 212 |
|
| 213 |
generate.click(fn=generate_and_meme, inputs=inputs, outputs=out)
|
| 214 |
|
|
|
|
| 215 |
for comp in [preset, use_prompt_for_text, top_text_manual, bottom_text_manual,
|
| 216 |
font_size, stroke_width, text_color, outline_color, align, top_nudge, bottom_nudge]:
|
| 217 |
comp.change(fn=generate_and_meme, inputs=inputs, outputs=out, show_progress=False)
|