Codex Claude Sonnet 4.6 commited on
Commit
786a8ec
·
1 Parent(s): 7301102

Remove char name field + page slider; illustrated PDF cover

Browse files

- Removed unused 'Character name' input field — only hero name is needed
- Removed 'Number of pages' slider — pages 7-10 generated identical filler
because the model reliably writes 6 pages; fixed at 6 for a complete book
- PDF cover now uses the first FLUX illustration as a full-bleed background
with dark banner overlays for kicker + title + badge, matching the in-app
HTML cover; fallback cream design kept for when no illustration is available
- Both story PDF and coloring PDF use the same illustrated cover system

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +5 -7
  2. book_builder.py +132 -72
  3. ui/layout.py +3 -20
app.py CHANGED
@@ -743,15 +743,13 @@ def generate_tts_gpu(text: str, voice: str = DEFAULT_VOICE,
743
  # MAIN BOOK CREATION (Generator for streaming)
744
  # ============================================================================
745
 
746
- def create_book(doodle_image, character_name, theme, hero_name,
747
  voice=DEFAULT_VOICE, make_coloring=False,
748
- num_pages=6, custom_voice_wav=None):
749
- """ZeroGPU book flow: story → images → narration → PDFs → coloring book,
750
- each a sequential @spaces.GPU call (ZeroGPU has one GPU per request)."""
751
  t_total = time.perf_counter()
752
- character_name = (character_name or "").strip() or "Little Hero"
753
- hero_name = (hero_name or "").strip() or character_name
754
- num_pages = max(6, min(10, int(num_pages or 6)))
755
 
756
  trace_data = {
757
  "backend": "zerogpu",
 
743
  # MAIN BOOK CREATION (Generator for streaming)
744
  # ============================================================================
745
 
746
+ def create_book(doodle_image, theme, hero_name,
747
  voice=DEFAULT_VOICE, make_coloring=False,
748
+ custom_voice_wav=None):
749
+ """ZeroGPU book flow: story → images → narration → PDFs → coloring book."""
 
750
  t_total = time.perf_counter()
751
+ hero_name = (hero_name or "").strip() or "Little Hero"
752
+ num_pages = 6
 
753
 
754
  trace_data = {
755
  "backend": "zerogpu",
book_builder.py CHANGED
@@ -196,45 +196,12 @@ def _paper_grain(img, rng_seed=42, amount=2000):
196
  )
197
 
198
 
199
- def render_cover_image(
200
- title: str,
201
- badge_text: str = "illustrated by FLUX.2-klein",
202
- kind: str = "story",
203
- ) -> bytes:
204
- """
205
- Render a full-page scrapbook-style cover as PNG bytes.
206
-
207
- Canvas ~1240x1754 (A4 @150dpi). Cream bg, crayon title with layered
208
- shadow, badge. Used as page 1 of both story and coloring PDFs.
209
- """
210
- W, H = 1240, 1754
211
- cream = (255, 248, 230)
212
- ink = (46, 42, 38)
213
- sun = (244, 198, 74)
214
- berry = (214, 81, 122)
215
- leaf = (116, 184, 90)
216
-
217
- img = Image.new("RGB", (W, H), cream)
218
- draw = ImageDraw.Draw(img)
219
-
220
- _paper_grain(img, amount=6000)
221
-
222
- # Kicker
223
- font_kicker = _load_font(FONT_CAVEAT, 60)
224
- draw.text((W // 2, 520), "a DoodleBook story", font=font_kicker,
225
- fill=berry, anchor="mm")
226
-
227
- # Title — layered shadow
228
- font_title = _load_font(FONT_GAEGU, 120)
229
-
230
- # wrap title to <=2 lines
231
  words = title.split()
232
- lines = []
233
- current = ""
234
  for w in words:
235
  test = f"{current} {w}".strip()
236
- bbox = font_title.getbbox(test)
237
- if bbox[2] - bbox[0] > W - 200:
238
  if current:
239
  lines.append(current)
240
  current = w
@@ -242,41 +209,128 @@ def render_cover_image(
242
  current = test
243
  if current:
244
  lines.append(current)
 
245
 
246
- total_h = len(lines) * 140
247
- y_start = 680
248
-
249
- for i, line in enumerate(lines):
250
- cy = y_start + i * 140
251
- # sun shadow
252
- draw.text((W // 2 + 4, cy + 4), line, font=font_title,
253
- fill=sun, anchor="mm")
254
- # ink on top
255
- draw.text((W // 2, cy), line, font=font_title,
256
- fill=ink, anchor="mm")
257
-
258
- # Badge — rounded rect
259
- font_badge = _load_font(FONT_GAEGU, 44)
260
- bbox = font_badge.getbbox(badge_text)
261
- tw = bbox[2] - bbox[0]
262
- th = bbox[3] - bbox[1]
263
- bx = W // 2 - (tw + 40) // 2
264
- by = y_start + total_h + 80
265
- draw.rounded_rectangle(
266
- [bx, by, bx + tw + 40, by + th + 24],
267
- radius=16, fill=leaf,
268
- )
269
- draw.text((bx + 20 + tw // 2, by + 12 + th // 2), badge_text,
270
- font=font_badge, fill=(255, 255, 255), anchor="mm")
271
 
272
- # Crayon underline squiggle
273
- import random
274
- rng = random.Random(77)
275
- sx = W // 2 - 180
276
- sy = y_start + total_h + 40
277
- pts = [(sx + j * 10, sy + rng.randint(-5, 5)) for j in range(36)]
278
- draw.line(pts, fill=leaf, width=5, joint="curve")
 
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  buf = io.BytesIO()
281
  img.save(buf, format="PNG")
282
  return buf.getvalue()
@@ -386,8 +440,11 @@ def export_pdf(
386
  pdf = FPDF()
387
  pdf.set_auto_page_break(auto=False)
388
 
389
- # Styled cover
390
- cover_png = render_cover_image(title, "illustrated by FLUX.2-klein", "story")
 
 
 
391
  pdf.add_page()
392
  _pdf_cover_page(pdf, cover_png)
393
 
@@ -424,8 +481,11 @@ def export_coloring_pdf(
424
  pdf = FPDF()
425
  pdf.set_auto_page_break(auto=False)
426
 
427
- # Styled cover
428
- cover_png = render_cover_image(title, "a coloring book to color in", "coloring")
 
 
 
429
  pdf.add_page()
430
  _pdf_cover_page(pdf, cover_png)
431
 
 
196
  )
197
 
198
 
199
+ def _wrap_title(title: str, font, max_w: int) -> list:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  words = title.split()
201
+ lines, current = [], ""
 
202
  for w in words:
203
  test = f"{current} {w}".strip()
204
+ if font.getbbox(test)[2] - font.getbbox(test)[0] > max_w:
 
205
  if current:
206
  lines.append(current)
207
  current = w
 
209
  current = test
210
  if current:
211
  lines.append(current)
212
+ return lines or [title]
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
+ def render_cover_image(
216
+ title: str,
217
+ badge_text: str = "illustrated by FLUX.2-klein",
218
+ kind: str = "story",
219
+ first_page_bytes: bytes = None,
220
+ ) -> bytes:
221
+ """
222
+ Render a full-page illustrated cover as PNG bytes (A4 @150dpi, 1240x1754).
223
 
224
+ When first_page_bytes is provided (the FLUX page 1 illustration), it fills
225
+ the cover as a full-bleed background with dark banner overlays for the title.
226
+ Falls back to cream-background design when no illustration is available.
227
+ """
228
+ W, H = 1240, 1754
229
+ sun = (244, 198, 74)
230
+ leaf = (116, 184, 90)
231
+ white = (255, 255, 255)
232
+ ink = (46, 42, 38)
233
+ berry = (214, 81, 122)
234
+ dark = (30, 26, 24)
235
+
236
+ if first_page_bytes:
237
+ # --- Full-bleed FLUX illustration cover ---
238
+ src = Image.open(io.BytesIO(first_page_bytes)).convert("RGB")
239
+ # Scale-and-crop to fill A4 exactly
240
+ src_r = src.width / src.height
241
+ tar_r = W / H
242
+ if src_r > tar_r:
243
+ new_h, new_w = H, int(H * src_r)
244
+ else:
245
+ new_w, new_h = W, int(W / src_r)
246
+ src = src.resize((new_w, new_h), Image.LANCZOS)
247
+ left = (new_w - W) // 2
248
+ top = (new_h - H) // 2
249
+ base = src.crop((left, top, left + W, top + H)).convert("RGBA")
250
+
251
+ # Dark banner overlays (semi-transparent)
252
+ ov = Image.new("RGBA", (W, H), (0, 0, 0, 0))
253
+ dov = ImageDraw.Draw(ov)
254
+ # Top strip (kicker)
255
+ dov.rectangle([0, 0, W, 170], fill=(*dark, 210))
256
+ # Bottom strip (title + badge) — taller when title is long
257
+ dov.rectangle([0, H - 540, W, H], fill=(*dark, 215))
258
+ # Decorative inner border
259
+ for i in range(3):
260
+ dov.rectangle([i*10, i*10, W - i*10, H - i*10],
261
+ outline=(255, 255, 255, 60), width=2)
262
+
263
+ img = Image.alpha_composite(base, ov).convert("RGB")
264
+ draw = ImageDraw.Draw(img)
265
+
266
+ # Kicker
267
+ font_kicker = _load_font(FONT_CAVEAT, 68)
268
+ draw.text((W // 2, 88), "✨ a DoodleBook story ✨",
269
+ font=font_kicker, fill=sun, anchor="mm")
270
+
271
+ # Title
272
+ font_title = _load_font(FONT_GAEGU, 108)
273
+ lines = _wrap_title(title, font_title, W - 140)
274
+ line_h = 128
275
+ total_h = len(lines) * line_h
276
+ y0 = H - 510 + max(0, (460 - total_h)) // 2
277
+ for i, line in enumerate(lines):
278
+ cy = y0 + i * line_h
279
+ draw.text((W // 2 + 3, cy + 3), line, font=font_title,
280
+ fill=sun, anchor="mm")
281
+ draw.text((W // 2, cy), line, font=font_title,
282
+ fill=white, anchor="mm")
283
+
284
+ # Badge
285
+ font_badge = _load_font(FONT_GAEGU, 46)
286
+ bb = font_badge.getbbox(badge_text)
287
+ tw, th = bb[2] - bb[0], bb[3] - bb[1]
288
+ bx = W // 2 - (tw + 44) // 2
289
+ by = H - 72
290
+ draw.rounded_rectangle([bx, by - th - 16, bx + tw + 44, by],
291
+ radius=14, fill=leaf)
292
+ draw.text((W // 2, by - th // 2 - 8), badge_text,
293
+ font=font_badge, fill=white, anchor="mm")
294
+
295
+ else:
296
+ # --- Cream fallback cover ---
297
+ cream = (255, 248, 230)
298
+ img = Image.new("RGB", (W, H), cream)
299
+ draw = ImageDraw.Draw(img)
300
+ _paper_grain(img, amount=6000)
301
+
302
+ font_kicker = _load_font(FONT_CAVEAT, 60)
303
+ draw.text((W // 2, 520), "✨ a DoodleBook story ✨",
304
+ font=font_kicker, fill=berry, anchor="mm")
305
+
306
+ font_title = _load_font(FONT_GAEGU, 120)
307
+ lines = _wrap_title(title, font_title, W - 200)
308
+ total_h = len(lines) * 140
309
+ y0 = 680
310
+ for i, line in enumerate(lines):
311
+ cy = y0 + i * 140
312
+ draw.text((W // 2 + 4, cy + 4), line, font=font_title,
313
+ fill=sun, anchor="mm")
314
+ draw.text((W // 2, cy), line, font=font_title,
315
+ fill=ink, anchor="mm")
316
+
317
+ font_badge = _load_font(FONT_GAEGU, 44)
318
+ bb = font_badge.getbbox(badge_text)
319
+ tw, th = bb[2] - bb[0], bb[3] - bb[1]
320
+ bx = W // 2 - (tw + 40) // 2
321
+ by = y0 + total_h + 80
322
+ draw.rounded_rectangle([bx, by, bx + tw + 40, by + th + 24],
323
+ radius=16, fill=leaf)
324
+ draw.text((bx + 20 + tw // 2, by + 12 + th // 2), badge_text,
325
+ font=font_badge, fill=white, anchor="mm")
326
+
327
+ import random
328
+ rng = random.Random(77)
329
+ sx, sy = W // 2 - 180, y0 + total_h + 40
330
+ pts = [(sx + j * 10, sy + rng.randint(-5, 5)) for j in range(36)]
331
+ draw.line(pts, fill=leaf, width=5, joint="curve")
332
+
333
+ _paper_grain(img, amount=2000)
334
  buf = io.BytesIO()
335
  img.save(buf, format="PNG")
336
  return buf.getvalue()
 
440
  pdf = FPDF()
441
  pdf.set_auto_page_break(auto=False)
442
 
443
+ # Cover: full-bleed FLUX illustration when available
444
+ cover_png = render_cover_image(
445
+ title, "illustrated by FLUX.2-klein", "story",
446
+ first_page_bytes=pages_images[0] if pages_images else None,
447
+ )
448
  pdf.add_page()
449
  _pdf_cover_page(pdf, cover_png)
450
 
 
481
  pdf = FPDF()
482
  pdf.set_auto_page_break(auto=False)
483
 
484
+ # Cover: first coloring-page illustration as full-bleed background
485
+ cover_png = render_cover_image(
486
+ title, "a coloring book to color in", "coloring",
487
+ first_page_bytes=outline_imgs[0] if outline_imgs else None,
488
+ )
489
  pdf.add_page()
490
  _pdf_cover_page(pdf, cover_png)
491
 
ui/layout.py CHANGED
@@ -284,12 +284,6 @@ body, gradio-app {
284
  }
285
  .theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; }
286
 
287
- /* Page-count slider */
288
- .page-slider label span {
289
- font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
290
- font-size: 19px !important; color: var(--ink) !important;
291
- }
292
- .page-slider input[type="range"] { accent-color: var(--crayon-teal) !important; }
293
 
294
  /* ============================== STATUS ============================== */
295
  .status-display textarea {
@@ -516,11 +510,6 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
516
  elem_classes=["doodle-input"],
517
  )
518
  gr.HTML('<p class="card-eyebrow">2 &middot; the details</p>')
519
- char_name = gr.Textbox(
520
- label="Character name",
521
- placeholder="Ziggy the robot",
522
- elem_classes=["field"],
523
- )
524
  hero_name = gr.Textbox(
525
  label="Hero's name in the story",
526
  placeholder="Ziggy",
@@ -531,11 +520,6 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
531
  label="Story theme (pick one)",
532
  elem_classes=["field", "theme-pick"],
533
  )
534
- num_pages = gr.Slider(
535
- minimum=6, maximum=10, step=1, value=6,
536
- label="Number of pages",
537
- elem_classes=["field", "page-slider"],
538
- )
539
  voice = gr.Radio(
540
  choices=VOICE_CHOICES, value=DEFAULT_VOICE,
541
  label="Narrator voice",
@@ -563,8 +547,8 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
563
  value="Ready when you are! ✏️",
564
  )
565
  gr.Examples(
566
- examples=[["assets/sample_doodle.jpg", "Ziggy", "Ziggy", "brave adventure"]],
567
- inputs=[doodle, char_name, hero_name, theme],
568
  label="Try an example",
569
  )
570
 
@@ -638,8 +622,7 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
638
  if create_book_fn:
639
  make_btn.click(
640
  fn=create_book_fn,
641
- inputs=[doodle, char_name, theme, hero_name, voice, make_coloring,
642
- num_pages, custom_voice_audio],
643
  outputs=[book_display, status, audio_narration, pdf_download,
644
  story_info, image_info, trace_info,
645
  coloring_display, coloring_pdf_download],
 
284
  }
285
  .theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; }
286
 
 
 
 
 
 
 
287
 
288
  /* ============================== STATUS ============================== */
289
  .status-display textarea {
 
510
  elem_classes=["doodle-input"],
511
  )
512
  gr.HTML('<p class="card-eyebrow">2 &middot; the details</p>')
 
 
 
 
 
513
  hero_name = gr.Textbox(
514
  label="Hero's name in the story",
515
  placeholder="Ziggy",
 
520
  label="Story theme (pick one)",
521
  elem_classes=["field", "theme-pick"],
522
  )
 
 
 
 
 
523
  voice = gr.Radio(
524
  choices=VOICE_CHOICES, value=DEFAULT_VOICE,
525
  label="Narrator voice",
 
547
  value="Ready when you are! ✏️",
548
  )
549
  gr.Examples(
550
+ examples=[["assets/sample_doodle.jpg", "Ziggy", "brave adventure"]],
551
+ inputs=[doodle, hero_name, theme],
552
  label="Try an example",
553
  )
554
 
 
622
  if create_book_fn:
623
  make_btn.click(
624
  fn=create_book_fn,
625
+ inputs=[doodle, theme, hero_name, voice, make_coloring, custom_voice_audio],
 
626
  outputs=[book_display, status, audio_narration, pdf_download,
627
  story_info, image_info, trace_info,
628
  coloring_display, coloring_pdf_download],