Showcase
Package

@shwcase/sdk

Generate branded, animated product explainer videos from a single config file — motion-graphics scenes, an original synth backing track, and an AI voiceover, rendered deterministically frame by frame.

Node ≥ 18ESMPlaywrightffmpeg

How it works

A four-stage pipeline: your narration drives the timing, the browser renders the pixels, ffmpeg does the rest.

01

One config file

Drop an explainer.config.mjs in your project root — brand, theme, voice, and a list of scenes.

02

Deterministic render

Headless Chromium (Playwright) renders every scene frame-by-frame at 30fps — perfectly smooth, no dropped frames.

03

Voice + music mix

Narration (ElevenLabs or macOS say) drives scene timing; an original synth track is sidechain-ducked underneath.

04

H.264 MP4

ffmpeg encodes the frames and loudness-normalized audio into a share-ready .mp4 (crf 16, faststart).

Quickstart

Point it at your app, let introspect scaffold the config, tweak, render.

0Get the kit

terminal
# not published to npm — the SDK lives in this repo at packages/sdk
cd packages/sdk && pnpm install     # pulls Playwright + Chromium
npm link                            # optional: makes `explainer` available everywhere
# or skip the link and call it by path: node packages/sdk/cli.mjs
# ffmpeg must be on your PATH (brew install ffmpeg)

The package is not on npm — it's vendored in this repo. Playwright pulls Chromium on install; ffmpeg is a system dependency the CLI shells out to for mixing and encoding.

1Introspect your app

terminal
cd ~/code/your-frontend-app     # a Next / Vite / Astro / SvelteKit / ... project
explainer introspect             # boots the dev script, records, scaffolds the config

explainer introspect --url http://localhost:3000   # app already running (localhost only)
explainer introspect --max-pages 4 --timeout 120

Run it from your project root: it reads package.json + README for brand defaults, boots your own dev script, records the running app, and writes a draft explainer.config.generated.mjs. Honest caveat: it refuses to run outside a Node frontend project, and the browser is hard-limited to localhost.

2Tweak the config

explainer.config.mjs
// explainer.config.mjs — in your project root
import { defineConfig, theme, intro, cards, demo, outro } from "@shwcase/sdk/config";

export default defineConfig({
  out: "explainer.mp4",
  width: 1920, height: 1080, fps: 30,

  brand: {
    name: "parlays.live",       // wordmark; any "." is auto-accented
    logo: "./logo.png",         // square logo, relative to this config
    tagline: "On-chain parlays on Hyperliquid",
  },

  theme: theme({ accent: "#3b7bf0", accent2: "#6ea0ff", bg: "#06080d" }),
  voice: { provider: "elevenlabs", voiceId: "nPczCjzI2devNBz1zQrb", apiKeyEnv: "ELEVENLABS_API_KEY" },
  music: { enabled: true },

  scenes: [
    intro({ words: ["BUILD.", "PRICE.", "WIN."],
      narration: "This is parlays dot live." }),

    cards({ kicker: "THE BET", title: "What is a parlay?",
      cards: [
        { label: "LEG 1", title: "BTC above $100k", value: "2.10x" },
        { label: "LEG 2", title: "ETH new high", value: "2.40x" },
      ],
      result: { label: "2-LEG PARLAY", sub: "both must win",
        value: "5.04x", count: [1, 5.04, 2], suffix: "x" },
      narration: "A parlay is one bet on several outcomes." }),

    demo({ title: "Home", shotPattern: "https://demoshots.local/…/f_%05d.jpg",
      shotCount: 180, shotFps: 30,
      narration: "Here it is, live." }),

    outro({ chips: ["Correlation-priced", "Fully collateralized", "Gasless"],
      narration: "parlays dot live." }),
  ],
});

Build scenes with the helpers from @shwcase/sdk/config — they validate fields as you go, and defineConfig checks the whole config before a single frame renders. Each scene's narration length sets its on-screen duration; titles and notes accept inline HTML.

3Render

terminal
export ELEVENLABS_API_KEY=sk_...
explainer                       # reads explainer.config.{mjs,js,json} from cwd
explainer my.config.mjs         # or point it at a specific config
node packages/sdk/cli.mjs       # equivalent, without the link

Output lands at out (default explainer.mp4). Temp frames and audio go in .explainer-tmp/, auto-cleaned after the encode.

4Or call it programmatically

render.mjs
import { createVideo, introspect } from "@shwcase/sdk";  // via npm link / workspace
import config from "./explainer.config.mjs";

const { out, duration } = await createVideo(config, { cwd: process.cwd() });
console.log(`wrote ${out} (${duration.toFixed(1)}s)`);

