Bedtime stories your child steers, in the languages your family speaks. Told aloud, painted in watercolor, and approved by you before a single word reaches little ears.
The Italian cantastorie stood in the piazza, sang a tale, and pointed at painted boards. This app is that craft, revived carefully: one warm narrator voice, soft watercolor pages, and a child's finger choosing the path β with a parent as the piazza's gatekeeper, seeing and hearing every word, picture, and sound before any child meets it.
Pre-readers cannot use story apps built on text, and screen apps built on taps train the wrong appetite at bedtime. Multilingual families juggle one-language apps with robotic voices in the smaller languages. And parents have no way to fully preview generated content before their child meets it.
Cantastorie is built on one belief: a bedtime app should wind a child down. Voice carries the story, pictures carry the choices, and nothing on screen asks a pre-reader to read.
A tap wakes the shelf, which greets the child aloud β "Ciao! Quale storia ascoltiamo oggi?" Tap a cover, "Si parte!", and page one begins. At most two taps and four seconds stand between opening the app and hearing a story. Watercolor pages turn themselves when the narration ends; one large play-pause button is the only control, and pausing resumes from the exact position.
At fixed branch points the page dims behind two picture cards with spoken labels. The child taps one and the story follows β agency without reading. A child who drifts off mid-choice still gets a complete, gentle ending: after a spoken nudge and a short wait, the first option auto-continues. Replayability lives in the branches β the boat story again, then the other ending.
The target is a single warm narrator identity across every story and language, like the piazza storyteller of old. Launch narration is generated by Gemini TTS (via OpenRouter) with one house voice pinned across every language; family voice cloning runs on Voxtral via the Mistral API, and Deepgram supplies word timings and the fallback voice bench now that ElevenLabs is retired. The trade-offs are recorded in ADR-004 and ADR-008. Every child-facing prompt is recorded per language β zero required text in child mode.
Soft watercolor, warm palette, rounded characters, nothing frightening β bedtime, not Saturday cartoons. Images carry no text and nothing scary. Every story's final page lands on comfort or sleepiness.
Everything grown-up sits behind a small, low-contrast corner of the shelf. The gate is a three-second hold followed by a two-integer addition on a keypad β no PIN, freshly random each time. Five failures lock it for five minutes, and the lockout survives reloads. Behind it: language settings, reading mode, and export/import (with the dashboard and review queue arriving in Phase 2).
Behind a separate operator secret, /workshop is where stories are born: start a generation run, watch each pipeline step's progress, inspect the staged story (text, audio, images), and publish to R2 when it's right. Runs execute in-process and survive restarts β the pipeline's filesystem checkpoints double as the resume mechanism, so a mid-run reboot re-buys zero API calls. The design is settled in ADR-005.
For reading-along parents and emerging readers, an optional text panel shows the current page with karaoke word highlighting and tap-word English glosses drawn from a precomputed gloss map β no network call. It is parent-enabled and off by default; the core experience never requires reading.
Italian and Spanish are the flagships β deepest content, first through every quality gate. English, Greek, and German ride along. Stories are authored natively per language, never translated: an Italian story reaches for biscotti della nonna, a Spanish one for magdalenas.
No child accounts, no tracking, no analytics. The child player is account-free β progress lives in the browser (IndexedDB) and exports to a file; nothing about the child ever leaves the device. A parent signs in via Clerk (magic link or OAuth) only to request and review stories β no Clerk script or cookie touches any child path. And every story passes a machine safety gate and a parent's eyes and ears before it reaches a shelf β a model mistake needs a human mistake on top of it to reach a child.
Cantastorie is one FastAPI app with three faces: a vanilla-JS child player, a server-rendered parent area, and an operator workshop (/workshop) for in-app story authoring and review. A plain-Python authoring pipeline runs in the same repo, either from the CLI or in-process via the workshop. The stack mirrors the sibling project habla-hermano; the reasoning behind each choice is in ADR-001.
| Layer | Technology | Why |
|---|---|---|
| Backend | FastAPI | Async, Pydantic validation, HTMX-friendly SSR β hermano-proven |
| Player UI | Vanilla ES modules + Web Audio API | Full-screen, audio-driven, FSM-managed; crossfades that work on iOS |
| Parent UI | Jinja2 + HTMX + Tailwind | Server-driven UI, minimal JS |
| Pipeline | Plain Python + Pydantic AI | Typed step functions, filesystem checkpoints, no graph framework |
| LLMs, images & narration | OpenRouter | One gateway, per-step model choice; narration on Gemini 3.1 Flash TTS, word timings via Deepgram (ElevenLabs retired β ADR-008) |
| Asset storage | Cloudflare R2 | Zero egress fees, access logs off, bucket-direct playback |
| Hosting | Render (Docker, render.yaml) |
Hermano's deploy precedent |
| Child persistence | IndexedDB | Progress, settings, lockout, family token β nothing server-side |
| Testing | pytest + Vitest + Playwright | Providers mocked in unit tests; child flows in a real browser |
One FastAPI app serves a static shell; everything the child experiences after page load happens in the browser, talking only to Cloudflare R2 and IndexedDB. The authoring pipeline runs either as a CLI or in-process via the workshop, sharing the same step functions. The app and the pipeline share only src/config.py and the story.json contract.
graph LR
B["Browser (child)<br/>ES modules + Web Audio + IndexedDB"]
R2["Cloudflare R2<br/>audio Β· images Β· manifests"]
F["FastAPI on Render<br/>player Β· parent area Β· workshop"]
P["Pipeline<br/>plain Python + Pydantic AI"]
OR["OpenRouter<br/>story Β· safety Β· glosses Β· images Β· narration"]
B -- "bucket-direct fetch" --> R2
B -- "page load, parent HTMX" --> F
F -- "workshop runs" --> P
P -- "publish" --> R2
P --> OR
For the code as built β module map, player and audio state machines, and the seams between them β see docs/system-overview.md. For the settled design and its rationale, see docs/architecture.md.
src/
βββ config.py Settings shared by app and pipeline (R2, keys, per-step models)
βββ api/ FastAPI app factory, player route, parent area, workshop
βββ pipeline/ Authoring pipeline: typed steps, cache, providers, models
β βββ steps/ write Β· safety Β· revise Β· gloss Β· narrate Β· illustrate Β· assemble
βββ workshop/ In-app authoring: run manager, run records, resume-on-boot
βββ templates/ Jinja2 (parent area + player shell + workshop)
βββ static/
βββ js/ Vanilla ES modules: fsm, audio engine, playback, screens, storage
βββ css/ Player watercolor CSS; parent Tailwind
content/ Pipeline working folders (gitignored)
staging/ Staged stories for operator review
tests/ pytest + Vitest + Playwright
docs/ product.md, architecture.md, system-overview.md, setup.md, adr/
Requires uv, Node.js 20+, and Python 3.12 (uv installs it automatically).
make install # uv sync + npm install
make install-hooks # pre-commit hooks (lint, format, types, secrets, commit style)
make dev # run the FastAPI app at http://localhost:8000
make dev-css # watch and compile Tailwind CSS (run alongside make dev)
make test # all tests (pytest + Vitest)
make check # lint + format check + strict mypy
make help # list every targetCopy .env.example to .env for pipeline work. Only OPENROUTER_API_KEY is needed to run the default pipeline end to end β story, safety, glosses, images, and narration all run through OpenRouter. ElevenLabs is retired (ADR-004). Two pipeline-only keys are the bounded exceptions: DEEPGRAM_API_KEY for the word-timing pass (OpenRouter does not carry the Deepgram models) and MISTRAL_API_KEY for voice cloning only (ADR-008). The player needs no keys at story time.
Commit messages follow Conventional Commits, enforced by commitizen via pre-commit. Every PR runs lint, format check, strict mypy, pytest, Vitest, a Bandit security scan, a Tailwind compile, and a Docker build (ci.yml). Deployment targets Render via render.yaml; the Cloudflare R2 bucket and Render setup are documented in docs/setup.md.
The authoring pipeline is built end to end β write, safety gate, bounded revise, gloss, narrate (Gemini 3.1 Flash TTS via OpenRouter, ADR-008), illustrate (character sheet β pages β cover), assemble, stage, and publish to R2 β with content-addressed caching so unchanged inputs cost zero API calls. The child player is built: a mobile-first FSM with Web Audio playback, auto page turns, crossfades, and IndexedDB state. The operator workshop at /workshop runs the pipeline in-process with step-level progress, staged review, and publish β all behind a single env-var secret, with resume-on-boot for interrupted runs (ADR-005). Published stories are live on R2 with bucket-direct playback.
What's next: the Gemini TTS bake-off to finalize per-language voices (AI-366, ADR-008), parent-face pack requests and the review queue (Phase 2), branching stories, and the family-voice narration feature (ADR-006, Proposed).
- Product Specification β vision, behaviors, content rules, decision log
- Architecture β the FastAPI app, the Web Audio player, the authoring pipeline, and narration
- System Overview β the code as built: module map, state machines, and seams
- Setup & Deploy β R2 bucket, CORS, and the Render blueprint
- Architecture Decision Records β settled decisions:
- ADR-001: Technology Stack β FastAPI, vanilla JS, plain-Python pipeline, OpenRouter, R2, Render
- ADR-002: Narration Provider β Voxtral via OpenRouter (superseded by ADR-004)
- ADR-003: Parent Authentication via Clerk β accepted
- ADR-004: Narration β Voxtral + Deepgram, ElevenLabs Retired β amended by ADR-008
- ADR-005: Workshop Area β in-app authoring with in-process pipeline runs
- ADR-006: Nonna Narrates β family voice cloning, proposed
- ADR-007: LangSmith Observability β app-wide tracing
- ADR-008: Gemini TTS Defaults, Mistral Cloning β default voices on Gemini via OpenRouter; cloning scoped to Voxtral on the Mistral API
MIT β see LICENSE.
