rickveloper commited on
Commit
eaad39c
·
verified ·
1 Parent(s): 2350fb4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -70
app.py CHANGED
@@ -1,33 +1,60 @@
1
  import gradio as gr
2
  from PIL import Image, ImageDraw, ImageFont
3
- import textwrap, os
4
 
5
- # -------- Fonts --------
6
  CANDIDATE_FONTS = [
7
  "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
8
  "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
9
  ]
10
- def get_font(size: int) -> ImageFont.FreeTypeFont:
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
- # -------- Text drawing helpers --------
17
- def draw_text_block(draw, text, img_w, y, font, fill, stroke_fill, stroke_width, align="center"):
18
- """Draw wrapped multi-line text with stroke at given Y; returns last Y used."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  lines = []
20
  max_chars = max(12, min(30, img_w // 30))
21
  for paragraph in (text or "").split("\n"):
22
  wrapped = textwrap.wrap(paragraph, width=max_chars)
23
  lines.extend(wrapped if wrapped else [""])
 
 
24
 
25
- # measure each line height
26
- line_heights = []
27
- for line in lines:
28
- bbox = draw.textbbox((0, 0), line, font=font, stroke_width=stroke_width)
29
- line_heights.append(bbox[3] - bbox[1])
30
-
31
  curr_y = y
32
  for i, line in enumerate(lines):
33
  w = draw.textlength(line, font=font, stroke_width=stroke_width)
@@ -35,105 +62,165 @@ def draw_text_block(draw, text, img_w, y, font, fill, stroke_fill, stroke_width,
35
  x = (img_w - w) / 2
36
  elif align == "left":
37
  x = int(img_w * 0.05)
38
- else: # right
39
  x = img_w - int(img_w * 0.05) - w
40
  draw.text((x, curr_y), line, font=font, fill=fill,
41
  stroke_width=stroke_width, stroke_fill=stroke_fill)
42
- curr_y += line_heights[i] + int(font.size * 0.25)
43
- return curr_y
44
-
45
- # -------- Core meme function --------
46
- def make_meme(
47
- image, top_text, bottom_text,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  font_size, stroke_width, text_color, outline_color,
49
- align, top_nudge, bottom_nudge
50
  ):
51
- if image is None:
52
- return None
53
-
54
- img = image.convert("RGB")
 
 
 
 
 
 
 
 
 
 
55
  draw = ImageDraw.Draw(img)
56
  w, h = img.size
57
 
58
- # font scales with width (feels right on mobile photos)
 
 
 
 
 
 
59
  base_size = max(12, int((w * font_size) / 100))
60
  font = get_font(base_size)
61
  stroke = int(max(0, stroke_width))
62
 
63
- # Top block
64
  top_y = int(h * 0.03) + int(top_nudge)
65
- draw_text_block(
66
- draw=draw, text=(top_text or "").strip(), img_w=w, y=top_y, font=font,
67
- fill=text_color, stroke_fill=outline_color, stroke_width=stroke, align=align
68
- )
69
-
70
- # Bottom block: measure height first to anchor from bottom
71
- lines = textwrap.wrap(bottom_text or "", width=max(20, w // 30))
72
- line_heights = [draw.textbbox((0, 0), l, font=font, stroke_width=stroke)[3] for l in lines]
73
- total_bottom_h = sum(line_heights) + (len(lines) - 1) * int(font.size * 0.25)
74
- bottom_y_start = int(h - total_bottom_h - h * 0.03) - int(bottom_nudge)
75
-
76
- draw_text_block(
77
- draw=draw, text=(bottom_text or "").strip(), img_w=w, y=bottom_y_start, font=font,
78
- fill=text_color, stroke_fill=outline_color, stroke_width=stroke, align=align
79
- )
80
  return img
81
 
82
- # -------- Theme (only props supported on older Gradio) --------
83
  THEME = gr.themes.Soft(
84
  primary_hue="indigo",
85
  secondary_hue="violet",
86
  neutral_hue="slate"
87
- ).set(
88
- button_border_width="0px",
89
- button_large_padding="14px",
90
- button_large_radius="14px",
91
- body_background_fill_dark="linear-gradient(180deg, #0b0f17 0%, #0a0920 100%)",
92
- block_shadow="none",
93
  )
94
 
95
  CUSTOM_CSS = """
 
 
96
  :root { --radius: 14px; }
