Concept 01
The pipeline is a resumable DAG, not a straight line
The seven commands in the topic — start, outline, portraits, illustrate, narrate, build, pdf — read like a line, but the real shape is a directed acyclic graph with a hidden eighth stage, write, doing the heavy lifting in the middle.
- write ∥ portraits — both need only the outline, so the script and the character portraits can be produced at the same time.
- illustrate ∥ narrate — both need only the script, so pictures and audio are generated independently.
- build and pdf are both leaf stages. Because the PDF is a printed book, it doesn't need audio at all — it can ship before or without narration.
story-tools.cjs state start-stage / complete-stage, recording progress in
STATE.md. story-tools.cjs graph next computes the next runnable stage from the DAG plus the completed set,
and /story:next auto-invokes it — no stage ever leaves you with a “now run X” instruction.Concept 02
Turning an idea into a script start → outline → write
The first three stages never touch an image or a sound — they turn a wisp of an idea into a fully specified shooting script.
/story:start → concept.md. Resolves the idea (inline text or a path to an idea file),
scaffolds story-output/ with its images/, audio/, and characters-source/ folders,
seeds STATE.md and config.json, and git-ignores the output. Its defining rule: preserve author intent
verbatim. The real job's concept.md even has a section literally titled “Core Idea (author intent — preserve
verbatim beats)” capturing the ADHD-celebrated theme and the pizza-slices-are-fractions climax.
/story:outline → outline.md. This is where the story gains structure — and where it builds the
single most important artifact in the whole pipeline: the Character Reference Sheet. Alongside the story arc and a
Visual Style Guide, a markdown table pins down every character:
| Column | Role downstream |
|---|---|
| Visual Description | The immutable source of truth every later stage repeats verbatim. |
| Age · Gender · Accent | Drive both portrait generation and — crucially — mechanical voice picking. |
| Portrait Prompt | Self-contained prompt for one front-facing reference portrait. |
| Photo Path | Optional — anchor identity to a real photo instead of AI art. |
| Voice ID | Left blank on purpose — narration fills it in mechanically, not by vibes. |
/story:write → script.md. For each scene it emits two blocks. The Narration
block tags prose with [VOICE:narrator] and dialogue with [VOICE:character-name] (tags must match the reference
sheet exactly). The Illustration Prompt block prepends the style-guide preamble, then describes only the scene —
and deliberately does not re-describe anyone's face, because identity is injected later as a reference image. Characters
are named with one identifier, e.g. “Milo in his mustard-yellow t-shirt.”
Concept 03
Keeping characters on-model portraits → illustrate
Here's the hard problem every AI picture book hits: text-only image models can't hold a character's identity across many stateless calls. Tiny wording shifts make the protagonist's face drift from page to page. Story Maker solves it in two stages.
/story:portraits → characters/<slug>.png. Generates exactly one canonical
1024×1024 portrait per non-narrator character. The source is chosen in priority order: (1) a per-story photo in
characters-source/, (2) a photo in the reusable central library ~/nsaf/data/story/characters/, (3) an explicit
Photo Path, or (4) AI-generated via Nano Banana. Mixed casts work naturally — a real-photo
kid can stand beside an AI-generated dragon. Runs once per story no matter how often you re-illustrate; ~$0.04 per AI portrait.
/story:illustrate → images/scene-NN.png. For each scene it figures out which characters are
present, maps each to its portrait PNG, and calls Nano Banana with the scene's illustration prompt plus those portraits as
reference images — up to 5 per scene (the model's limit; ties broken by who has dialogue, then prominence). It
calls the REST API directly (the old CLI-plus-extension chain was the pipeline's most notorious failure) and classifies errors so a quota
error stops instead of retrying for an hour; only a non-quota failure falls through to the Leonardo text-only path. Result: 10
scenes, ~$0.40 total, every character on-model.
Concept 04
Casting and speaking every line narrate
/story:narrate turns the [VOICE:…]-tagged script into per-scene audio, and its cleverness is in casting.
Deterministic, deduped cast assignment. It builds cast.json — one entry per character plus the narrator —
and calls pick-voice.cjs cast, which assigns voices mechanically and spreads them across the cast
so two similar characters don't collide. (An old bug gave two young boys the same OpenAI echo voice; cast-level dedup fixed it.)
The resolved IDs are written back into the outline so re-runs are stable.
Loudness-matched assembly. Each scene's narration is split into ordered (voice, text) segments, each
synthesized to its own mp3, then concatenated by concat-scene-audio.sh — which applies per-segment loudnorm to
−16 LUFS + 25 ms fade-in + 50 ms fade-out and matched-rate silence between speakers. That one helper is why voices
no longer clunk between different perceived loudness levels.
scene-NN.mp3 gets a sidecar JSON recording provider, sample rate, and a hash of the voice assignments.
A scene is skipped only if its sidecar matches the current provider and casting — otherwise the stale file is deleted and
regenerated. This stops a leftover 24 kHz OpenAI clip from surviving an ElevenLabs re-render and silently corrupting the concat.Concept 05
Assembling the film — and the book build → final.mp4, pdf
-c copy./story:build → final.mp4. After a pre-flight that verifies every scene image has a matching
scene audio, build does something subtle first: it detects the audio parameters (sample rate, channel layout) from
scene-01.mp3 with ffprobe, because the title and credits cards must match those exactly — otherwise the
-c copy concat silently drops the audio stream. Then it renders a title card, turns each scene-NN.png +
scene-NN.mp3 into a clip whose length is driven by its narration (-shortest), renders a credits card, and
concatenates title → scenes → credits into final.mp4.
/story:pdf → <slug>-book.pdf. A leaf stage that needs only the script and the scene images
(no audio). It lays out an 8.5×8.5 inch print book — cover, one page per scene (image on top, narration in a cream text
zone rendered as real vector type, so no AI-misspelling risk), and a credits page — via headless Chrome. Cost is $0 because it
reuses artifacts you already have.
Worked example
“The Boy Who Jumped Ahead,” end to end
Milo & Byte — a 10-scene watercolor book about ADHD
A boy whose racing brain leaps past the “steps,” and the patient AI tutor bot who follows his jumps instead of fighting them — until pizza slices click into fractions.
The whole trip, in one line:
→ script.md ([VOICE] tags + illustration prompts)
→ characters/ (identity anchors)
→ images/ ∥ audio/
→ final.mp4 (+ optional book.pdf)
That's Story Maker: eight small stages, one shared state file, a character sheet that keeps faces and voices consistent, and a pair of leaf stages that pour the same artifacts into a movie or a book.