// or scaffold programmatically (same security gate as the CLI):
await introspect({ cwd: process.cwd(), maxPages: 5 });

createVideo resolves paths against cwd and returns the output path and final duration in seconds.

Introspect

explainer introspect turns a running codebase into a first-draft explainer. Here's exactly what it captures — and what it will and won't do.

Brand defaults

package.json name + description and the README's first heading and paragraph become brand.name and the tagline.

Screen recordings

A 1920×1080 Chromium context records the root page and up to a handful of same-origin nav links — each page settles, then slow-scrolls so the footage has motion.

Full-page screenshots

One full-quality PNG per visited page, saved next to the videos under .explainer-introspect/ (auto-gitignored).

Demo-ready frames

Each recording is exploded into a JPEG frame sequence with ffmpeg, wired straight into a demo scene in the generated config.

A draft config

explainer.config.generated.mjs — intro from your brand, one demo scene per recording, placeholder cards + outro, all built with the config helpers. Existing files are never overwritten.

Honest guardrails

Refuses to run outside a Node frontend project, only ever executes the project's own dev script, and hard-blocks any navigation (including redirects) off localhost.

The security gate, plainly: it only runs when ./package.json declares a known frontend framework (next, react-scripts, vite, astro, @sveltejs/kit, remix, nuxt, gatsby — or a dev script plus a frontend UI lib). The only command it ever executes is your package manager's dev script (detected by lockfile), plus local ffmpeg for frame extraction. Playwright navigation is intercepted per-request: any document load — including redirect targets — that isn't localhost/127.0.0.1 is aborted.

Config builders

The config is one big literal — @shwcase/sdk/config gives you validated constructors for it instead.

@shwcase/sdk/config
import { defineConfig, theme, SCENE_TYPES,
  intro, demo, cards, versus, stat, pipeline, merge, outro,
  hook, flashwords, finale, logodraw, glitchline, signoff,
  hero, swap, tags, combine } from "@shwcase/sdk/config";

// each helper returns a correctly-shaped scene and rejects typo'd fields:
intro({ wordz: ["OOPS."] });
// -> intro(...): unknown field "wordz". Accepted: kicker, narration, lead,
//    tail, min, hideMark, legend, words, tagline, bullets

// defineConfig validates the whole thing against the real layout registry:
defineConfig({ brand: { name: "x" }, scenes: [{ type: "explosion" }] });
// -> Invalid explainer config:
//      - brand.logo (path to a square logo image) is required
//      - scenes[0] has unknown type "explosion" (known types: intro, demo, ...)

theme({ accent: "#3b7bf0" });   // merged over the kit's DEFAULT_THEME

One helper per scene layout, each accepting exactly the fields its layout reads (plus the shared kicker / narration / lead / tail / min timing fields). Errors are readable and cite the scene index, so a bad config dies in milliseconds instead of mid-render.

Scene types

Eighteen config-driven layout generators. Compose them in any order — each one animates itself via declarative data-a hooks.

intro

Word flashes, then a logo slam on the music drop

cards

Card row with an optional glowing count-up result

versus

Two opposing cards around a vs divider, plus chips

stat

Chart, strikethrough, and a big animated counter

merge

Inputs drawn together into one glowing output card

pipeline

Step-by-step flow with arrows and glow highlights

outro

Chip stack, logo, wordmark, and tagline endcard

hook

Stacked kinetic headline lines, clip-revealed

flashwords

Rapid centered word-by-word kinetic montage

finale

Held end statement with stamp and self-drawing mark

logodraw

Logo mark line-draws itself in, centered

glitchline

RGB-split glitch line with a struck-through fragment

signoff

Loading bar, strobe, then a CRT power-off to black

hero

Giant gradient number with count-up and tag chips

swap

Struck-out old value replaced by a huge new counter

tags

Oversized tag lines stacked and clip-revealed

combine

Dual scrolling marquees feeding a central slip

demo

Framed screen-recording playback from a frame sequence

Built in

Voiceover, three ways

ElevenLabs neural TTS (set ELEVENLABS_API_KEY), macOS say, or none — narration length sets each scene's duration.

Procedural synth music

An original royalty-free track — pad, plucked arp, four-on-the-floor kick — sidechain-compressed under the voice.

Broadcast loudness

The final mix is loudness-normalized to −15 LUFS with a −1.5 dBTP ceiling, so it sounds right everywhere.

Declarative animation

Layouts emit HTML with data-a hooks — reveal, pop, draw, count, strike, flash — resolved deterministically per frame.

Landscape or vertical

1920×1080 by default; set any width/height — portrait configs restack every layout for 9:16 shorts.

Fast visual checks

Pass _sampleFrames: [2, 9, 18] to dump single PNGs at chosen timestamps and skip the full render.