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 (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 frombrand/generated/relay-tokens-v36.css(loadTokens()); only neutral paper stock is local.engine/brand.jsandbrand.config.json: the brand layer. The lockup, type, paper, palette, backgrounds, cast words and Code verb enter the engine frombrand/brand.json, which is generated frombrand.config.json. This folder is the Relay child of the ec-resourcesframeworks/paper-studio/framework (seeFRAMEWORK.md):engine/ builder/ crew/ tools/are the origin's, so fix them there first, then runadopt.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=1exposeswindow.__filmfor rendering.?t=12.5opens on a frame.<film>/film.js: one file per film. It holds the voice placement, the scenes, the foley cue list, the score andportrait(), the film's vertical camera.<film>/script.jsonis the voiceover source.<film>/cut15.js: the 15 s cutdown, a re-edit made withengine/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 fromengine/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 andrelayctx.com/x/<slug>, via?invite=<CODE>&x=<slug>. Codes live in relay-marketingvideo/campaigns.jsonand are never typed into a film. Each film and cut setsinviteAt, the moment its end card settles. - Vertical (9:16) is
paper.setFormat('vertical')plus the film'sportrait()camera. Screen-space elements (end cards, wipes, the night sky) use the liveP.W/P.H.engine/captions.jsburns in the voice lines as cut-paper strips, up tocapsUntil, where the end card takes over.
Rebuild#
Serve the repo root (the pages resolve the token stylesheet by relative path):
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).