Spaces:
Sleeping
Sleeping
v10: Pillow speech bubbles overlay for legible dialogue + Gemini 3 Pro primary
Browse files
app.py
CHANGED
|
@@ -270,6 +270,98 @@ def build_page_prompt(page_data, script, style):
|
|
| 270 |
return prompt[:1800]
|
| 271 |
|
| 272 |
# ββ GENERATE ONE PAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
def generate_page(page_data, script, style, title, page_num, total, api_key):
|
| 274 |
prompt = build_page_prompt(page_data, script, style)
|
| 275 |
seed = page_num * 137
|
|
@@ -291,6 +383,10 @@ def generate_page(page_data, script, style, title, page_num, total, api_key):
|
|
| 291 |
print(' Using placeholder')
|
| 292 |
art = placeholder(page_num, title)
|
| 293 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
final = add_header_footer(art, page_num, total, title)
|
| 295 |
print('Page ' + str(page_num) + ' done')
|
| 296 |
return final
|
|
|
|
| 270 |
return prompt[:1800]
|
| 271 |
|
| 272 |
# ββ GENERATE ONE PAGE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 273 |
+
|
| 274 |
+
def draw_speech_bubbles(img, panels):
|
| 275 |
+
"""Draw readable speech bubbles with Pillow over the comic page."""
|
| 276 |
+
draw = ImageDraw.Draw(img)
|
| 277 |
+
W, H = img.size
|
| 278 |
+
|
| 279 |
+
# Calculate panel zones (4 panels: top-wide, mid-left, mid-right, bottom-wide)
|
| 280 |
+
panel_zones = [
|
| 281 |
+
(0, 0, W, H//2), # Panel 1 top half
|
| 282 |
+
(0, H//2, W//2, H), # Panel 2 bottom-left
|
| 283 |
+
(W//2, H//2, W, H), # Panel 3 bottom-right
|
| 284 |
+
]
|
| 285 |
+
if len(panels) >= 4:
|
| 286 |
+
panel_zones = [
|
| 287 |
+
(0, 0, W, H//3), # Panel 1 top strip
|
| 288 |
+
(0, H//3, W//2, 2*H//3), # Panel 2 mid-left
|
| 289 |
+
(W//2, H//3, W, 2*H//3), # Panel 3 mid-right
|
| 290 |
+
(0, 2*H//3, W, H), # Panel 4 bottom strip
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
font_b = fnt(_B, 22)
|
| 294 |
+
font_r = fnt(_R, 20)
|
| 295 |
+
|
| 296 |
+
for i, panel in enumerate(panels[:len(panel_zones)]):
|
| 297 |
+
if i >= len(panel_zones):
|
| 298 |
+
break
|
| 299 |
+
px0, py0, px1, py1 = panel_zones[i]
|
| 300 |
+
dialogues = panel.get('dialogue', [])
|
| 301 |
+
caption = panel.get('caption', '')
|
| 302 |
+
|
| 303 |
+
bubble_y = py1 - 15 # Start from bottom of panel
|
| 304 |
+
|
| 305 |
+
# Draw dialogues (from bottom up)
|
| 306 |
+
for j, dlg in enumerate(reversed(dialogues[-2:])): # max 2 bubbles per panel
|
| 307 |
+
char = dlg.get('character', '')
|
| 308 |
+
text = dlg.get('text', '')
|
| 309 |
+
if not text:
|
| 310 |
+
continue
|
| 311 |
+
|
| 312 |
+
# Wrap text
|
| 313 |
+
max_chars = 25
|
| 314 |
+
words = text.split()
|
| 315 |
+
wrapped_lines = []
|
| 316 |
+
line = ''
|
| 317 |
+
for w in words:
|
| 318 |
+
if len(line) + len(w) + 1 <= max_chars:
|
| 319 |
+
line = (line + ' ' + w).strip()
|
| 320 |
+
else:
|
| 321 |
+
if line: wrapped_lines.append(line)
|
| 322 |
+
line = w
|
| 323 |
+
if line: wrapped_lines.append(line)
|
| 324 |
+
wrapped_lines = wrapped_lines[:3] # max 3 lines
|
| 325 |
+
|
| 326 |
+
# Calculate bubble dimensions
|
| 327 |
+
line_h = 26
|
| 328 |
+
text_h = len(wrapped_lines) * line_h + 8
|
| 329 |
+
char_w = max(len(l) for l in wrapped_lines) * 13 + 20
|
| 330 |
+
char_w = min(char_w, (px1 - px0) - 20)
|
| 331 |
+
bub_h = text_h + 30 # space for char name
|
| 332 |
+
|
| 333 |
+
# Position bubble
|
| 334 |
+
bub_x0 = px0 + 15
|
| 335 |
+
bub_y1 = bubble_y
|
| 336 |
+
bub_y0 = bub_y1 - bub_h
|
| 337 |
+
bub_x1 = bub_x0 + char_w
|
| 338 |
+
|
| 339 |
+
if bub_y0 < py0 + 10:
|
| 340 |
+
break # No space
|
| 341 |
+
|
| 342 |
+
# Draw white rounded bubble
|
| 343 |
+
draw.rounded_rectangle([bub_x0, bub_y0, bub_x1, bub_y1], radius=12,
|
| 344 |
+
fill=(255,255,255), outline=(20,20,20), width=2)
|
| 345 |
+
|
| 346 |
+
# Character name in bold
|
| 347 |
+
draw.text((bub_x0+10, bub_y0+6), char[:12] + ':', font=font_b, fill=(20,20,80))
|
| 348 |
+
|
| 349 |
+
# Dialogue text
|
| 350 |
+
for k, wl in enumerate(wrapped_lines):
|
| 351 |
+
draw.text((bub_x0+10, bub_y0+28+k*line_h), wl, font=font_r, fill=(20,20,20))
|
| 352 |
+
|
| 353 |
+
bubble_y = bub_y0 - 8 # Stack bubbles upward
|
| 354 |
+
|
| 355 |
+
# Draw caption box at top of panel if exists
|
| 356 |
+
if caption:
|
| 357 |
+
cap_text = caption[:50]
|
| 358 |
+
cap_w = len(cap_text) * 10 + 20
|
| 359 |
+
cap_w = min(cap_w, px1 - px0 - 10)
|
| 360 |
+
draw.rectangle([px0+5, py0+5, px0+cap_w, py0+30], fill=(255,255,220), outline=(100,100,0), width=1)
|
| 361 |
+
draw.text((px0+10, py0+8), cap_text, font=fnt(_R, 16), fill=(60,40,0))
|
| 362 |
+
|
| 363 |
+
return img
|
| 364 |
+
|
| 365 |
def generate_page(page_data, script, style, title, page_num, total, api_key):
|
| 366 |
prompt = build_page_prompt(page_data, script, style)
|
| 367 |
seed = page_num * 137
|
|
|
|
| 383 |
print(' Using placeholder')
|
| 384 |
art = placeholder(page_num, title)
|
| 385 |
|
| 386 |
+
# Add readable speech bubbles with Pillow (works even with Pollinations)
|
| 387 |
+
panels = page_data.get('panels', [])
|
| 388 |
+
art = draw_speech_bubbles(art.copy(), panels)
|
| 389 |
+
|
| 390 |
final = add_header_footer(art, page_num, total, title)
|
| 391 |
print('Page ' + str(page_num) + ' done')
|
| 392 |
return final
|