# Project export: Lullow

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: When you can't be there at bedtime, your love and values still can. Lullow turns a child's big feelings into calming, personalized stories — with a soft lamp that glows along with the tale.
- Devpost: https://devpost.com/software/lullow
- GitHub: https://github.com/eugenegujing/Lullow
- Video: https://www.youtube.com/embed/FxOtw63PPOA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Claude Opus 4.8 (17 commits), eugenegujing (13 commits), gualle (5 commits), srinivas (4 commits), Thinh Nguyen (2 commits)

## Devpost submission (written by the team)

### Inspiration

Tonight, somewhere, a four-year-old is lying awake in the dark — a little scared, missing a parent who is a thousand miles away on a factory floor, working a night shift, or simply too exhausted to tell one more story. So much of what we later call anxiety, insecurity, the "difficult kid," even the struggles we carry into adulthood, traces back to one quiet thing: the love, the secure attachment, and the gentle guidance a child did — or did not — receive at the very beginning. And that love has never been handed out fairly. The science is clear: Attachment theory (Bowlby & Ainsworth): a child's early bond with a responsive caregiver hardens into an attachment pattern that predicts lifelong emotional regulation and resilience. Bedtime — darkness plus separation — is when that attachment system lights up brightest. The landmark CDC–Kaiser ACE study found childhood adversity (including emotional neglect and parental absence) raises the odds of adult depression, addiction, and suicide attempts 4–12×. And the gap falls hardest on the families with the least flexibility: China's tens of millions of left-behind children (50–80% higher risk of suicidal ideation), the 52% of US families where both parents work full-time, and the 1 in 12 U.S. children who lose a parent or sibling by age 18. Being tucked in should be a right. Too often, it's a privilege. Lullow is our swing at closing that gap — from the cradle.

### What it does

Lullow is a voice-first, parent-controlled bedtime companion: It sits with the child and settles the feeling. The child speaks how they feel — "I'm scared of the dark and I miss mom." Lullow answers in a warm voice, reflects the feeling gently, and turns it into a personalized, low-stimulation story that ends in a calming goodnight. It carries the parent's own love, values, and lessons into the room — "I want my daughter know that no one should touch her private part, and she should tell mom if that happens.Make a story about this" even on the nights the parent can't be there. This is the heart of Lullow. A physical mood lamp glows along with the story. Each scene's atmosphere maps to a bedtime-safe color — warm gold for calm, deep purple for fear, gold for victory — so the AI story becomes something you can feel in the room. It grows with the child, remembering characters, fears, and the lessons a parent has taught, all inside parent-set safety boundaries. Crucially, Lullow is the parent's messenger, not a replacement — the attachment, the value, and the love stay with the parent.

### How we built it

Claude (Anthropic) — the reasoning layer: emotion extraction, safe story planning, personalized story writing, and per-scene mood tagging. Deepgram — voice-first: speech-to-text for the child, soft Aura text-to-speech narration. Gemini — image-first picture-book pages with a locked character reference for consistency. Pika — gentle low-motion animation of each page. Redis — family memory, story-world continuity, and semantic caching. Govee smart light (v2 API) — the physical mood lamp; the backend maps each scene's mood to a color and drives the lamp live as the story plays.

### Challenges we ran into

A hidden voice deadlock. A busy state guard silently aborted the check-in right after speech-to-text — STT returned 200 but the UI froze forever. It only surfaced when we tested with a real microphone (everyone had been testing by typing). Govee's API. Our newer H612F strip wasn't supported by the legacy v1 API; we migrated to the v2 capability-based API. RGBWW strips also wash out color, so we hand-tuned a bedtime-safe palette. Live latency. Running everything for real (several sequential Claude calls plus per-scene image and TTS) stacks up — we learned exactly where the pipeline needs parallelizing. Hardware, the night of. ESP32s sold out and sockets had the wrong base — we pivoted fast. And we merged four parallel workstreams onto one backend.

