All articles

AI Agents7 min read

Building an AI Storytelling Platform for Children with Gemini, Streamlit, and Panel

Personalized story generation with consistent narrative arcs, multi-turn branching, age-appropriate content controls, and two UI variants deployed on Cloud Run

  • Gemini 2.5 Flash
  • Streamlit
  • Panel
  • Cloud Run
  • Python
  • Docker
  • Content Safety

The hard part of AI storytelling for children isn't generation — it's coherence. A child who tells Gemini their character's name is "Luna" and she has a purple unicorn companion expects Luna and the unicorn to appear consistently across all five chapters. The system prompt alone can't guarantee this for longer stories. The solution is a two-stage approach: plan the story structure first, then generate each chapter with the plan as a hard constraint.

Two-Stage Generation Architecture

User Input
  ├── Character: name, appearance, personality traits
  ├── Setting: enchanted forest | underwater city | space station | historical era
  ├── Theme: adventure | friendship | problem-solving | environmental | magical
  └── Age group: toddler (3-5) | early reader (6-8) | middle grade (9-12)
         ↓
Stage 1: Story Planner (Gemini 2.5 Flash)
  ├── Generates 5-7 story beats
  ├── Defines character voices and motivations
  ├── Plans setup → conflict → resolution arc
  └── Outputs structured JSON outline
         ↓
Stage 2: Chapter Generator (Gemini 2.5 Flash, streaming)
  ├── Receives: outline + previous chapters + user choices
  ├── Maintains character voice consistency
  ├── Calibrates vocabulary to age group
  └── Ends with: cliffhanger (ch 1-4) or resolution (ch 5)
         ↓
UI Presentation
  ├── Streamlit: linear chapter display, "next chapter" button
  └── Panel: choice cards for interactive branching

Story Planning Prompt

python
1import json
2from vertexai.generative_models import GenerativeModel
3
4model = GenerativeModel("gemini-2.5-flash")
5
6def plan_story(
7    character_name: str,
8    character_traits: list[str],
9    setting: str,
10    theme: str,
11    age_group: str,
12) -> dict:
13    prompt = f"""Create a story plan for a children's story.
14
15Character: {character_name} — traits: {', '.join(character_traits)}
16Setting: {setting}
17Theme: {theme}
18Age group: {age_group}
19
20Create a 5-chapter story plan as JSON:
21{{
22  "title": "Story title",
23  "characters": {{
24    "{character_name}": {{
25      "voice": "how they speak",
26      "motivation": "what they want",
27      "companions": ["any companion names and descriptions"]
28    }}
29  }},
30  "chapters": [
31    {{
32      "chapter": 1,
33      "beat": "Setup — introduce character and world",
34      "key_event": "specific event that happens",
35      "ends_with": "cliffhanger or choice"
36    }}
37    // ... chapters 2-5
38  ],
39  "resolution": "how the story ends positively"
40}}
41
42Ensure the theme of {theme} is woven throughout.
43The resolution must be positive and age-appropriate for {age_group}."""
44
45    response = model.generate_content(
46        prompt,
47        generation_config={"temperature": 0.7, "response_mime_type": "application/json"},
48    )
49    return json.loads(response.text)

Age-Calibrated Chapter Generation

python
1AGE_PROFILES = {
2    "toddler": {
3        "word_count": "150-250 words",
4        "vocabulary": "simple words only, no complex sentences",
5        "style": "repetitive patterns are good, short sentences, lots of action",
6        "avoid": "suspense, fear, anything scary"
7    },
8    "early_reader": {
9        "word_count": "300-500 words",
10        "vocabulary": "grade 1-3 vocabulary, occasional new word with context",
11        "style": "clear narrative, dialogue, simple emotional beats",
12        "avoid": "violence, mature themes, complex subplots"
13    },
14    "middle_grade": {
15        "word_count": "500-800 words",
16        "vocabulary": "varied vocabulary, metaphors welcome",
17        "style": "character development, moral dilemmas, humor",
18        "avoid": "adult themes, graphic content"
19    }
20}
21
22def generate_chapter(
23    plan: dict,
24    chapter_num: int,
25    previous_chapters: list[str],
26    user_choice: str | None,
27    age_group: str,
28) -> str:
29    profile = AGE_PROFILES[age_group]
30    chapter_plan = plan["chapters"][chapter_num - 1]
31
32    # Inject all character details from the plan — prevents drift
33    character_context = json.dumps(plan["characters"], indent=2)
34
35    prompt = f"""Write chapter {chapter_num} of a children's story.
36
37STORY PLAN (follow this exactly):
38{json.dumps(chapter_plan, indent=2)}
39
40CHARACTERS (maintain these consistently):
41{character_context}
42
43AGE GROUP: {age_group}
44- Length: {profile['word_count']}
45- Vocabulary: {profile['vocabulary']}
46- Style: {profile['style']}
47- Avoid: {profile['avoid']}
48
49PREVIOUS STORY (maintain continuity):
50{chr(10).join(f"Chapter {i+1}: {ch}" for i, ch in enumerate(previous_chapters))}
51
52{f"USER CHOSE: {user_choice}" if user_choice else ""}
53
54Write the chapter in flowing prose. End with the planned cliffhanger or choice."""
55
56    # Stream for responsive UI
57    response_stream = model.generate_content(prompt, stream=True)
58    return "".join(chunk.text for chunk in response_stream if chunk.text)

Content Safety Configuration

python
1from vertexai.generative_models import SafetySetting, HarmCategory, HarmBlockThreshold
2
3# Maximum safety settings for children's content
4CHILD_SAFETY_SETTINGS = [
5    SafetySetting(
6        category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
7        threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
8    ),
9    SafetySetting(
10        category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
11        threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
12    ),
13    SafetySetting(
14        category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
15        threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
16    ),
17    SafetySetting(
18        category=HarmCategory.HARM_CATEGORY_HARASSMENT,
19        threshold=HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
20    ),
21]
22
23model_with_safety = GenerativeModel(
24    "gemini-2.5-flash",
25    safety_settings=CHILD_SAFETY_SETTINGS,
26    system_instruction="""You write children's stories. You must:
27- Always portray kindness, courage, and inclusive values
28- Never include violence, fear, or adult themes
29- Give every story a positive resolution
30- Make heroes solve problems through cleverness and cooperation, not force"""
31)

Streamlit vs. Panel — Which to Use

  • Streamlit: deploy in minutes, shareable URL, great for demos and single-user scenarios. Chapter display is linear — click "Next Chapter" to continue. Limited layout control.
  • Panel: richer widget system, interactive choice cards for story branching, better for complex layouts. More setup required, runs on Bokeh server.
  • Both deploy on Cloud Run with the same Docker image — environment variable selects which app entry point to use.
Chapters per story with consistent narrative arc
5
Age groups supported (toddler, early reader, middle grade)
3
UI variants (Streamlit + Panel)
2
Harm blocking threshold for all 4 safety categories
MAX

Request a discovery call

30 minutesGoogle MeetFree