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>
- README.md +1 -1
- app.py +44 -29
- book_builder.py +10 -2
- ui/layout.py +20 -19
|
@@ -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 |
-
- **
|
| 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 |
---
|
|
@@ -419,21 +419,22 @@ Rules:
|
|
| 419 |
|
| 420 |
|
| 421 |
def build_story_prompt(hero_name: str, theme: str, age: int,
|
| 422 |
-
num_pages: int = 6
|
| 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
|
| 432 |
- Each page: 2–3 vivid, emotionally warm sentences a {age}-year-old can follow and feel.
|
| 433 |
-
- Use sensory details — colours, sounds, textures,
|
| 434 |
-
-
|
| 435 |
-
-
|
| 436 |
-
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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,
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 860 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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']}")
|
|
@@ -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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)):
|
|
@@ -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:
|
| 325 |
-
background: radial-gradient(circle at 30% 20%, rgba(244,198,74,.25), transparent 55%),
|
|
|
|
| 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(
|
| 332 |
-
text-shadow:
|
| 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,
|
| 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],
|