### Accomplishments we're proud of

A real, physical mood lamp that changes color live with the AI story's emotional arc. A genuinely child-safe design: a danger-escalation gate, no diagnosis, no secrecy, parent-in-the-loop, and full mock fallbacks. A product with a soul — it doesn't try to replace parents; it carries their value into the room.

### What we learned

The hardest, most meaningful problems — childhood emotional development, the fairness of love — are worth swinging at, even in a weekend. Voice plus ambient light makes an AI feel present in a way a screen never does. Real-device testing surfaces bugs that mocks never will.

### What's next

Let parents leave lessons and a goodnight in their own recorded (or cloned) voice. Parallelize and stream the pipeline for instant, gapless playback. Pilot with night-shift and migrant-worker families, and measure children's emotional-literacy growth over time.

## README (from the GitHub repository)

<div align="center">

<img src="assets/banner.png" alt="Lullow — Light & Lore" width="100%" />

# Lullow 🌙

**A gentle glow for big feelings at bedtime.**

</div>

Lullow is a **voice-first bedtime comfort companion for children ages 3–8**. It
listens to a child's nighttime feelings, turns them into a gentle personalized
lullaby story, narrates it in a calming voice while a **physical mood lamp
follows each scene**, leaves a soft **picture-book keepsake** to read
afterwards, remembers each child's emotional growth, and stays inside
parent-approved safety boundaries.

> Lullow is **not** a therapist or a generic story generator. It is a bedtime
> emotional support companion built around **voice, memory, safety, and calm**.

