sush0401 commited on
Commit
80f1887
·
verified ·
1 Parent(s): f029f5f

Crayon art style, parallel/early narration, reveal PDF download buttons, coloring = img2img from color pages

Browse files
Files changed (1) hide show
  1. app.py +77 -78
app.py CHANGED
@@ -106,8 +106,9 @@ if ON_ZEROGPU:
106
  logger.exception(f"Module-level load failed for {_name}")
107
 
108
  COLOR_ART_STYLE = (
109
- "children's crayon storybook illustration, bold black outlines, "
110
- "flat bright colors, simple shapes"
 
111
  )
112
  COLOR_PAGE_SUFFIX = "full colorful background scene, the character clearly visible."
113
  LINE_ART_STYLE = (
@@ -381,52 +382,28 @@ def generate_images_gpu(
381
 
382
 
383
  @spaces.GPU(duration=150)
384
- def generate_coloring_images_gpu(
385
- character_desc: str,
386
- scenes: list,
387
- doodle_bytes: bytes = None,
388
- seed: int = 42,
389
- ) -> list:
390
- """Generate coloring pages directly with FLUX as line art (no tracing)."""
391
  import io
392
  from PIL import Image
393
 
394
  pipe = load_flux()
395
- num_steps, guidance = 6, 1.0
396
-
397
- canonical = None
398
- if doodle_bytes:
399
- try:
400
- ref = Image.open(io.BytesIO(doodle_bytes)).convert("RGB")
401
- canonical = pipe(
402
- prompt=(f"Turn this child's drawing into a clean, friendly, full-body cartoon "
403
- f"character for a children's coloring book. Keep the EXACT same creature, "
404
- f"face, and features as the drawing. {LINE_ART_STYLE}, "
405
- f"plain white background, full character visible, centered."),
406
- image=ref, height=768, width=768, guidance_scale=guidance,
407
- num_inference_steps=num_steps,
408
- generator=torch.Generator("cuda").manual_seed(seed),
409
- ).images[0]
410
- logger.info("Line-art canonical character built from doodle")
411
- except Exception as e:
412
- logger.warning(f"Line-art canonical build failed ({e}); text2img fallback")
413
- canonical = None
414
-
415
- images = []
416
- for i, scene in enumerate(scenes):
417
- if canonical is not None:
418
- prompt = f"The same character. {scene}. {LINE_ART_STYLE}, {LINE_ART_SUFFIX}"
419
- kw = dict(image=canonical, prompt=prompt)
420
- else:
421
- prompt = (f"{character_desc}. Scene: {scene}. {LINE_ART_STYLE}, "
422
- f"white background, centered, full character visible")
423
- kw = dict(prompt=prompt)
424
- kw.update(height=768, width=768, guidance_scale=guidance,
425
- num_inference_steps=num_steps,
426
- generator=torch.Generator("cuda").manual_seed(seed + i + 101))
427
- images.append(pipe(**kw).images[0])
428
- logger.info(f"Generated coloring page {i+1}/{len(scenes)}")
429
- return images
430
 
431
 
432
  @spaces.GPU(duration=120)
@@ -546,16 +523,44 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
546
 
547
  full_text = f"{title}. {' '.join(page_texts)}"
548
 
549
- # ---- IMAGES (FLUX on ZeroGPU) ----
550
- img_bytes, engine = None, "sketch"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  t_images = time.perf_counter()
552
  try:
553
  for kind, payload in _with_heartbeat(
554
  lambda: generate_images_gpu(char_desc, scenes, doodle_bytes, BASE_SEED),
555
  lambda s: (
556
  magic_loader_html("images", hero_name),
557
- f"{title} — illustrating on ZeroGPU… {s}s",
558
- None, _keep, story, "", json.dumps(trace_data, indent=2), _no, _keep,
 
559
  ),
560
  ):
561
  if kind == "hb":
@@ -580,28 +585,17 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
580
 
581
  book_html = build_book_html(img_bytes, page_texts, title, engine)
582
 
583
- # ---- NARRATION (VoxCPM2 on ZeroGPU) sequential: one GPU per request ----
584
- audio_path = None
585
- t_tts = time.perf_counter()
586
- try:
587
- for kind, payload in _with_heartbeat(
588
- lambda: generate_tts_gpu(full_text, voice),
589
- lambda s: (
590
- book_html,
591
- f"{title} — recording the narration… {s}s",
592
- None, _keep, story, "", json.dumps(trace_data, indent=2), _no, _keep,
593
- ),
594
- ):
595
- if kind == "hb":
596
- yield payload
597
- else:
598
- voice_bytes = payload
599
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
600
- tmp.write(voice_bytes)
601
- audio_path = tmp.name
602
- except Exception as e:
603
- logger.exception("TTS failed")
604
- trace_data["tts_error"] = repr(e)
605
  trace_data["tts_sec"] = round(time.perf_counter() - t_tts, 2)
606
 
607
  pdf_path = None
@@ -620,7 +614,7 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
620
  try:
621
  from services.coloring import _crispen
622
  for kind, payload in _with_heartbeat(
623
- lambda: generate_coloring_images_gpu(char_desc, scenes, doodle_bytes, BASE_SEED),
624
  lambda s: (
625
  book_html,
626
  f"{title} — building coloring book… {s}s",
@@ -649,25 +643,30 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
649
  trace_data["coloring_book"] = True
650
  trace_data["coloring_engine"] = "flux-direct-lineart"
651
  except Exception as e:
652
- logger.warning(f"Direct FLUX coloring book failed ({e}); using traced fallback")
 
653
  try:
654
- from services.coloring import derive_coloring_pages
655
- outlines = derive_coloring_pages(img_bytes)
656
  coloring_html = build_coloring_html(outlines, page_texts, title)
657
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
658
  coloring_pdf_path = export_coloring_pdf(outlines, page_texts, title, tmp.name)
659
  trace_data["coloring_book"] = True
660
- trace_data["coloring_engine"] = "trace-fallback"
661
  except Exception as e2:
662
- logger.warning(f"Coloring book fallback failed: {e2}")
 
663
  trace_data["coloring_sec"] = round(time.perf_counter() - t_coloring, 2)
664
 
665
  trace_data["completed"] = True
666
  trace_data["pages_generated"] = len(img_bytes)
667
  trace_data["total_sec"] = round(time.perf_counter() - t_total, 2)
668
 
669
- pdf_update = gr.update(value=pdf_path) if pdf_path else _keep
670
- coloring_pdf_update = gr.update(value=coloring_pdf_path) if coloring_pdf_path else _keep
 
 
 
671
  coloring_display_update = (gr.update(visible=True, value=coloring_html) if coloring_html
672
  else _no)
673
 
 
106
  logger.exception(f"Module-level load failed for {_name}")
107
 
108
  COLOR_ART_STYLE = (
109
+ "hand-drawn crayon children's storybook illustration, soft waxy crayon "
110
+ "texture and visible crayon strokes, warm colorful crayon shading, "
111
+ "simple friendly shapes, looks drawn by hand with crayons"
112
  )
113
  COLOR_PAGE_SUFFIX = "full colorful background scene, the character clearly visible."
114
  LINE_ART_STYLE = (
 
382
 
383
 
384
  @spaces.GPU(duration=150)
385
+ def generate_coloring_images_gpu(color_pngs: list, seed: int = 7) -> list:
386
+ """Coloring pages = FLUX redraws each finished COLOR page as clean line art
387
+ (img2img). This MATCHES the storybook composition and avoids the speckly
388
+ look of tracing crayon texture. Caller crispens the result to black-on-white."""
 
 
 
389
  import io
390
  from PIL import Image
391
 
392
  pipe = load_flux()
393
+ prompt = f"{LINE_ART_STYLE}, {LINE_ART_SUFFIX}"
394
+ outs = []
395
+ for i, png in enumerate(color_pngs):
396
+ ref = Image.open(io.BytesIO(png)).convert("RGB")
397
+ base = dict(prompt=prompt, image=ref, height=768, width=768,
398
+ guidance_scale=1.0, num_inference_steps=6,
399
+ generator=torch.Generator("cuda").manual_seed(seed + i))
400
+ try: # strength may not be accepted
401
+ img = pipe(**base, strength=0.85).images[0]
402
+ except TypeError:
403
+ img = pipe(**base).images[0]
404
+ outs.append(img)
405
+ logger.info(f"Generated coloring page {i+1}/{len(color_pngs)}")
406
+ return outs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
 
409
  @spaces.GPU(duration=120)
 
523
 
524
  full_text = f"{title}. {' '.join(page_texts)}"
525
 
526
+ # ---- NARRATION starts NOW, in PARALLEL with the images (it only needs the
527
+ # story text). The audio is surfaced the moment it's ready — usually
528
+ # before the illustrations finish — so narration appears first. ----
529
+ import threading
530
+ voice_box = {}
531
+ t_tts = time.perf_counter()
532
+
533
+ def _do_voice():
534
+ try:
535
+ voice_box["bytes"] = generate_tts_gpu(full_text, voice)
536
+ except Exception as e:
537
+ voice_box["err"] = e
538
+
539
+ voice_thread = threading.Thread(target=_do_voice, daemon=True)
540
+ voice_thread.start()
541
+
542
+ def _audio_now():
543
+ """Write the narration to a temp wav once it's ready; return its path."""
544
+ if voice_box.get("bytes") and not voice_box.get("path"):
545
+ try:
546
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
547
+ tmp.write(voice_box["bytes"])
548
+ voice_box["path"] = tmp.name
549
+ except Exception as e:
550
+ logger.warning(f"writing audio failed: {e}")
551
+ return voice_box.get("path")
552
+
553
+ # ---- IMAGES (FLUX on ZeroGPU), surfacing the narration as soon as it lands ----
554
+ img_bytes, engine, images = None, "sketch", None
555
  t_images = time.perf_counter()
556
  try:
557
  for kind, payload in _with_heartbeat(
558
  lambda: generate_images_gpu(char_desc, scenes, doodle_bytes, BASE_SEED),
559
  lambda s: (
560
  magic_loader_html("images", hero_name),
561
+ f"{title} — illustrating… {s}s"
562
+ + (" · narration ready " if _audio_now() else " · recording narration…"),
563
+ _audio_now(), _keep, story, "", json.dumps(trace_data, indent=2), _no, _keep,
564
  ),
565
  ):
566
  if kind == "hb":
 
585
 
586
  book_html = build_book_html(img_bytes, page_texts, title, engine)
587
 
588
+ # ---- collect the parallel narration (usually already finished) ----
589
+ while voice_thread.is_alive():
590
+ voice_thread.join(timeout=4)
591
+ if voice_thread.is_alive():
592
+ yield (book_html, f"{title} finishing narration…",
593
+ _audio_now(), _keep, story, "", json.dumps(trace_data, indent=2),
594
+ _no, _keep)
595
+ audio_path = _audio_now()
596
+ if voice_box.get("err"):
597
+ logger.warning(f"TTS failed: {voice_box['err']}")
598
+ trace_data["tts_error"] = repr(voice_box["err"])
 
 
 
 
 
 
 
 
 
 
 
599
  trace_data["tts_sec"] = round(time.perf_counter() - t_tts, 2)
600
 
601
  pdf_path = None
 
614
  try:
615
  from services.coloring import _crispen
616
  for kind, payload in _with_heartbeat(
617
+ lambda: generate_coloring_images_gpu(img_bytes, 7),
618
  lambda s: (
619
  book_html,
620
  f"{title} — building coloring book… {s}s",
 
643
  trace_data["coloring_book"] = True
644
  trace_data["coloring_engine"] = "flux-direct-lineart"
645
  except Exception as e:
646
+ logger.exception("FLUX line-art coloring failed; using local trace fallback")
647
+ trace_data["coloring_error"] = repr(e)
648
  try:
649
+ from services.coloring import _to_line_art_opencv
650
+ outlines = [_to_line_art_opencv(b) for b in img_bytes]
651
  coloring_html = build_coloring_html(outlines, page_texts, title)
652
  with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
653
  coloring_pdf_path = export_coloring_pdf(outlines, page_texts, title, tmp.name)
654
  trace_data["coloring_book"] = True
655
+ trace_data["coloring_engine"] = "opencv-trace-fallback"
656
  except Exception as e2:
657
+ logger.exception("Coloring fallback also failed")
658
+ trace_data["coloring_error2"] = repr(e2)
659
  trace_data["coloring_sec"] = round(time.perf_counter() - t_coloring, 2)
660
 
661
  trace_data["completed"] = True
662
  trace_data["pages_generated"] = len(img_bytes)
663
  trace_data["total_sec"] = round(time.perf_counter() - t_total, 2)
664
 
665
+ # Reveal the download buttons (they start visible=False) value alone left
666
+ # them hidden, which is why there was "no download option" (incl. on mobile).
667
+ pdf_update = gr.update(value=pdf_path, visible=True) if pdf_path else _keep
668
+ coloring_pdf_update = (gr.update(value=coloring_pdf_path, visible=True)
669
+ if coloring_pdf_path else _keep)
670
  coloring_display_update = (gr.update(visible=True, value=coloring_html) if coloring_html
671
  else _no)
672