97
  * { -webkit-tap-highlight-color: transparent; }
98
- .gradio-container { padding: 10px; max-width: 820px; margin: 0 auto; }
99
- h2, p { text-align:center }
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  """
101
 
102
- # -------- App --------
103
  with gr.Blocks(theme=THEME, css=CUSTOM_CSS) as demo:
104
- gr.Markdown("<h2>📱 Mobile Meme Maker</h2><p>Upload → Type → Download. Built on iPhone, for iPhone.</p>")
 
105
 
106
  with gr.Row():
107
- with gr.Column():
108
- in_img = gr.Image(type="pil", label="Upload photo", height=320)
 
 
109
 
110
- top_text = gr.Textbox(label="Top text", value="WHEN THE WIFI JUST WORKS")
111
- bottom_text = gr.Textbox(label="Bottom text", value="CHEF'S KISS")
 
112
 
113
- # No inline=… (older Gradio)
114
- align = gr.Radio(choices=["left", "center", "right"], value="center", label="Alignment")
 
 
115
 
116
- font_size = gr.Slider(8, 22, value=12, step=1, label="Font size (% of width)")
117
- stroke_width = gr.Slider(0, 14, value=4, step=1, label="Outline thickness")
 
118
 
119
  with gr.Row():
120
- text_color = gr.ColorPicker(value="#FFFFFF", label="Text")
121
- outline_color = gr.ColorPicker(value="#000000", label="Outline")
122
 
123
  with gr.Row():
124
  top_nudge = gr.Slider(-300, 300, value=0, step=1, label="Top nudge (px)")
125
  bottom_nudge = gr.Slider(-300, 300, value=0, step=1, label="Bottom nudge (px)")
126
 
127
- with gr.Column():
128
- out = gr.Image(type="pil", label="Preview / Download", height=440, show_download_button=True)
129
- generate = gr.Button("✨ Generate", size="lg")
 
 
 
 
130
 
131
- inputs = [in_img, top_text, bottom_text, font_size, stroke_width, text_color, outline_color, align, top_nudge, bottom_nudge]
132
- generate.click(fn=make_meme, inputs=inputs, outputs=out)
133
 
134
- # live preview as controls change
135
- for comp in inputs:
136
- comp.change(fn=make_meme, inputs=inputs, outputs=out, show_progress=False)
 
137
 
138
  if __name__ == "__main__":
139
  demo.launch()
 
1
  import gradio as gr
2
  from PIL import Image, ImageDraw, ImageFont
3
+ import textwrap, os, hashlib
4
 
5
+ # ---------------- Fonts ----------------
6
  CANDIDATE_FONTS = [
7
  "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
8
  "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
9
  ]
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 ----------------
17
+ PRESETS = {
18
+ "None": "",
19
+ "Retro Comic": "bold comic outline, grain, 35mm scan, high contrast",
20
+ "Vaporwave": "vaporwave, neon pink and cyan, miami sunset, grid floor, retro synth",
21
+ "Game Boy": "4-color green palette, pixelated, 160x144 vibe, dithering",
22
+ "Newspaper Halftone": "b&w halftone dots, newspaper print texture, vintage headline vibe",
23
+ "Cyberpunk Neon": "rainy night, neon kanji signs, purple and blue rim light, blade runner",
24
+ "90s Web": "early web aesthetic, gradients, bevel buttons, clipart stars, lens flare",
25
+ "Synthwave Grid": "purple/indigo gradient sky, mountain silhouette, glowing sun, grid floor",
26
+ }
27
+
28
+ # ---------------- Utility: gradient fallback (fast, offline) ----------------
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))
33
+ c1 = tuple(min(255, int(v*1.2)) for v in c1)
34
+ c2 = tuple(min(255, int(v*1.1)) for v in c2)
35
+ img = Image.new("RGB", (w, h), c1)
36
+ px = img.load()
37
+ for y in range(h):
38
+ t = y / (h-1)
39
+ r = int(c1[0]*(1-t) + c2[0]*t)
40
+ g = int(c1[1]*(1-t) + c2[1]*t)
41
+ b = int(c1[2]*(1-t) + c2[2]*t)
42
+ for x in range(w):
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))
50
  for paragraph in (text or "").split("\n"):
51
  wrapped = textwrap.wrap(paragraph, width=max_chars)
52
  lines.extend(wrapped if wrapped else [""])
53
+ heights = [draw.textbbox((0, 0), ln, font=font, stroke_width=stroke)[3] for ln in lines]
54
+ return lines, heights
55
 
56
+ def draw_block(draw, text, img_w, y, font, fill, stroke_fill, stroke_width, align="center"):
57
+ lines, heights = wrap_lines(draw, text, img_w, font, stroke_width)
 
 
 
 
58
  curr_y = y
59
  for i, line in enumerate(lines):
60
  w = draw.textlength(line, font=font, stroke_width=stroke_width)
 
62
  x = (img_w - w) / 2
63
  elif align == "left":
64
  x = int(img_w * 0.05)
65
+ else:
66
  x = img_w - int(img_w * 0.05) - w
67
  draw.text((x, curr_y), line, font=font, fill=fill,
68
  stroke_width=stroke_width, stroke_fill=stroke_fill)
69
+ curr_y += heights[i] + int(font.size * 0.25)
70
+ total_h = sum(heights) + (len(heights)-1) * int(font.size*0.25)
71
+ return curr_y, total_h
72
+
73
+ def smart_split_text(prompt: str):
74
+ p = (prompt or "").strip()
75
+ if not p:
76
+ return "TOP TEXT", "BOTTOM TEXT"
77
+ for sep in ["|", " - ", " — ", ":", ";"]:
78
+ if sep in p:
79
+ a, b = p.split(sep, 1)
80
+ return a.strip().upper(), b.strip().upper()
81
+ words = p.split()
82
+ if len(words) > 6:
83
+ mid = len(words) // 2
84
+ return " ".join(words[:mid]).upper(), " ".join(words[mid:]).upper()
85
+ return p.upper(), ""
86
+
87
+ # ---------------- Optional: call public FLUX Space ----------------
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]
101
+ return Image.open(result)
102
+ except Exception:
103
+ raise e
104
+
105
+ # ---------------- Main pipeline ----------------
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
+ base = (prompt or "").strip()
112
+ style_suffix = PRESETS.get(preset_name or "None", "")
113
+ gen_prompt = (base + " " + style_suffix).strip()
114
+
115
+ # 1) Image
116
+ if use_ai:
117
+ try:
118
+ img = try_generate_with_flux(gen_prompt, width, height)
119
+ except Exception:
120
+ img = gradient_from_prompt(gen_prompt, w=width, h=height)
121
+ else:
122
+ img = gradient_from_prompt(gen_prompt, w=width, h=height)
123
+
124
+ img = img.convert("RGB")
125
  draw = ImageDraw.Draw(img)
126
  w, h = img.size
127
 
128
+ # 2) Meme text
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) Draw
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) + int(top_nudge)
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) - int(bottom_nudge)
145
+ draw_block(draw, bottom_text, w, bottom_y_start, font, text_color, outline_color, stroke, align=align)
146
+
 
 
 
 
 
 
 
 
147
  return img
148
 
149
+ # ---------------- Theme + Retro CSS ----------------
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
+ position: relative; border: 2px solid #2a3350; border-radius: 12px; overflow: hidden;
167
+ box-shadow: 0 0 0 1px #0f1427 inset, 0 0 40px rgba(60,80,255,.25);
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
+ # ---------------- App ----------------
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>")
182
 
183
  with gr.Row():
184
+ with gr.Column(scale=1, elem_classes=["crt"]):
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")
192
 
193
+ gr.Markdown("### Meme Text")
194
+ use_prompt_for_text = gr.Checkbox(label="Auto from prompt", value=True)
195
+ top_text_manual = gr.Textbox(label="Top text (if not auto)", value="", interactive=True)
196
+ bottom_text_manual = gr.Textbox(label="Bottom text (if not auto)", value="", interactive=True)
197
 
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)")
209
 
210
+ with gr.Column(scale=1, elem_classes=["crt"]):
211
+ out = gr.Image(type="pil", label="Preview / Download", height=540, show_download_button=True)
212
+ generate = gr.Button("✨ Generate Image + Meme", variant="primary")
213
+
214
+ inputs = [prompt, preset, use_ai, width, height,
215
+ font_size, stroke_width, text_color, outline_color,
216
+ align, top_nudge, bottom_nudge, use_prompt_for_text, top_text_manual, bottom_text_manual]
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)
224
 
225
  if __name__ == "__main__":
226
  demo.launch()