@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.
How it works
A four-stage pipeline: your narration drives the timing, the browser renders the pixels, ffmpeg does the rest.
One config file
Drop an explainer.config.mjs in your project root — brand, theme, voice, and a list of scenes.
Deterministic render
Headless Chromium (Playwright) renders every scene frame-by-frame at 30fps — perfectly smooth, no dropped frames.
Voice + music mix
Narration (ElevenLabs or macOS say) drives scene timing; an original synth track is sidechain-ducked underneath.
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
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
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
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
Output lands at out (default explainer.mp4). Temp frames and audio go in .explainer-tmp/, auto-cleaned after the encode.
4Or call it programmatically
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.
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.
Word flashes, then a logo slam on the music drop
Card row with an optional glowing count-up result
Two opposing cards around a vs divider, plus chips
Chart, strikethrough, and a big animated counter
Inputs drawn together into one glowing output card
Step-by-step flow with arrows and glow highlights
Chip stack, logo, wordmark, and tagline endcard
Stacked kinetic headline lines, clip-revealed
Rapid centered word-by-word kinetic montage
Held end statement with stamp and self-drawing mark
Logo mark line-draws itself in, centered
RGB-split glitch line with a struck-through fragment
Loading bar, strobe, then a CRT power-off to black
Giant gradient number with count-up and tag chips
Struck-out old value replaced by a huge new counter
Oversized tag lines stacked and clip-revealed
Dual scrolling marquees feeding a central slip
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.