**Status:** Integrated and verified — **173 backend tests** + **40 frontend
tests** passing, frontend builds clean, and the full pipeline runs **with zero
API keys** (every integration has a graceful mock fallback). Add keys to `.env`
to go live. This branch combines four workstreams — a RAG backend, the live
voice/visual pipeline, a moonlit dreamscape UI, and a physical mood lamp — into
one app (see [Contributors](#contributors)).

---

## Table of contents
1. [How to run](#how-to-run)
2. [Architecture](#architecture)
3. [Features (what's built)](#features-whats-built)
4. [Safety design](#safety-design)
5. [Project structure](#project-structure)
6. [API & environment](#api--environment)
7. [Demo flow](#demo-flow)
8. [Operational notes](#operational-notes)
9. [Contributors](#contributors)
10. [Credits & assets](#credits--assets)

---

## How to run

You need **two terminals** — backend (FastAPI, port 8000) and frontend (Vite,
port 5173). The Vite dev server proxies `/api` → `http://localhost:8000`, so no
CORS setup is needed. **It runs fully with no API keys** thanks to mock
fallbacks; add keys to `.env` to go live.

Prereqs: Python 3.12, Node ≥ 18 (tested on Node 25 / npm 11).

### 1. Backend (terminal 1)

```bash
cd "backend"

# First time only — create venv + install deps:
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

# (Optional) add API keys to go live — the app works without this:
cp ../.env.example ../.env     # then edit ../.env

# Run the API:
.venv/bin/uvicorn app.main:app --reload --port 8000
```

- API docs (Swagger): http://localhost:8000/docs
- Live-vs-mock status: http://localhost:8000/api/status
- On startup it seeds a **demo parent account** for login (`demo_parent` /
  `lullow-demo`). **No demo child is seeded** — every child profile is created by
  the family, so stories use the child's own name and world.

> A `.venv` with all deps may already exist in `backend/` — if so, skip the
> create/install steps and just run the last command. Note: `.env` is **not**
> watched by `--reload`; restart the server after changing keys/voice.

### 2. Frontend (terminal 2)

```bash
cd "frontend"
npm install        # first time only
npm run dev        # → http://localhost:5173
```

Open **http://localhost:5173** → the **moonlit profile picker**. Create a child
profile, then open it to enter Child Bedtime Mode (`/child`). The Parent
Dashboard is at **/parent**. The frontend signs in to the demo parent account
transparently, so there's no login screen for the single-family demo.

### 3. Tests

```bash
cd "backend"  && .venv/bin/pytest        # 173 tests, deterministic (mock mode)
cd "frontend" && npx vitest run          # 40 component/context tests
cd "frontend" && npm run build           # type-checks + production build
```

### 4. Health monitor (optional)

```bash
.venv/bin/python scripts/healthcheck.py  # end-to-end check of every integration
```

---

## Architecture

```
Frontend (React + Vite + TS + Tailwind v4)        Backend (FastAPI, Python 3.12)
  ├── Profile picker  (route /)                     ├── Auth        → parent login + sessions
  │     moonlit dreamscape landing                  ├── Voice       → Deepgram (STT + Aura-2 TTS)
  ├── Child Bedtime Mode  (route /child)            ├── Story brain → Claude  ⇄  Fetch.ai ASI One
  │     voice/text check-in → calming audio story   │     emotion → plan → safety → story body
  │     → goodnight → silent picture-book keepsake, │     → safety eval → review trail
  │     looping lullaby BGM, "find a grown-up"      ├── RAG memory  → Redis (split app/profile DBs)
  └── Parent Dashboard  (route /parent)             │     vector store + agent memory
        profile, safety settings, story-world       ├── Visual      → image model → Pika clips
        memory, history + review trail + storybook,  └── Mood lamp   → Govee (physical, per scene)
        journal
```

Two design principles run through everything:

1. **Graceful mocks.** Every integration is wrapped in a client with a mock
   fallback and lazy SDK import, so a missing key or package never crashes the
   app — it degrades to deterministic mock output. `GET /api/status` reports
   which integrations are live vs. mocked.
2. **Switchable LLM.** The story brain runs on **Claude (Anthropic)** by default
   and can switch to **Fetch.ai ASI One** via `PROMPT_PROVIDER`, behind one
   `prompt_agent` facade — so the rest of the pipeline never changes.

---

## Features (what's built)

**Moonlit profile picker (landing)**
- A dark "dreamscape" landing — glowing animated portraits, a sleeping moon,
  drifting stars/mist/clouds — for choosing or creating a child profile.
- Per-device roster (localStorage); each child's stories + memory live on the
  backend. Honors `prefers-reduced-motion`.

**Child Bedtime Mode**
- Voice-first emotional check-in (hold-to-talk mic → Deepgram STT) **with a text
  fallback** and keyboard support.
- Gentle, voice/tone-compliant reflection of the child's feeling.
- **Audio-first bedtime.** The story is delivered as **continuous, calming
  narration** with a gentle female voice (Deepgram **Aura-2 `cora`**), slowed and
  pitch-preserved; long stories are stitched into one clip so narration never
  plays in disjointed segments. Eyes can stay closed — no screen to watch.
- **Looping lullaby BGM** (soft piano) that starts on the first tap and plays at
  a steady level under the narration, with a mute toggle.
- **Physical mood lamp** (Govee) follows each scene's atmosphere as the story
  plays, then fades off at goodnight. Safe no-op without hardware.
- **Silent picture-book keepsake.** While the audio plays, soft illustrations
  (2–3 quiet pages) are painted **in the background**. They surface only *after*
  goodnight as an optional "📖 Read last night's storybook" — a self-paced,
  **silent** reader (no narration, no lamp). Generation never blocks or
  interrupts the child; the button stays soft-disabled until the art is ready.
- **Persistent "Find a grown-up" help button on every screen**, plus an
  automatic warm escalation screen on danger signals.

**Parent Dashboard**
- Child profile editor; parent safety settings (blocked topics/words, visual
  mode, max length, toggles).
- **Family memory / story-world editor** (recurring character + setting + past
  themes), incl. the character's master reference-image thumbnail.
- Story history with the full **review trail** (child said / memory used / safety
  constraints / avoided topics / parent edits / status), safety scores, and a
  **"📖 View storybook"** to read any story's illustrated pages.
- Parent **revise** (e.g. "make softer") and **approve** (writes back to memory).
- **Growth journal** (emotion counts + helpful elements + non-diagnostic
  reflection).

**RAG backend & memory**
- Redis with a **split keyspace** (app/RAG in DB0, profiles in DB1), optional
  JSON compression, and an in-memory fallback when Redis is absent.
- A **vector store + agent memory** (semantic / episodic / procedural / safety /
  working) used to ground each story in the child's history and constraints.

**Backend pipeline**
`emotion → load memory + parent constraints → safety/escalation gate → plan →
story body → safety evaluation (regenerates once / safe fal

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 127 recognized source files, 558 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 136)

```
.env.example
.gitignore
API_CONTRACT.md
BACKEND_DESIGN.md
BACKEND_QUICK_DESIGN.md
BACKEND_WORKFLOW_DIAGRAM.md
backend/app/__init__.py
backend/app/config.py
backend/app/dependencies.py
backend/app/integrations/__init__.py
backend/app/integrations/anthropic_client.py
backend/app/integrations/arize_client.py
backend/app/integrations/deepgram_client.py
backend/app/integrations/embedding_client.py
backend/app/integrations/fetchai_client.py
backend/app/integrations/govee_client.py
backend/app/integrations/image_client.py
backend/app/integrations/midjourney_client.py
backend/app/integrations/pika_client.py
backend/app/integrations/redis_app_client.py
backend/app/integrations/redis_client.py
backend/app/integrations/redis_profile_client.py
backend/app/integrations/terac_client.py
backend/app/integrations/vector_store.py
backend/app/integrations/voice_client.py
backend/app/main.py
backend/app/models/__init__.py
backend/app/models/schemas.py
backend/app/prompts/__init__.py
backend/app/prompts/prompts.py
backend/app/routers/__init__.py
backend/app/routers/admin.py
backend/app/routers/auth.py
backend/app/routers/journal.py
backend/app/routers/lamp.py
backend/app/routers/profile.py
backend/app/routers/rag.py
backend/app/routers/session.py
backend/app/routers/settings.py
backend/app/routers/story.py
backend/app/routers/visual.py
backend/app/routers/voice.py
backend/app/services/__init__.py
backend/app/services/asset_cache.py
backend/app/services/auth.py
backend/app/services/comfort_strategy.py
backend/app/services/emotion.py
backend/app/services/journal.py
backend/app/services/memory.py
backend/app/services/planner.py
backend/app/services/prompt_agent.py
backend/app/services/review_trail.py
backend/app/services/safety.py
backend/app/services/story_retrieval.py
backend/app/services/story.py
backend/app/services/visual.py
backend/pytest.ini
backend/requirements.txt
backend/scripts/debug_rag.py
backend/scripts/debug_vector.py
backend/scripts/debug_vector2.py
backend/tests/__init__.py
backend/tests/conftest.py
backend/tests/test_comfort_strategy.py
backend/tests/test_emotion.py
backend/tests/test_memory.py
backend/tests/test_planner.py
backend/tests/test_prompt_agent.py
backend/tests/test_redis_client.py
backend/tests/test_review_trail_and_journal.py
backend/tests/test_routers.py
backend/tests/test_safety.py
backend/tests/test_story_retrieval_vector_reason.py
backend/tests/test_story_retrieval.py
backend/tests/test_story.py
backend/tests/test_vector_store.py
backend/tests/test_visual.py
docs/RAG_API.md
docs/REDIS_PROD.md
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/README.md
frontend/src/api.ts
frontend/src/App.css
frontend/src/App.tsx
frontend/src/components/BgmToggle.tsx
frontend/src/components/Brand.tsx
frontend/src/components/ErrorBoundary.tsx
frontend/src/components/HelpScreen.tsx
frontend/src/components/MicButton.tsx
frontend/src/components/NightSky.tsx
frontend/src/components/NinoFox.tsx
frontend/src/components/ProfileSwitcher.tsx
frontend/src/components/StatusBadge.tsx
frontend/src/components/StorybookReader.tsx
frontend/src/components/ui/Avatar.tsx
frontend/src/components/ui/Button.tsx
frontend/src/components/ui/Card.tsx
frontend/src/components/ui/ChipInput.tsx
frontend/src/components/ui/EmojiPicker.tsx
frontend/src/components/ui/index.ts
frontend/src/components/ui/Modal.tsx
frontend/src/components/ui/SectionHeader.tsx
frontend/src/components/ui/Slider.tsx
frontend/src/components/ui/TextField.tsx
frontend/src/components/ui/Toggle.tsx
frontend/src/components/WarmBackground.tsx
frontend/src/context/ProfileContext.tsx
frontend/src/hooks/useAudio.ts
frontend/src/index.css
frontend/src/lib/audioFade.ts
frontend/src/lib/bgm.ts
frontend/src/lib/profileStore.ts
frontend/src/main.tsx
frontend/src/pages/ChildMode.tsx
frontend/src/pages/ParentDashboard.tsx
frontend/src/pages/ProfileForm.tsx
[16 more files omitted for size]
```

### Dependencies

- backend/requirements.txt: anthropic@==0.42.0, deepgram-sdk@==3.8.0, fastapi@==0.115.6, httpx@==0.28.1, pydantic@==2.10.4, pydantic-settings@==2.7.1, pytest@==8.3.4, pytest-asyncio@==0.25.2, python-dotenv@==1.0.1, python-multipart@==0.0.20, redis@==5.2.1, requests@==2.34.2, uvicorn[standard]@==0.34.0
- frontend/package.json: @eslint/js@^10.0.1, @tailwindcss/postcss@^4.3.1, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @testing-library/user-event@^14.6.1, @types/node@^24.12.3, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, @vitest/coverage-v8@^4.1.9, autoprefixer@^10.5.0, eslint@^10.3.0, eslint-plugin-react-hooks@^7.1.1, eslint-plugin-react-refresh@^0.5.2, globals@^17.6.0, jsdom@^29.1.1, postcss@^8.5.15, react@^19.2.6, react-dom@^19.2.6, react-router-dom@^7.18.0, tailwindcss@^4.3.1, typescript@~6.0.2, typescript-eslint@^8.59.2, vite@^8.0.12, vitest@^4.1.9

### Recent commits (newest first)

- docs: sync README with current bedtime flow
- Bedtime flow: audio-only default, silent storybook, smoother audio
- Prepare Wally demo story flow
- Prepare demo happy path flow
- Lamp: audio-only mood sync + faster story generation
- Fix voice check-in deadlock (mic input froze on 'Listening…')
- Home: change CTA from "Let's get cozy" to "Let's start"
- Landing logo: vector bell-lamp-over-book glyph + Lullow wordmark
- Remove end-of-story ritual feature; swap landing logo
- Merge PR #1, #2, #4 — all team code integrated (#3 is the base)
- Integrate all four workstreams onto the RAG backend
- Add physical Govee mood lamp synced to story scenes
- Redesign profile picker dreamscape
- Implement backend RAG integrations
- feat: looping lullaby BGM, demo cleanup, fast/continuous narration
- perf(story): generate story body with Sonnet instead of Opus
- chore(scripts): add end-to-end health-check / monitoring script
- Use logo as favicon; keep only banner in README header
- feat(voice+visual): calming live TTS + character-consistent picture-book (Track 2)
- Add Lullow logo and hero banner to README

## Key source files (fetched from GitHub, selected and truncated for size)

### BACKEND_QUICK_DESIGN.md

```markdown
# Lullow Backend Quick Design

This is the short version for implementation.

## Core Idea

Lullow backend is a safe bedtime AI pipeline:

```text
child voice/text
  -> Deepgram STT if voice
  -> emotion + safety extraction
  -> Redis profile/settings/memory retrieval
  -> Redis vector RAG examples
  -> story plan
  -> safe story generation
  -> safety evaluation
  -> Deepgram TTS narration
  -> optional Midjourney picture-book scenes
  -> Redis save + parent review trail
  -> Sentry trace
```

## Main Services

```text
routers/
  session.py   check-in and emotion extraction
  story.py     generate, revise, approve, reject
  voice.py     Deepgram STT/TTS
  visual.py    Midjourney picture-book images
  profile.py   child profile
  settings.py  parent safety settings
  rag.py       seed/search/index examples

services/
  emotion.py      extract feeling, trigger, reflection
  safety.py       input/output/visual safety gates
  retrieval.py    Redis vector search + agent memory
  planner.py      story plan before generation
  story.py        orchestrates full pipeline
  visual.py       scene split + image prompts
  memory.py       Redis read/write
  journal.py      parent-facing summaries

integrations/
  deepgram_client.py
  midjourney_client.py
  redis_profile_client.py
  redis_app_client.py
  vector_store.py
  sentry_client.py
```

## Redis Layout

Use Redis as AI memory, not just cache.

```text
Redis DB 1: profile/private parent data
  profile:{child_id}
  settings:{child_id}
  consent:{child_id}
  family:{family_id}:children

Redis DB 0: app state + RAG + generated memories
  world:{child_id}
  story:{story_id}
  child:{child_id}:stories
  visual:{story_id}:{scene_index}
  journal:{child_id}:{period}
  rag:example:{id}
  rag:child:{child_id}:story:{story_id}
  agent:{child_id}:semantic
  agent:{child_id}:episodic:{session_id}
  agent:{child_id}:procedural
  agent:{child_id}:safety
  agent:{child_id}:working:{session_id}
```

Agent memory types:

- Semantic: parent-approved facts like favorite character, comfort object.
- Episodic: past bedtime sessions and feedback.
- Procedural: what tends to help this child settle.
- Safety: rejected topics, unsafe outputs, successful rewrites.
- Working: current session state, short TTL.

## RAG

Store sample profiles, requests, stories, rituals, image prompts, and pictures.

Retrieve by:

- age band
- emotion
- trigger
- preferred character
- parent blocked topics
- successful past rituals
- parent ratings

Use RAG for inspiration and personalization, never to override safety settings.

## Story Generation Flow

```text
POST /api/story/generate
  1. Load profile/settings from Redis DB 1.
  2. Load story world and memory from Redis DB 0.
  3. Use check-in extraction or extract emotion.
  4. Run escalation gate.
  5. Retrieve RAG examples.
  6. Build story plan.
  7. Safety-check plan.
  8. Generate story.
  9. Evaluate story safety.
  10. Regenerate once if unsafe.
  11. Fall back to deterministic safe story 
[truncated — 2396 more characters]
```

### API_CONTRACT.md

```markdown
# Lullow API Contract

Shared contract between the **backend dev** (implements these endpoints) and the
**frontend dev** (consumes them). All request/response bodies are the Pydantic
models in `backend/app/models/schemas.py` — treat that file as the source of
truth for field names/types. Base URL in dev: `http://localhost:8000`.

All endpoints are prefixed `/api`. JSON in/out unless noted.

## System
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | `/api/health` | — | `{status, app}` |
| GET | `/api/status` | — | `{features: {anthropic, deepgram, redis, pika, image, arize, terac}}` (true=live, false=mock) |

## Profile & memory (Redis layer)
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | `/api/profile` | — | `ChildProfile[]` (all children, for picker) |
| GET | `/api/profile/{child_id}` | — | `ChildProfile` (404 if missing) |
| PUT | `/api/profile` | `ChildProfile` | `ChildProfile` (upsert) |
| GET | `/api/profile/{child_id}/world` | — | `StoryWorld` (defaulted if none) |
| PUT | `/api/profile/{child_id}/world` | `StoryWorld` | `StoryWorld` |

## Parent safety settings
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | `/api/settings/{child_id}` | — | `ParentSafetySettings` (defaults if none) |
| PUT | `/api/settings` | `ParentSafetySettings` | `ParentSafetySettings` |

## Emotion check-in (session)
| Method | Path | Body | Returns |
|---|---|---|---|
| POST | `/api/session/checkin` | `CheckInRequest` | `CheckInResponse` (extraction + optional escalation) |

If `extraction.safety_flag` is true, `escalation.triggered` is true and the UI
must show the warm "find a grown-up / help" screen instead of a story.

## Story pipeline
| Method | Path | Body | Returns |
|---|---|---|---|
| POST | `/api/story/generate` | `StoryRequest` | `StoryGenerateResponse` |
| POST | `/api/story/revise` | `StoryReviseRequest` | `StoryGenerateResponse` (parent edit) |
| GET | `/api/story/{story_id}` | — | `Story` |
| GET | `/api/story?child_id=...` | — | `Story[]` (history, newest first) |
| POST | `/api/story/{story_id}/approve` | — | `Story` (sets review_trail.final_status=parent_approved, saves memory) |
| POST | `/api/story/{story_id}/annotate` | `AnnotationRequest` | annotation record (Terac) |
| GET | `/api/story/{story_id}/annotations` | — | annotation record list |

### POST /api/story/generate — schema details

**Request body** (`StoryRequest`):
```json
{
  "child_id": "child_001",
  "input_source": "voice" | "text",
  "speaker": "child" | "parent",
  "raw_input": "I'm scared of the dark.",
  "visual_mode": "off" | "low_stimulation" | null,
  "extraction": null   // optional: pass the CheckInResponse.extraction here to
                        // skip re-extraction (saves a Claude call)
}
```

**Response body** (`StoryGenerateResponse`):
```json
{
  "story": { ... } | null,          // null when danger input blocked generation
  "escalation": { ... } | null,     // set (triggered:true) when story is null
  "used_mock": { "emotio
[truncated — 3498 more characters]
```

### backend/requirements.txt

```
fastapi==0.115.6
uvicorn[standard]==0.34.0
pydantic==2.10.4
pydantic-settings==2.7.1
python-dotenv==1.0.1
python-multipart==0.0.20
httpx==0.28.1
anthropic==0.42.0
deepgram-sdk==3.8.0
requests==2.34.2
redis==5.2.1

# Testing
pytest==8.3.4
pytest-asyncio==0.25.2

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "test": "vitest run"
  },
  "dependencies": {
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "react-router-dom": "^7.18.0"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@tailwindcss/postcss": "^4.3.1",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@testing-library/user-event": "^14.6.1",
    "@types/node": "^24.12.3",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "@vitest/coverage-v8": "^4.1.9",
    "autoprefixer": "^10.5.0",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "jsdom": "^29.1.1",
    "postcss": "^8.5.15",
    "tailwindcss": "^4.3.1",
    "typescript": "~6.0.2",
    "typescript-eslint": "^8.59.2",
    "vite": "^8.0.12",
    "vitest": "^4.1.9"
  }
}

```

### frontend/src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/src/App.tsx

```typescript
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { ProfileProvider, useProfiles } from './context/ProfileContext'
import ProfilePicker from './pages/ProfilePicker'
import ProfileForm from './pages/ProfileForm'
import ChildMode from './pages/ChildMode'
import ParentDashboard from './pages/ParentDashboard'
import StatusBadge from './components/StatusBadge'
import BgmToggle from './components/BgmToggle'
import ErrorBoundary from './components/ErrorBoundary'
import type { ReactNode } from 'react'

/**
 * RequireProfile — guards routes that need an active profile. If there is no
 * active child once the roster has loaded, bounce back to the picker.
 */
function RequireProfile({ children }: { children: ReactNode }) {
  const { activeChildId, ready } = useProfiles()
  if (!ready) return null
  if (!activeChildId) return <Navigate to="/" replace />
  return <>{children}</>
}

export default function App() {
  return (
    <ErrorBoundary>
      <ProfileProvider>
        <BrowserRouter>
          {/* Unobtrusive live/mock status badge — visible for judges */}
          <StatusBadge />
          {/* Mute/unmute the looping lullaby music */}
          <BgmToggle />
          <Routes>
            <Route path="/" element={<ProfilePicker />} />
            <Route path="/create" element={<ProfileForm />} />
            <Route path="/edit/:id" element={<ProfileForm />} />
            <Route
              path="/child"
              element={
                <RequireProfile>
                  <ChildMode />
                </RequireProfile>
              }
            />
            <Route
              path="/parent"
              element={
                <RequireProfile>
                  <ParentDashboard />
                </RequireProfile>
              }
            />
            <Route path="*" element={<Navigate to="/" replace />} />
          </Routes>
        </BrowserRouter>
      </ProfileProvider>
    </ErrorBoundary>
  )
}

```

### backend/app/main.py

```python
"""Lullow FastAPI application entrypoint.

Run with:  uvicorn app.main:app --reload --port 8000   (from the backend/ dir)
"""
from __future__ import annotations

import importlib
import logging

from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from .config import settings

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("lullow")


@asynccontextmanager
async def _lifespan(app: FastAPI):
    """Seed demo data on startup if no children exist yet."""
    try:
        from .services.auth import seed_demo_user
        # No demo child seeding (no Leo / Nino / Moonberry) — profiles are user-created.
        seed_demo_user()
    except Exception as exc:  # pragma: no cover
        logger.warning("Demo seed failed (non-fatal): %s", exc)
    yield


app = FastAPI(
    title="Lullow",
    description="A voice-first bedtime comfort companion for children.",
    version="0.1.0",
    lifespan=_lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.cors_origin_list,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
)


@app.get("/api/health")
def health() -> dict:
    return {"status": "ok", "app": "lullow"}


@app.get("/api/status")
def status() -> dict:
    """Which sponsor integrations are live vs. running on mock fallbacks."""
    return {
        "features": settings.feature_status(),
        "note": "true = live integration, false = mock fallback (app still works)",
    }


# Include feature routers defensively so the app runs even while some are still
# being implemented by the backend dev.
_ROUTERS = [
    "auth",
    "session",
    "story",
    "voice",
    "visual",
    "profile",
    "settings",
    "journal",
    "lamp",
    "rag",
    "admin",
]

for name in _ROUTERS:
    try:
        module = importlib.import_module(f".routers.{name}", package="app")
        app.include_router(module.router)
        logger.info("Mounted router: %s", name)
    except ModuleNotFoundError:
        logger.info("Router not yet present (skipping): %s", name)
    except Exception as exc:  # pragma: no cover
        logger.warning("Failed to mount router %s: %s", name, exc)

```

### frontend/src/components/ui/index.ts

```typescript
/** Barrel export for the Lullow light-theme UI component set. */
export { default as Button } from './Button'
export { default as Card } from './Card'
export { default as ChipInput } from './ChipInput'
export { default as EmojiPicker, AVATAR_EMOJIS } from './EmojiPicker'
export { default as Modal } from './Modal'
export { default as SectionHeader } from './SectionHeader'
export { default as Slider } from './Slider'
export { default as TextField } from './TextField'
export { default as Toggle } from './Toggle'
export { default as Avatar } from './Avatar'

```

### frontend/postcss.config.js

```javascript
export default {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
}

```

### frontend/vitest.config.ts

```typescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
  },
})

```

[112 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]