Codex Claude Sonnet 4.6 commited on
Commit
ef904db
·
1 Parent(s): 3375ac7

Fix voice freeze, cover design, output order, story continuity

Browse files

- Voice cloning (my_voice) now runs sequentially AFTER FLUX images to avoid
ZeroGPU concurrency freeze: two concurrent @spaces.GPU threads caused the
browser to hang whenever the user recorded their voice
- Book cover now shows the first page illustration as a framed artwork above
the title, replacing the plain cream-background placeholder
- PDF downloads moved to the output card (below audio, above book pages) so
the output reads: audio → PDFs → storybook
- Removed 'Story spark' input — the 1B model couldn't reliably follow it
and it produced inconsistent results; input wiring updated (8 → 7 inputs)
- Story prompt rules tightened: explicit continuity requirement (each page
flows from the previous), distinctive hero trait anchoring, and required
lesson phrasing on the final page

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

Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +44 -29
  3. book_builder.py +10 -2
  4. ui/layout.py +20 -19
README.md CHANGED
@@ -78,7 +78,7 @@ DoodleBook uses a structured prompting system to guarantee quality:
78
  - **Story arc rules**: Pages 1–2 introduce the hero and challenge; middle pages build it; final pages resolve and teach a clear lesson.
79
  - **2–3 rich sentences per page**: Every page uses sensory detail — colours, sounds, textures, feelings — not just plot.
80
  - **10 diverse themes** covering kindness, imagination, friendship, courage, identity, and adventure.
81
- - **Optional story spark**: Parents/kids can give a hint ("loves dinosaurs") to steer the story without breaking the structure.
82
  - **Variable page count (6–10)**: More pages = longer story and longer narration.
83
 
84
  ---
 
78
  - **Story arc rules**: Pages 1–2 introduce the hero and challenge; middle pages build it; final pages resolve and teach a clear lesson.
79
  - **2–3 rich sentences per page**: Every page uses sensory detail — colours, sounds, textures, feelings — not just plot.
80
  - **10 diverse themes** covering kindness, imagination, friendship, courage, identity, and adventure.
81
+ - **Continuity rules**: Each page flows from the one before the story reads as one coherent arc, not isolated scenes.
82
  - **Variable page count (6–10)**: More pages = longer story and longer narration.
83
 
84
  ---
app.py CHANGED
@@ -419,21 +419,22 @@ Rules:
419
 
420
 
421
  def build_story_prompt(hero_name: str, theme: str, age: int,
422
- num_pages: int = 6, story_idea: str = "") -> str:
423
  mid = num_pages - 2
424
- spark_line = (f"\n- Story spark from the child: {story_idea.strip()}" if story_idea and story_idea.strip() else "")
425
  return f"""{FEW_SHOT_EXEMPLAR}
426
 
427
  Write a {num_pages}-page children's storybook for age {age} about {hero_name} with theme: {theme}.
428
 
429
  Rules:
430
  - {hero_name} must appear by name in EVERY page text. Every single page.
431
- - Keep all characters consistent — do NOT introduce random new characters after page 3.
432
  - Each page: 2–3 vivid, emotionally warm sentences a {age}-year-old can follow and feel.
433
- - Use sensory details — colours, sounds, textures, emotions — to bring each moment alive.
434
- - Pages 1–2 introduce {hero_name} and the challenge. Pages 3–{mid} deepen the adventure. Pages {mid+1}–{num_pages} resolve it and teach.
435
- - Page {num_pages} ends with a clear, warm lesson {hero_name} has learned and a feeling of pride or joy.
436
- - Scene describes exactly what would appear in ONE illustration.{spark_line}
 
 
437
  - Return ONLY valid JSON (no explanation, no markdown fences):
438
  """
439
 
@@ -583,11 +584,11 @@ def load_sample_book() -> str:
583
 
584
  @spaces.GPU(duration=90)
585
  def generate_story_gpu(hero_name: str, theme: str, age: int = 5,
586
- num_pages: int = 6, story_idea: str = "") -> dict:
587
  """Generate a story on ZeroGPU, falling back to a deterministic local story."""
588
  try:
589
  model, tok = load_story()
590
- prompt = build_story_prompt(hero_name, theme, age, num_pages, story_idea)
591
  inputs = tok.apply_chat_template(
592
  [{"role": "user", "content": prompt}],
593
  add_generation_prompt=True,
@@ -744,7 +745,7 @@ def generate_tts_gpu(text: str, voice: str = DEFAULT_VOICE,
744
 
745
  def create_book(doodle_image, character_name, theme, hero_name,
746
  voice=DEFAULT_VOICE, make_coloring=False,
747
- num_pages=6, story_idea=None, custom_voice_wav=None):
748
  """ZeroGPU book flow: story → images → narration → PDFs → coloring book,
749
  each a sequential @spaces.GPU call (ZeroGPU has one GPU per request)."""
750
  t_total = time.perf_counter()
@@ -777,8 +778,7 @@ def create_book(doodle_image, character_name, theme, hero_name,
777
 
778
  t_story = time.perf_counter()
779
  try:
780
- story = generate_story_gpu(hero_name, theme, num_pages=num_pages,
781
- story_idea=story_idea or "")
782
  except Exception as e:
783
  logger.error(f"Story generation failed: {e}")
784
  yield (
@@ -817,9 +817,6 @@ def create_book(doodle_image, character_name, theme, hero_name,
817
 
818
  full_text = f"{title}. {' '.join(page_texts)}"
819
 
820
- # ---- NARRATION starts NOW, in PARALLEL with the images (it only needs the
821
- # story text). The audio is surfaced the moment it's ready — usually
822
- # before the illustrations finish — so narration appears first. ----
823
  import threading
824
  voice_box = {}
825
  t_tts = time.perf_counter()
@@ -833,9 +830,6 @@ def create_book(doodle_image, character_name, theme, hero_name,
833
  except Exception as e:
834
  voice_box["err"] = e
835
 
836
- voice_thread = threading.Thread(target=_do_voice, daemon=True)
837
- voice_thread.start()
838
-
839
  def _audio_now():
840
  """Write the narration to a temp wav once it's ready; return its path."""
841
  if voice_box.get("bytes") and not voice_box.get("path"):
@@ -847,7 +841,18 @@ def create_book(doodle_image, character_name, theme, hero_name,
847
  logger.warning(f"writing audio failed: {e}")
848
  return voice_box.get("path")
849
 
850
- # ---- IMAGES (FLUX on ZeroGPU), surfacing the narration as soon as it lands ----
 
 
 
 
 
 
 
 
 
 
 
851
  img_bytes, engine, images = None, "sketch", None
852
  t_images = time.perf_counter()
853
  try:
@@ -855,9 +860,12 @@ def create_book(doodle_image, character_name, theme, hero_name,
855
  lambda: generate_images_gpu(char_desc, scenes, doodle_bytes, BASE_SEED),
856
  lambda s: (
857
  magic_loader_html("images", hero_name),
858
- f"{title} — illustrating… {s}s"
859
- + (" · narration ready ▶" if _audio_now() else " · recording narration…"),
860
- _audio_now(), _keep, story, "", json.dumps(trace_data, indent=2), _no, _keep,
 
 
 
861
  ),
862
  ):
863
  if kind == "hb":
@@ -882,13 +890,20 @@ def create_book(doodle_image, character_name, theme, hero_name,
882
 
883
  book_html = build_book_html(img_bytes, page_texts, title, engine)
884
 
885
- # ---- collect the parallel narration (usually already finished) ----
886
- while voice_thread.is_alive():
887
- voice_thread.join(timeout=4)
888
- if voice_thread.is_alive():
889
- yield (book_html, f"{title} finishing narration…",
890
- _audio_now(), _keep, story, "", json.dumps(trace_data, indent=2),
891
- _no, _keep)
 
 
 
 
 
 
 
892
  audio_path = _audio_now()
893
  if voice_box.get("err"):
894
  logger.warning(f"TTS failed: {voice_box['err']}")
 
419
 
420
 
421
  def build_story_prompt(hero_name: str, theme: str, age: int,
422
+ num_pages: int = 6) -> str:
423
  mid = num_pages - 2
 
424
  return f"""{FEW_SHOT_EXEMPLAR}
425
 
426
  Write a {num_pages}-page children's storybook for age {age} about {hero_name} with theme: {theme}.
427
 
428
  Rules:
429
  - {hero_name} must appear by name in EVERY page text. Every single page.
430
+ - Keep all characters consistent — do NOT introduce new characters after page 3.
431
  - Each page: 2–3 vivid, emotionally warm sentences a {age}-year-old can follow and feel.
432
+ - Use sensory details — colours, sounds, textures, feelings — to bring each moment alive.
433
+ - The story is ONE continuous narrative: each page flows naturally from the one before it. Page N+1 begins where page N ended.
434
+ - Pages 1–2 introduce {hero_name} and the problem clearly. Pages 3–{mid} build the challenge step by step. Pages {mid+1}–{num_pages} resolve it and teach a lesson.
435
+ - Page {num_pages} must end with a clear, warm moral using "{hero_name} had learned" or "{hero_name} now understood" or "{hero_name} discovered that". Include a feeling of pride or joy.
436
+ - Give {hero_name} ONE distinctive physical detail on page 1 and refer to it at least once more later.
437
+ - Scene describes exactly what would appear in ONE simple illustration — one moment, one setting, one emotion.
438
  - Return ONLY valid JSON (no explanation, no markdown fences):
439
  """
440
 
 
584
 
585
  @spaces.GPU(duration=90)
586
  def generate_story_gpu(hero_name: str, theme: str, age: int = 5,
587
+ num_pages: int = 6) -> dict:
588
  """Generate a story on ZeroGPU, falling back to a deterministic local story."""
589
  try:
590
  model, tok = load_story()
591
+ prompt = build_story_prompt(hero_name, theme, age, num_pages)
592
  inputs = tok.apply_chat_template(
593
  [{"role": "user", "content": prompt}],
594
  add_generation_prompt=True,
 
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()
 
778
 
779
  t_story = time.perf_counter()
780
  try:
781
+ story = generate_story_gpu(hero_name, theme, num_pages=num_pages)
 
782
  except Exception as e:
783
  logger.error(f"Story generation failed: {e}")
784
  yield (
 
817
 
818
  full_text = f"{title}. {' '.join(page_texts)}"
819
 
 
 
 
820
  import threading
821
  voice_box = {}
822
  t_tts = time.perf_counter()
 
830
  except Exception as e:
831
  voice_box["err"] = e
832
 
 
 
 
833
  def _audio_now():
834
  """Write the narration to a temp wav once it's ready; return its path."""
835
  if voice_box.get("bytes") and not voice_box.get("path"):
 
841
  logger.warning(f"writing audio failed: {e}")
842
  return voice_box.get("path")
843
 
844
+ # Preset voices are fast (~20-40s): run narration parallel with FLUX images so
845
+ # audio appears first. Voice cloning is slow (~90-120s) and calling two
846
+ # @spaces.GPU functions concurrently from threads causes ZeroGPU to freeze the
847
+ # browser — so for "my_voice" we run TTS sequentially AFTER images complete.
848
+ run_tts_parallel = (voice != "my_voice")
849
+ if run_tts_parallel:
850
+ voice_thread = threading.Thread(target=_do_voice, daemon=True)
851
+ voice_thread.start()
852
+ else:
853
+ voice_thread = None
854
+
855
+ # ---- IMAGES (FLUX on ZeroGPU) ----
856
  img_bytes, engine, images = None, "sketch", None
857
  t_images = time.perf_counter()
858
  try:
 
860
  lambda: generate_images_gpu(char_desc, scenes, doodle_bytes, BASE_SEED),
861
  lambda s: (
862
  magic_loader_html("images", hero_name),
863
+ f"{title} — illustrating… {s}s" + (
864
+ (" · narration ready ▶" if _audio_now() else " · recording narration…")
865
+ if run_tts_parallel else " · voice cloning after illustrations"
866
+ ),
867
+ _audio_now() if run_tts_parallel else None,
868
+ _keep, story, "", json.dumps(trace_data, indent=2), _no, _keep,
869
  ),
870
  ):
871
  if kind == "hb":
 
890
 
891
  book_html = build_book_html(img_bytes, page_texts, title, engine)
892
 
893
+ # ---- collect / run narration ----
894
+ if not run_tts_parallel:
895
+ # Voice cloning runs sequentially now that FLUX images are done
896
+ yield (book_html, f"{title} — cloning your voice…",
897
+ None, _keep, story, "", json.dumps(trace_data, indent=2), _no, _keep)
898
+ _do_voice()
899
+ else:
900
+ # Preset voice ran parallel — wait for it to finish (usually already done)
901
+ while voice_thread.is_alive():
902
+ voice_thread.join(timeout=4)
903
+ if voice_thread.is_alive():
904
+ yield (book_html, f"{title} — finishing narration…",
905
+ _audio_now(), _keep, story, "", json.dumps(trace_data, indent=2),
906
+ _no, _keep)
907
  audio_path = _audio_now()
908
  if voice_box.get("err"):
909
  logger.warning(f"TTS failed: {voice_box['err']}")
book_builder.py CHANGED
@@ -42,7 +42,8 @@ PAGE_HTML = """
42
 
43
  COVER_HTML = """
44
  <div class="book-cover">
45
- <p class="cover-kicker">a DoodleBook story</p>
 
46
  <h2 class="cover-title">{title}</h2>
47
  {badge}
48
  </div>
@@ -62,7 +63,14 @@ def build_book_html(
62
  ) -> str:
63
  """Build storybook HTML from images and texts."""
64
  badge = ENGINE_BADGES.get(engine, "")
65
- cover = COVER_HTML.format(title=title, badge=badge)
 
 
 
 
 
 
 
66
 
67
  pages_html = ""
68
  for i, (img_bytes, text) in enumerate(zip(pages_images, pages_texts)):
 
42
 
43
  COVER_HTML = """
44
  <div class="book-cover">
45
+ <p class="cover-kicker">a DoodleBook story</p>
46
+ {cover_art}
47
  <h2 class="cover-title">{title}</h2>
48
  {badge}
49
  </div>
 
63
  ) -> str:
64
  """Build storybook HTML from images and texts."""
65
  badge = ENGINE_BADGES.get(engine, "")
66
+ cover_art = ""
67
+ if pages_images:
68
+ cover_art = (
69
+ f'<div class="cover-art">'
70
+ f'<img src="{_img_src_for_bytes(pages_images[0])}" alt="Cover illustration"/>'
71
+ f'</div>'
72
+ )
73
+ cover = COVER_HTML.format(title=title, badge=badge, cover_art=cover_art)
74
 
75
  pages_html = ""
76
  for i, (img_bytes, text) in enumerate(zip(pages_images, pages_texts)):
ui/layout.py CHANGED
@@ -321,15 +321,22 @@ body, gradio-app {
321
  text-shadow: 2px 2px 0 var(--crayon-sun);
322
  }
323
  .book-cover {
324
- text-align: center; padding: 34px 22px 30px; margin: 10px auto 26px;
325
- background: radial-gradient(circle at 30% 20%, rgba(244,198,74,.25), transparent 55%), #fff8e6;
 
326
  border-radius: 18px; box-shadow: 0 12px 28px rgba(46,42,38,.16); transform: rotate(-1deg);
327
  }
328
- .book-cover .cover-kicker { font-family: 'Caveat', cursive; font-size: 22px; color: var(--crayon-berry); }
 
 
 
 
 
 
329
  .book-cover .cover-title {
330
  font-family: 'Gaegu', cursive; font-weight: 700;
331
- font-size: clamp(34px, 5vw, 52px); color: var(--ink); margin: 4px 0 2px;
332
- text-shadow: 2px 2px 0 var(--crayon-sun);
333
  }
334
  .book-page {
335
  position: relative; background: #fffdf6; border-radius: 14px;
@@ -524,12 +531,6 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
524
  label="Story theme (pick one)",
525
  elem_classes=["field", "theme-pick"],
526
  )
527
- story_idea = gr.Textbox(
528
- label="Story spark (optional)",
529
- placeholder="e.g. 'loves dinosaurs', 'wants to fly', 'learns to share'",
530
- max_lines=2,
531
- elem_classes=["field"],
532
- )
533
  num_pages = gr.Slider(
534
  minimum=6, maximum=10, step=1, value=6,
535
  label="Number of pages",
@@ -561,13 +562,6 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
561
  elem_classes=["status-display"],
562
  value="Ready when you are! ✏️",
563
  )
564
- with gr.Row(elem_classes=["download-row"]):
565
- pdf_download = gr.DownloadButton(
566
- "⬇ Story PDF", visible=False, elem_classes=["btn-pdf"],
567
- )
568
- coloring_pdf_download = gr.DownloadButton(
569
- "⬇ Coloring PDF", visible=False, elem_classes=["btn-pdf"],
570
- )
571
  gr.Examples(
572
  examples=[["assets/sample_doodle.jpg", "Ziggy", "Ziggy", "brave adventure"]],
573
  inputs=[doodle, char_name, hero_name, theme],
@@ -581,6 +575,13 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
581
  autoplay=False,
582
  elem_classes=["audio-player"],
583
  )
 
 
 
 
 
 
 
584
  book_display = gr.HTML(
585
  elem_classes=["book-stage"],
586
  value="""
@@ -638,7 +639,7 @@ def create_layout(load_sample_fn=None, create_book_fn=None):
638
  make_btn.click(
639
  fn=create_book_fn,
640
  inputs=[doodle, char_name, theme, hero_name, voice, make_coloring,
641
- num_pages, story_idea, custom_voice_audio],
642
  outputs=[book_display, status, audio_narration, pdf_download,
643
  story_info, image_info, trace_info,
644
  coloring_display, coloring_pdf_download],
 
321
  text-shadow: 2px 2px 0 var(--crayon-sun);
322
  }
323
  .book-cover {
324
+ text-align: center; padding: 28px 22px 26px; margin: 10px auto 26px;
325
+ background: radial-gradient(circle at 30% 20%, rgba(244,198,74,.25), transparent 55%),
326
+ radial-gradient(circle at 75% 80%, rgba(214,81,122,.12), transparent 50%), #fff8e6;
327
  border-radius: 18px; box-shadow: 0 12px 28px rgba(46,42,38,.16); transform: rotate(-1deg);
328
  }
329
+ .book-cover .cover-kicker { font-family: 'Caveat', cursive; font-size: 22px; color: var(--crayon-berry); margin-bottom: 10px; }
330
+ .cover-art {
331
+ margin: 14px auto 16px; max-width: 420px; border-radius: 14px; overflow: hidden;
332
+ box-shadow: 0 6px 18px rgba(46,42,38,.18), 0 0 0 4px var(--crayon-sun), 0 0 0 7px var(--ink);
333
+ transform: rotate(0.8deg);
334
+ }
335
+ .cover-art img { display: block; width: 100%; }
336
  .book-cover .cover-title {
337
  font-family: 'Gaegu', cursive; font-weight: 700;
338
+ font-size: clamp(28px, 4.5vw, 48px); color: var(--ink); margin: 12px 0 4px;
339
+ text-shadow: 3px 3px 0 var(--crayon-sun), -1px -1px 0 rgba(255,255,255,.6);
340
  }
341
  .book-page {
342
  position: relative; background: #fffdf6; border-radius: 14px;
 
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",
 
562
  elem_classes=["status-display"],
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],
 
575
  autoplay=False,
576
  elem_classes=["audio-player"],
577
  )
578
+ with gr.Row(elem_classes=["download-row"]):
579
+ pdf_download = gr.DownloadButton(
580
+ "⬇ Story PDF", visible=False, elem_classes=["btn-pdf"],
581
+ )
582
+ coloring_pdf_download = gr.DownloadButton(
583
+ "⬇ Coloring PDF", visible=False, elem_classes=["btn-pdf"],
584
+ )
585
  book_display = gr.HTML(
586
  elem_classes=["book-stage"],
587
  value="""
 
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],