# Explainer films

Six paper-collage explainer films and six feature shorts for Relay, playable in the browser with voice, score and
foley. Each has a full cut and a 15 s cutdown, and each plays in 16:9 and in 9:16 (vertical, with
burned-in captions). **Draft for EC review. Stealth: not for public channels.** All external copy
goes through Ada (relay-gtm) before it ships.

**Running a campaign with this kit?** Start at relay-marketing
[`video/PLAYBOOK.md`](https://github.com/relaycontext/relay-marketing/blob/main/video/PLAYBOOK.md)
(what exists, one-time setup, making a post, a short or a lesson, the gates). Then plan the campaign
in relay-marketing `briefs/`. This folder is where the pieces are made. Any session can load the kit
by claiming the Relay series `relay-campaign-kit`.

| Film | Folder | Voice | Full | 15 s |
|---|---|---|---|---|
| A · Blank Page | `blank-page/` | Kokoro `af_heart` | 0:38 | 0:15 |
| A2 · Blank Page · Together (sharing, collaboration) | `blank-page-together/` | Kokoro `af_heart` | 0:34 | 0:14 |
| B · Mid-Thought | `mid-thought/` | Kokoro `bf_emma` | 0:42 | 0:14 |
| C · The Baton | `the-baton/` | Kokoro `bm_george` | 0:43 | 0:14 |
| D · Pass It On (the baton, cute, with the paper runners) | `pass-it-on/` | Kokoro `af_heart` | 0:37 | 0:15 |
| E · Fridge Door (Relay at home) | `fridge-door/` | Kokoro `af_heart` | 0:34 | 0:15 |
| F · Connecting the Dots (the campaign film: the week drawn into a paper plane) | `connecting-the-dots/` | Kokoro `af_heart` | 0:18 | (is short) |

**Feature shorts** (`shorts/<feature>/`, about 15 s, 16:9 and 9:16): Streams, Series, Sessions,
Recall, Forward, Look first, Expiry, Team, Status, Search. Each is one feature: the problem, the
feature, the payoff. They share `engine/shortkit.js` (timing laid out from the voice, the feature
label, the end card, a score), so a new short is only its own scene. Draw only what the docs state
about a feature. **Idea shorts** (`shorts/idea-*/`) say the deck's positioning plainly, one move each: The invisible wall, Not another tool, Any tool you like, If it speaks MCP, You own what travels. The concept is `/concepts/big-ideas`. **Side-by-side shorts** put Relay next to a tool a viewer already knows, for one viewer at a time: *beads + Relay* (`shorts/beads-and-relay/`, 0:28 and a 0:18 cut) has six voice lines, so it lays out its own timeline and end card instead of shortkit's four-line one. **Life shorts** use the same kit for the core loop at home: The sitter
(`shorts/dog-sitter/`), The trip (`shorts/group-trip/`), The recipe (`shorts/recipe/`), Moving day
(`shorts/moving-day/`), Study group (`shorts/study-group/`), The gift list (`shorts/gift-list/`), The leak
(`shorts/the-leak/`) and Road trip (`shorts/road-trip/`). **Lesson shorts** (`shorts/lesson-<n>-<slug>/`) are the onboarding series *First Relay, in paper*
(relay-marketing `briefs/onboarding-paper-series.md`): one onboarding step each, opened by a *Lesson n of 6*
tab and closed on *Next: …*. `lessonOf(n, name, next)` in `shorts/_kit.js` gives any short that accepts it
(a third argument to its film) the lesson's label, tab and close; `lessonCut` makes a lesson from a longer
film's segments. Lessons 1 and 3 (`shorts/lesson-1-wrap-it-up/`, `shorts/lesson-3-another-tool/`) are new drawing; 2 is the Boots from a Code
scene with its own lines, 4 is Streams as it is, and 5 is cut from Together. 4 and 5 read their voice from
the source folder (`base` in their `index.html`), so there is one copy of it. **Line shorts** (`shorts/line-<n>-<slug>/`) are *Line by line*, the journey building Relay, one chapter per short (relay-marketing `briefs/how-relay-came-to-be.md`, concept `first-line/`): first person, the short kit's four beats as the wall → what got built → how → the receipt. `shorts/_lines.js` holds what they share: the *Line n · building Relay* tab, the paper receipt (`receipt(ctx, tq, t0, rows)`, never dated before 2026-04-17), the journey map (`lineMap`) and the *Line by line.* close; `lineOf(n, name)` wires them in the way `lessonOf` does. Line 4, *Ran out of room* (`shorts/line-4-ran-out-of-room/`), is the first. **Concept shorts** cut a concept as a short:
Boots from a Code (`shorts/boots-from-a-code/`), scenes 1 and 2 of `second-brain/`. `shorts/_kit.js` holds the scene pieces
the later shorts share: the paper phone, the Code message, the wrap-up into a Code, a hand lens.

**The character kit** (`crew/svg.html`, files in `svg/`, Figma plugin in `figma/`): the character as
SVG components. `engine/svg.js` records the engine's drawing as vectors:
- **Body** per paper stock, **Face** per mood, **Arms** per pose, **Outfit** per character; they
  share one frame and stack back into the character;
- the **Crew** in six moods;
- **Held** items and **Props**.

Re-export after any change to the cast with
`BASE=http://127.0.0.1:8124/marketing/explainer node marketing/explainer/tools/svgkit.mjs`.
The Figma plugin (import `figma/manifest.json` in Figma desktop) builds colour styles, a component
set per part, a composable **Character** with swappable parts, and a **concept board**.

**The cast** (`engine/crew.js`, sheet at `crew/`): Page, the original, and seven characters
dressed for the work people do: Memo (office), Plumb (trades), Pitch (sales), Doodle (creative),
Chalk (teaching), Crumb (hospitality), Sticky (home). `crew(ctx, id, x, y, opts)` takes any buddy
option; `bare: true` draws the paper without the outfit. **No black or dark paper, no skin-like
tones** (EC, 2026-09-25): tools are told apart by sky, mint, lilac, butter, graph, ruled and dot grid.

**Asset Builder** (`builder/`): post images from the same engine and cast in six formats and five
layouts. Copy presets come from `brand/messaging.json`, which is lifted from canon (the voice guide
and the social-accounts guide); the never-list is flagged as you type. A design is a URL. **Copy
pack entry** gives a line for a pack file, and `tools/pack.mjs` renders a whole pack
(`marketing/packs/<pack>/pack.json`, with each post's caption) to PNGs beside it.

**Gallery**: `marketing/gallery/` shows every film, short, the cast and the packs, with posters.

`index.html` is the review page: a player with film, cut and format switches, the comparison, the
timed scripts and the decisions before a final cut. Each film also plays alone at
`<film>/index.html`. Add `?cut=15` for the cutdown and `?format=vertical` for 9:16.

**Marketing catalog:** relay-marketing `video/` holds one entry per film: its job, segments, gates,
use-in-outreach notes, alt text, the timed scripts and SRT captions. The scripts and captions
there are **generated** from this folder by `tools/catalog.mjs`, so this folder stays the source.
After changing a script, regenerate the voice and re-run the catalog.

## How it's built

Everything is JavaScript. There is no video file in the repo and no editing software involved.

- **`engine/paper.js`**: a Canvas 2D collage engine. It draws torn paper with contact shadows,
  tapered ink that re-draws at 12 fps (the boil of hand-drawn animation), stop-motion drift on
  every cut-out, a camera, and paper wipes. Every frame is a pure function of time, so export is
  deterministic. **Brand colours are read at boot from `brand/generated/relay-tokens-v36.css`**
  (`loadTokens()`); only neutral paper stock is local.
- **`engine/brand.js`** and **`brand.config.json`**: the brand layer. The lockup, type, paper,
  palette, backgrounds, cast words and Code verb enter the engine from `brand/brand.json`, which
  is generated from `brand.config.json`. This folder is the Relay **child** of the ec-resources
  `frameworks/paper-studio/` framework (see `FRAMEWORK.md`): `engine/ builder/ crew/ tools/` are the
  origin's, so fix them there first, then run `adopt.py --update`. Execution Space uses the same engine.
- **`engine/props.js`**: the cast. Paper buddies (AI sessions), paper-glove hands, people,
  the Code ticket, sticky notes, bubbles, a dog, a train, a bench, a thought cloud.
- **`engine/audio.js`**: a sample-based sequencer, synthesised foley and an offline mixer.
  The voice is ducked under the music, with a glue compressor and a limiter, to about −14 LUFS.
- **`engine/player.js`**: fonts, tokens, the mix, the picture, the controls and captions.
  `?export=1` exposes `window.__film` for rendering. `?t=12.5` opens on a frame.
- **`<film>/film.js`**: one file per film. It holds the voice placement, the scenes, the foley
  cue list, the score and `portrait()`, the film's vertical camera. `<film>/script.json` is the
  voiceover source.
- **`<film>/cut15.js`**: the 15 s cutdown, a re-edit made with **`engine/recut.js`**. It lists
  source segments (with optional speed-ups and paper wipes over cuts). Picture, voice and foley
  travel with their segment, and the score is written fresh for the short timeline from
  **`engine/scorekit.js`** (each film's musical vocabulary as helpers).
- **Campaign invite** (`engine/invite.js`): a film rendered for a campaign ends on an invite card
  with the campaign's access code and `relayctx.com/x/<slug>`, via `?invite=<CODE>&x=<slug>`.
  Codes live in relay-marketing `video/campaigns.json` and are never typed into a film. Each film
  and cut sets `inviteAt`, the moment its end card settles.
- **Vertical (9:16)** is `paper.setFormat('vertical')` plus the film's `portrait()` camera. Screen-space
  elements (end cards, wipes, the night sky) use the live `P.W`/`P.H`. **`engine/captions.js`**
  burns in the voice lines as cut-paper strips, up to `capsUntil`, where the end card takes over.

## Rebuild

Serve the repo root (the pages resolve the token stylesheet by relative path):

```bash
npx http-server -p 8124 -c-1 .          # from the relay-creative root
```

| Task | Command (from a working dir with `npm i playwright`) |
|---|---|
| Regenerate voice | `python3 marketing/explainer/tools/vo.py marketing/explainer/<film>/script.json [line ids]` |
| Rebuild the sample bank | `node tools/bank.mjs <explainer dir> blank-page mid-thought the-baton` (needs `SF=` pointing at FluidR3 per-note MP3s) |
| Stills | `node tools/shot.mjs <film> 3.5 10.8` → `out/<film>-<t>.png` |
| 1080p MP4 | `node tools/render.mjs <film>` → `out/<film>.mp4` (24 fps, x264 CRF 18, AAC 192k). Env `Q="cut=15&format=vertical"` (or `--q`) for other cuts and formats: the output is named from the query (`out/<film>-15-vertical.mp4`) unless `TAG`/`--tag` names it. Honoured since paper-studio 1.2.2 |
| Campaign render | `Q="invite=BLANKPAGE&x=blankpage&cut=15" TAG=-blankpage node tools/render.mjs blank-page` |
| Marketing catalog | `node marketing/explainer/tools/catalog.mjs <relay-marketing>/video` |
| A post pack | `node marketing/explainer/tools/pack.mjs marketing/packs/<pack>/pack.json` (writes the PNGs beside it) |
| Loudness by stem | `node tools/stems.mjs <film> && python3 tools/loud.py <film>` |

The voice uses **Kokoro v1.0** (82M, Apache-2.0), run locally from `/opt/tts/kokoro-v1.0.onnx`
and `voices-v1.0.bin` (the `thewh1teagle/kokoro-onnx` releases), with
`pip install kokoro-onnx soundfile scipy imageio-ffmpeg`. Codes are spelled with an explicit
phoneme override (`code_ph` in `script.json`), so "A9M4FG" is read letter by letter.
The brief asked for OpenRouter TTS, but the session's network policy blocked `openrouter.ai`.
Any line can be swapped for a hosted or human read by dropping an MP3 of the same id into
`<film>/vo/` and updating its `dur` in `vo.json`.

## Rules these films keep

- Only the user's side of the product appears: package, a Code, relay it in, who sent it and
  who took it in. No storage, routing, envelopes, padlocks or architecture, because the
  provisional is unfiled.
- Every AI tool is an unbranded paper character. Third-party names and logos are held for
  EC's per-name clearance.
- No Code length is stated. Codes are example-shaped. Nothing uses the retired vocabulary.
- One closing line per film. "Context that moves." and "The session ends. The context
  shouldn't." never share a surface.

## Credits

Instrument samples: FluidR3 GM, rendered to per-note MP3 by `gleitz/midi-js-soundfonts` (MIT).
Fonts: Cormorant Garamond, Outfit, JetBrains Mono and Caveat (SIL OFL 1.1).
Voice: Kokoro v1.0 (Apache-2.0).
