remotion-video — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited remotion-video (Agent Skill) and scored it 83/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 4 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 4 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Turn a PR into a rendered short-form promo video with a hook, code moments, and CTA, using Remotion. Iterate with live preview in Remotion Studio, then render video.mp4 and poster.jpg for X/LinkedIn/social.
Unlike /video-script (which produces a textual script), this skill produces the actual video file.
Invoke when the user says: "make a video for this PR", "remotion video", "render a promo video", "video for X/LinkedIn", or when selected from /marketing-pipeline.
Resolve input
→ Phase 1: Discovery
→ Phase 2: Configuration
→ Phase 3: Narrative planning
→ Phase 4: Scaffold
→ Phase 5: First draft + iterate
→ Phase 6: Render
→ Phase 7: CleanupPhases 1, 3, 5, and 7 have explicit approval gates. Phase 2 is an interactive Q&A. Phase 5 is a freeform iteration loop that can run many rounds.
When the user invokes the skill with phrasing like "use sane defaults", "don't ask questions", "non-interactive", "just ship it", or any equivalent — interpret it precisely:
It DOES override (skip the prompt, pick the default):
marketing/<feature-slug>/remotion/It does NOT override (must always run regardless):
If you're tempted to skip a HARD-GATE because the user "said no questions" — re-read this section. The user said no questions, not no gates.
Resolve the argument (if provided) in this order:
.md containing "Executive Summary" or "Key Messages") → marketing brief.md with blog post structure) → blog post#\d+ pattern → PR<ref>..<ref> or <ref>...<ref> (alphanumeric + /, _, ., - on each side) → git ref rangeIf no argument is provided, ask: "What should the video be about? You can provide a PR URL/number, marketing brief, blog post, changelog, git ref range, file/directory path, or just describe the feature."
When invoked from the pipeline with a PR and upstream marketing-brief/blog-post paths, read both: PR for technical accuracy, upstream content for positioning/tone.
(Detailed phase specs begin below — see Phase 1.)
remotion-best-practices availabilityAttempt to invoke the remotion-best-practices skill via the Skill tool. If unavailable, present:
"The remotion-best-practices skill isn't installed. Options: a) proceed with baseline Remotion knowledge (quality may be reduced) b) wait while you install it c) cancel"If the user picks (a), emit a warning in the final summary noting reduced quality.
| Input type | What to read |
|---|---|
| Marketing brief | positioning, key messages, audience |
| Blog post | headline, narrative, examples |
| Changelog | highest-impact entry |
| PR | gh pr view <n> --json title,body,files,labels, diff (gh pr diff), commit messages, linked issues |
| Git refs | git diff <range> + git log <range> --oneline |
| Codebase path | read the specified files/directories |
| Freeform | parse the user's description |
For PRs with 20+ files, filter to user-facing changes only — skip tests/, ci/, .github/, lockfile changes, dep bumps.
Error handling:
gh not available → tell the user, ask for an alternative (diff file, freeform description)When the input is a PR, the brief table in Step 1.2 is not enough. PR bodies are routinely vague, outdated, or focused on implementation rather than user value, and a video built off the body alone tends to overclaim or miss the headline angle entirely. Before continuing to Step 1.3, run this structured analysis and produce a written PR analysis block that becomes the source of truth for Steps 1.4 (scope confirmation), Q2.1 (duration ladder), and Phase 3 (narrative planning).
Mandatory steps — do all of them:
gh pr view <n> --json number,title,body,baseRefName,headRefName,files,labels,commits,additions,deletionsRecord baseRefName (usually main / master / develop) — that is the diff reference. gh pr diff <n> automatically compares the PR head against this base.
gh pr diff <n>For very large PRs (>500 lines or >20 files), also list changed files via gh pr view <n> --json files.
tests/, __tests__/, *.test.*, *.spec.*, __mocks__/, ci/, .github/, lockfiles, dep bumps without behavior change, generated/build artifacts (dist/, build/, .next/), and formatting-only diffs. What remains is the user-facing surface of the PR.exports (functions, types, components, hooks, classes, constants)Grep the diff for added lines beginning with export , new top-level function/class/const in src/ / lib/ / packages/*/src/, new files in those trees, and changes to public type signatures. If you cannot point at a line in the diff for a claimed API, the claim is wrong — drop it.
The "had to" half must be a real prior workflow you can describe — copy-pasting an adapter from the docs, installing a second package, writing boilerplate, hitting an error, switching to a different tool. If the honest "Before" sentence is "they could already do this", the PR has no user-visible value delta — see step 8.
"This PR looks like an internal refactor / test-only / dep-bump PR — I can't find a user-visible value delta. Options: a) shift to a performance / DX / cleanup angle if numbers support it b) pick a different PR or input c) cancel"
Do not invent a feature angle to fill the gap. A confabulated angle wastes a full iteration round and destroys user trust on the very first draft.
PR analysis — #1234 "Add fromZodSchema support"
Base: main · Head: feature/zod-schema · Author: <login>
User-facing files: 3 (packages/core/src/index.ts, packages/core/src/zod.ts, packages/core/src/types.ts)
Filtered out: 5 test files, 2 doc files, lockfile
Public-API delta:
+ export function fromZodSchema(schema: ZodSchema): StandardSchema
+ export type ZodCompatibleSchema
~ default error code for ZodError changed: 'invalid_type' → 'STANDARD/type'
Behavior delta:
- Users importing zod schemas no longer need a manual adapter.
- Error messages from zod paths now use the StandardError shape.
Before / after:
Before: copy a 15-line adapter from the docs into every project that mixes zod with this library.
After: one import + one call.
Surprises (in diff but not in PR body):
- Default error shape change (above) — could be the headline angle for migration-aware audiences.
Headline angle: "drop the 15-line adapter — one import, one call"
Scope band for Q2.1: one idea (15–25s)The "Headline angle" line feeds Phase 3.0's motif derivation and Phase 3.3's hook copy. The "Scope band" line feeds Q2.1's scope-derived duration proposal.
For git-ref-range and codebase-path inputs, apply the same structure with the diff coming from git diff <range> / direct file reads in place of gh pr diff. Steps 4–9 (public-API delta → analysis block) are not PR-specific.
Read if they exist: README.md, docs/, package.json. If nothing found, ask: "Can you briefly describe the product and who it's for?"
For PR inputs, the bullets and the compelling angle below must be derived from the PR analysis block written in Step 1.2a — not from the PR title or body. Quote the "Headline angle" line directly as the proposed story seed; turn the public-API delta and the before/after statement into the bullets. If the user added clarifying context after Step 1.2a, fold it in here.
For non-PR inputs, derive the bullets from the relevant entry in Step 1.2 (marketing brief positioning, blog headline, changelog highlight, codebase reading, freeform description).
Present:
"Here's what I'll base the video on: - [feature summary bullet 1 — concrete, grounded in the diff / source] - [feature summary bullet 2 — the before → after value, in one line]
>
The compelling angle: [proposed story seed — for PRs, the "Headline angle" from the analysis block]
>
Anything to add, remove, or correct?"
Do not proceed until the user confirms.
Before continuing, scan for: security patches, internal pricing, credentials, unreleased roadmap items, content marked confidential. Flag anything questionable to the user.
Ask these questions one at a time, in order.
Do not ask the user to pick a fixed length from a menu. A fixed number becomes a constraint, and the dominant failure mode is padding — freeze-frames, repeated beats, black or empty trailing frames, or filler bullets added solely to reach the chosen target. Instead, derive a duration from the scope of the change and present it as a proposal the user can confirm or override.
Allowed range: 15–60 seconds. Anything outside this window is wrong by default. Going under 15s means the story can't breathe; going over 60s means it's two videos.
Scope → duration ladder:
| Scope of the PR / feature | Distinct payoff beats | Target |
|---|---|---|
| One idea (single API addition, single bug fix, one QoL win) | hook + 1 delivery + CTA | 15–25s |
| Typical PR-sized feature (problem → solution → proof, or a multi-chapter code walk) | hook + 2–3 delivery + CTA | 25–40s |
| Multi-faceted release (multiple distinct sub-features, or comparison needing problem + solution + proof beats) | hook + 3–4 delivery + CTA | 40–60s |
A distinct payoff beat is one new thing the viewer learns. Two scenes whose payoff sentences (see Phase 3.3 item 1) reduce to the same idea are one beat, not two.
Sanity check before proposing — do this silently first:
Propose to the user (no menu, no fixed lengths):
"Based on the scope of this PR, I'm targeting ~Xs (range Ys–Zs). Beats: <one short sentence listing the beats>. Confirm, or override with a different length anywhere in 15–60s."
Hard rule — story sets duration, never the other way around. If at any later phase a scene needs to be stretched, held on a static frame, repeated, backed by black/empty frames, or filled with filler bullets to reach the chosen target — stop, shorten the target, re-confirm with the user, and ship the shorter video. Padding to hit a number is the single behavior this rule exists to forbid. If a 30s scope honestly tells in 18s, ship 18s.
Breathing-room constraint. Whatever duration is chosen, it must allow every on-screen text element to (a) finish animating in, (b) dwell long enough to be read, and (c) settle for at least ~0.4s before the next scene begins. Cutting to a new scene the instant a line of text finishes appearing is forbidden. See Phase 3.3 (item 3) and the Phase 6 audit for the concrete dwell-time table.
"Which aspect ratio? 1. 16:9 landscape (1920×1080, default) — desktop X/LinkedIn 2. 1:1 square (1080×1080) — mobile-friendly feed 3. 9:16 vertical (1080×1920) — Reels/Shorts/TikTok 4. Multi-format — render all three from the same story"
Frame rate is fixed at 30fps. The audit thresholds throughout this skill are expressed in absolute frame counts assuming 30fps; do NOT change fps in remotion.config.ts without simultaneously updating every frame-based threshold (Phase 6 pre-render audit, the dwell-time table, the breathing-room rules, chapters-required threshold).
"Where should the Remotion project live? Default: marketing/<feature-slug>/remotion/ (fresh per-video) Override: specify a path."<HARD-GATE> *The brand scan is mandatory. It is not skippable under any user instruction — including "use sane defaults", "don't ask questions", "just ship it", or "non-interactive". Those instructions affect interactive confirmation; they do NOT affect detection.*
You MUST run the heuristics in brand-detection.md against the actual target repository — the source code that owns the feature, not the marketing/ output directory — before selecting any color, font, or logo.
Forbidden shortcuts (these produce wrong colors and waste an iteration):
The scan must produce a written record before any composition file is written. Output a short block listing, for each field: the source file checked, the value found (or not found), and the final value used. Example:
Brand scan — TanStack/ai
Primary : checked tailwind.config.* (none) · packages/*/styles.css (--brand: #0a3d2e) → #0a3d2e
Accent : checked theme.json (none) · brand.json (none) · derived from primary → #14b870
Logo : checked public/logo.svg → public/logo.svg (will be referenced from src/brand.ts)
Font : checked next/font (none) · @fontsource (none) · README ref → Inter (fallback)If the scan finds nothing for a field, fall through to the neutral defaults below — but only after the scan ran and is recorded. Skipping the scan and going straight to defaults is the failure mode this gate exists to prevent.
If you cannot find a logo, ask the user — do NOT invent one. The same rule applies to any field where detection genuinely turned up nothing and no sensible neutral default exists.
The following are non-negotiable regardless of "no questions" mode:
marketing/<feature-slug>/remotion/ path or override)In interactive mode, present findings (the block above) and ask for confirmation. In non-interactive / "use sane defaults" mode, print the same block and proceed without asking — the record is required either way. </HARD-GATE>
Present findings as:
"I found: - Logo:public/logo.svg- Primary:#0066ff(fromtailwind.config.js) - Font:Inter(fromnext/font)
>
Use these, customize some, or provide your own?"
Persistence: write chosen brand to marketing/<feature-slug>/remotion/.marketing/brand.json (mirrors the hyperframes-video location). On subsequent runs, ask:
"I loaded brand settings from marketing/<feature-slug>/remotion/.marketing/brand.json. Use saved, or re-detect?"Fallback when nothing detected: ask explicitly with these neutral defaults. These are intentionally neutral — auto-detection of the project's actual brand is always preferred, and these values should only appear when detection turns up nothing.
#3B82F6 (neutral blue)#8B5CF6 (neutral violet)#0A0A0A (near-black, dark mode)#FFFFFF (white)#9CA3AF#22C55E#EF4444Geist (loaded automatically via @remotion/google-fonts)Confirm with the user before scaffolding.
Note on fonts: The default scaffold loads Geist via @remotion/google-fonts and wires it into brand.font.family. To use a different Google Font, edit src/fonts.ts to import from a different @remotion/google-fonts/<Name> module. brand.font.googleFont is the display name used by the skill to decide which module to load. Non-Google fonts require manual wiring.
Before picking a story pattern, identify the signature visual motif that will carry the narrative across scenes. This is the single most important call for not looking generic.
references/visual-motifs.md — the catalog maps 20+ common verbs to motifs, state axes, and example custom elements. If the verb isn't listed, apply the heuristic in that file.Confirm with user:
"The core verb is compose music, so I'll use an animated waveform as the signature motif — smooth/clean in the hook and solution, jagged/red in the problem. This thread will appear in scenes 1, 2, and 4. Approve, pick a different motif, or let me propose alternatives?"
Do NOT skip this step. A video without a derived motif defaults to generic bullet-list storytelling and fails the generic test (Storytelling Rule 10).
Scan the PR/input for signals and pick one of 5 patterns. See the detection signals table in patterns/README.md — that file is the single source of truth.
Load the matching pattern spec from patterns/<pattern>.md.
Confirm with user:
"This looks like an [API/Library feature] PR. I'll use that story template. Override? Options: api-library / ui / performance / bugfix / generic / describe a custom pattern"
Per pattern:
Offer user override:
"For code snippets, I'll synthesize realistic usage examples rather than paste raw diff. Override: use-diff / synthesize / mix"
Before synthesizing usage code for a PR, verify the library's actual public API. Do not invent method names, argument shapes, or import paths.
For each snippet the skill plans to include:
packages/<name>/src/index.ts, docs examples in docs/, test fixtures in tests/).If the library isn't available locally and the skill can't verify, ask the user before synthesizing. A wrong API in the first draft destroys user trust and wastes a full iteration round.
This step has two parts: a silent self-improvement loop that you run before the user sees anything, and the user-facing approval gate that follows.
Do not show the user the first thing you wrote. The first version of a scene plan is almost always weaker than the second. The agent's job here is to hand the user the strongest plan it can build given the rules in this skill, not the first draft.
#### Self-improvement loop (silent — run before presenting)
After drafting an initial scene plan, run a deliberate improvement pass against the rule sections listed below. Iterate at least twice. Stop only when one full pass produces zero further changes — the draft has stabilized.
On every pass, for each scene and for the plan as a whole, ask: "Does this satisfy this rule? If not, can I rewrite, merge, drop, split, or reorder a scene to fix it?" Apply the fix in-place, then continue the pass.
The five Core checks below must be satisfied — every plan, no exceptions. On top of those, scan against the broader rule sections at the end of this step.
Core checks (every plan must satisfy all five):
Reject plans where the shortest and longest scene differ by less than ~2×. Equal-slice plans are the single strongest "AI-generated" tell. Reject plans where the CTA is under 4s — a rushed CTA destroys the conversion the rest of the video bought.
| Element | Minimum dwell |
|---|---|
| Short headline / one phrase (≤6 words) | ≥ 1.5s (≥45 frames at 30fps) |
| Long headline / single sentence (7–14 words) | ≥ 2.5s (≥75 frames) |
| Two-line text / short paragraph (15–30 words) | ≥ 3.5s (≥105 frames) |
| Code chapter (per chapter, after focus lands) | ≥ 3s (≥90 frames) |
| CTA URL / handle (must be clearly readable) | ≥ 3s (≥90 frames) |
Every scene must also include a ~0.4s settle hold (≥12 frames at 30fps) between the last animation completing and the scene transitioning out. Cutting on the same frame an animation finishes is forbidden — the eye needs a beat to confirm what it saw, and transitions that arrive on the resolve-frame feel cluttered and amateur. If the proposed durationFrames cannot accommodate these minima, shorten the beat list, do not shrink the dwell times.
Broader rule sections to scan on every improvement pass. Search this file (or the linked file) for each section heading and walk it against the current draft:
hooks/hook-rules.md) — applied to the HookTitle scene. If it fails any check, rewrite the headline; don't just record the failure./*…*/, pre-break long imports.patterns/<pattern>.md) — the scene sequence and payoffs should be coherent with the template; deviations must have a clear justification.Improvements you may apply silently during the loop (no user question needed):
durationFrames to satisfy pacing variance and breathing-room minima — only by trimming, never by stretching or paddingChanges that must wait for the user — flag them, do not apply silently:
Stop condition. End the loop when one full pass over all rule sections (Core checks + broader sections) produces zero changes. If you hit five passes without stabilizing, you have a structural problem the loop can't fix — stop and ask the user for guidance instead of churning.
#### Present the (already-improved) scene plan for approval
When presenting to the user, include a brief "Self-review notes" line near the top of the response so the user can see the improvement work was actually done. List the 2–5 most material changes the loop applied. Example:
Self-review notes: tightened the hook line from "Add validation easily" to "Swap validation libs with one line"; merged a redundant "why it matters" scene into the problem setup; trimmed delivery 14s → 11s so the CTA dwells ≥4s.
If the loop produced no material changes (rare — usually means the first draft was already strong, or the agent isn't pushing hard enough), say so explicitly: "Self-review notes: draft was stable on first pass; no rewrites needed."
Example output:
"Here's the plan (30s target):
>
1. HookTitle (0–3s, 90f) —"Swap validation libs with one line"— payoff: there's one line that replaces N SDKs 2. ProblemSetup (3–8s, 150f) — three evidence cards with concrete conflicting API shapes — payoff: viewer sees the real API-shape conflict they live with today 3. LibrarySwap (8–22s, 420f) — shared Standard Schema code, import line cycles zod → valibot → arktype — payoff: viewer sees the "one line change" literally happen on screen 4. CTAEndScreen (22–30s, 240f) —"Ship it"+ link to standardschema.dev — payoff: viewer knows exactly where to go next
>
Pacing: hook 3s / problem 5s / delivery 14s / CTA 8s (ratio ~4.7×) — passes variance check. Motif: schema-interop glyph (interlocking rings) appears in scenes 1, 2, 4; rings are disconnected in scene 2, unified in 1 and 4.
>
Approve or adjust any section?"
Do not scaffold until the user approves the scene plan.
See patterns/README.md for how patterns map to scene plans.
Hard cuts between scenes are acceptable; match-cuts are what make a video feel crafted. When the signature motif (Phase 3.0) appears in adjacent scenes, the motif must carry over as a match-cut, not restart from zero.
Rules:
SceneBackground variant must change between adjacent scenes (no two adjacent scenes share a variant). This is enforced at pre-render.Write the planned transitions into the scene plan before scaffolding:
Transition 1→2: waveform carries over at the same position; color fades from brand.primary → brand.danger; amplitude jitters from smooth to glitchy over 8 frames. Transition 2→3: glitch waveform contracts into a flat line → that line becomes the top border of the code card in scene 3. Transition 3→4: active-provider pill in scene 3 slides down and morphs into the URL pill of the CTA.
If the feature has a visible UI surface — a generated image, a rendered audio player, a dashboard, a settings toggle, a diff view — the video must include at least one beat that shows that surface. Research on product-launch video is consistent: "show the product in action" is the strongest single predictor of viewer recall.
Options (in order of preference):
<Video src={...}/> in a Custom scene.Place the UI moment in the delivery scene (Scene 3 in the default structure), ideally at its midpoint so the viewer gets code-plus-result. A code-only delivery feels like a reference doc; code-plus-UI feels like a demo.
Skip this step only if the feature is purely API-level with no user-visible surface (a parser, a compiler pass, a type-level utility).
When the user picked option 4 (Multi-format) in Q2.2, the skill must plan per-aspect narrative variants — a 9:16 vertical video is not just a cropped 16:9.
#### Rule 0 (the load-bearing one): Fill the canvas. Don't just shrink content.
The most common failure mode when porting a 16:9 layout to 1:1 or 9:16: the agent keeps the original element sizes, only changes Composition width and height, and ships a video where content occupies ~50% of the new canvas with huge dead margins on top/bottom (vertical) or sides (square).
The right move is the opposite of intuition: when the canvas gets smaller in one axis, the content's per-element size needs to get bigger, not smaller. Less competing content = each element earns more visual space.
Concrete defaults when porting from 1920×1080 (landscape) to other aspects:
Stack-style flex gap of 56px on landscape becomes ~80px on 9:16. Whitespace is doing brand work — keep it generous.Implementation pattern in Remotion: read the current aspect from useVideoConfig() (or a context) inside each scene/Panel and switch sizing values via a small byAspect({ landscape, square, vertical }) helper, instead of hardcoding pixel sizes. The same Story data drives all three compositions; only the per-aspect sizing layer differs.
Run the fill check mentally before rendering and as a hard pre-render audit:
At the hero frame (peak content) of every scene at every aspect, the bounding box of the foreground content reaches within ~80–100px of every canvas edge. If any scene at any aspect has more than ~120px of dead margin on either axis, the per-aspect sizing is wrong — the content is undersized.
If you're tempted to keep all the landscape sizes "since they look fine on landscape" — stop. They look fine on landscape because they fill landscape. They will not fill a different aspect at the same sizes.
#### Layout-shape adjustments
BeforeAfter, multi-pill pill rows, and wide LibrarySwap layouts must switch to stacked (top/bottom) or single-item-at-a-time layouts on 9:16. Mark these scenes with an aspectOverrides hint during scene planning, and branch the JSX in the scene component on useVideoConfig().width / height ratio.tanstack.com/ai instead of https://tanstack.com/ai/audio-generation) for 9:16.Write the per-aspect deltas into the scene plan as a short section, even if most scenes render identically. Example:
9:16 deltas: Scene 3 LibrarySwap stacks the code card above the provider pills (same content, vertical flow). Headline 132px → 144px (kept large). Padding 32px → 200px top/bottom. Code font 18px → 18px (held). Total duration shaved to 26s (hook 2.5s / problem 4s / delivery 12s / CTA 7.5s).Skip this step if the user picked a single aspect.
(Caption emphasis syntax, text alignment, background variants, and code-block theming are governed by the Layout and Typography Rules section below.)
At the location chosen in Q2.3 (default marketing/<feature-slug>/remotion/):
<project-root>/
├── package.json
├── tsconfig.json
├── remotion.config.ts
├── src/
│ ├── index.ts # Entry point: registerRoot + font side-effect
│ ├── Root.tsx # Composition registry
│ ├── Main.tsx # Scene dispatcher
│ ├── story.ts # Typed story JSON (written by skill from scene plan)
│ ├── story-types.ts # Scene union type (source of truth)
│ ├── brand.ts # Brand tokens (extended palette: primary/accent/background/text/muted/success/danger)
│ ├── fonts.ts # Loads Geist via @remotion/google-fonts — default font
│ ├── highlight.tsx # <Highlight text="**word**" /> component for caption emphasis
│ ├── highlighted-code.tsx # Shiki-based code highlighter
│ ├── scenes/ # Copied from templates/scenes/
│ └── assets/ # Copied logo, intro/outro if anyCopy files from skills/remotion-video/templates/project/ (stripping the .template suffix):
package.json.template → package.jsontsconfig.json.template → tsconfig.jsonremotion.config.ts.template → remotion.config.tsindex.ts.template → src/index.ts (entry point — calls registerRoot and imports ./fonts for side-effect)Root.tsx.template → src/Root.tsxbrand.ts.template → src/brand.ts (then populate with Q2.4 values)story-types.ts.template → src/story-types.ts (scene union type — single source of truth)fonts.ts.template → src/fonts.ts (loads Geist by default via @remotion/google-fonts/Geist)highlight.tsx.template → src/highlight.tsx (text emphasis helper — renders **word** in primary color)shiki-theme.ts.template → src/shiki-theme.ts (brand-derived Shiki theme — keywords in primary, strings in accent, comments in muted, background transparent)highlighted-code.tsx.template → src/highlighted-code.tsx (wraps shiki for Remotion async frame capture; defaults to brandShikiTheme)scene-background.tsx.template → src/scene-background.tsx (brand-tinted scene wrapper — every scene uses this)Main.tsx.template → src/Main.tsx (scene dispatcher)Copy all .tsx files from skills/remotion-video/templates/scenes/ into <project-root>/src/scenes/.
Detect package manager from lockfile (pnpm-lock.yaml → pnpm, bun.lockb → bun, yarn.lock → yarn, package-lock.json → npm). Install:
remotion @remotion/cli @remotion/google-fonts shiki react react-dom typescript @types/react @types/react-dom @types/nodeExample (pnpm):
pnpm --dir marketing/<feature-slug>/remotion installstory.tsPopulate from the approved scene plan. The Story and Scene types are defined in src/story-types.ts (copied from templates/project/story-types.ts.template). See that file for the full type definitions.
src/Main.tsx for Custom scenesThe skill copies Main.tsx.template → src/Main.tsx in Step 4.1. That base file dispatches all 9 bundled scene types. Whenever a story plan uses a Custom scene, the skill must extend the renderScene switch in src/Main.tsx: add the import for the custom component and a case for its componentName that renders it with its props (instead of throwing). The base template throws on Custom so missing dispatch is loud, not silent.
UIShowcase note: When a UIShowcase scene references a mockComponent (instead of or in addition to mediaSrc), the skill must extend the UIShowcase dispatch case in Main.tsx to resolve the component name to an actual React element and pass it as the mockComponentNode prop. The default dispatch only handles mediaSrc (image/video files). Mock-component UI mocks follow the same "skill extends the switch" pattern as Custom scenes.
When the user picked option 4 (Multi-format) in Q2.2, the skill modifies src/Root.tsx to register three compositions instead of one, each passing the same Main component with a different width/height:
MainLandscape — 1920×1080MainSquare — 1080×1080MainVertical — 1080×1920All three share the same durationInFrames and fps from story.meta. The base Root.tsx.template ships with a single Main composition by default; the skill replaces that block with the three-composition block when multi-format is selected.
When the story plan calls for a scene shape the 9 bundled templates can't express, generate a custom scene under src/scenes/custom/<Name>.tsx.
Rules (enforced by the skill):
remotion, shiki, react, ../../highlighted-code, ../../scene-background, and ../../brandtsc --noEmit) before render — skill runs typecheck and self-corrects up to 2 times on failureinterpolate / spring for animations (no setTimeout, no external animation libs)durationFrames from its Scene entrysrc/Root.tsx alongside the bundled scenesUse remotion-best-practices skill when available for guidance. Fallback to baseline Remotion docs patterns.
Run as a background process, invoking the scaffolded project via --dir:
pnpm --dir marketing/<feature-slug>/remotion exec remotion studio --port 3000(Fall back: try 3001, 3002, 3003 if 3000 is in use. Fail loud if all taken.)
Capture the URL and present to the user:
"Studio is running at http://localhost:3000. Open it in your browser and review the first draft.
>
What do you want to change? (freeform — 'make the hook punchier', 'swap scenes 2 and 3', 'use arktype instead of yup', 'drop the problem scene', 'longer pause on the code', anything you want.)"
Parse the user's request and decide which file to edit:
| Request kind | File to edit |
|---|---|
| Content / copy change | src/story.ts (scene's text, caption, code fields) |
| Timing / ordering change | src/story.ts (reorder array, update durationFrames) |
| Visual / animation tweak on a bundled scene | src/scenes/<Scene>.tsx |
| Novel visual request the library can't express | src/scenes/custom/<Name>.tsx (create new) + src/Root.tsx (register) + src/story.ts (reference with { type: "Custom", componentName: "<Name>", props: {...} }) |
| Brand change (colors, font, logo) | src/brand.ts |
After each edit:
pnpm exec tsc --noEmit silentlyRe-prompt:
"Saved. Studio should have refreshed. Next change, or ready to render?"
User signals "ready to render", "looks good", "ship it", "render", "done", or similar → move to Phase 6.
Each iteration stores a snapshot of src/story.ts content in an in-memory session history. If the iteration loop hits 10 rounds without approval:
"We've iterated 10 times. Options: 1. Reset to an earlier draft (I'll show the history) 2. Keep going 3. Render the current draft as-is"
If the user picks (1), show a numbered list of snapshot summaries (hook text of each draft) and let them pick.
#### Render quality is not optional
The Studio preview is pristine; the render pipeline must preserve that. Two structural problems ruin h264 output of otherwise-perfect React scenes:
on dark backgrounds, thin colored lines, and subpixel anti-aliasing get blurred. The fix is supersampling: render at 2x composition dimensions (--scale 2) so ffmpeg downsamples from a sharper source.
The fix must be at the source, not at the codec: SceneBackground ships a subtle SVG noise overlay (NoiseDither) that dithers the quantization below perception. 8-bit h264 encoding is the only format that plays reliably across Windows 11 Films & TV, Discord, browsers, VLC, and social upload pipelines. Do NOT default to 10-bit (yuv420p10le) — consumer GPU decoders often skip h264 high10 without falling back to software, producing black frames on Win11/Discord.
Defaults ship: --scale 2 + --image-format png + --crf 14 + --pixel-format yuv420p. A 30-40s 1080p video lands around 6-15 MB.
Edge case — 10-bit opt-in: the render:10bit npm script exists for creators who verified their end-to-end pipeline supports h264 high10 profile (e.g., shipping to a known YouTube upload that always transcodes, or to a target audience on mpv/VLC only). Never use as a default.
Never render with --image-format jpeg for a final. The JPEG quality-80 intermediate blurs text independently of anything else.
How `NoiseDither` works: an overlay layer inside SceneBackground renders an SVG feTurbulence fractalNoise filter at ~4% opacity in overlay blend mode. The noise breaks up the 8-bit gradient-quantization step boundaries perceptually, turning visible bands into faint grain. This is the same technique Figma and export pipelines use.
Before rendering, all of these must pass. If any fail, report exactly what and where, and do not render.
Technical checks:
pnpm exec tsc --noEmit passesscenes[].durationFrames / meta.fps is within the 15–60s window AND within ±20% of the scope-derived target proposed in Q2.1 (or matches an explicit user override). If the natural story comes in shorter than the proposed target, the target was wrong — re-derive from scope, do not pad.durationFrames endcode renders without shiki errors (test by invoking the shiki highlighter on each snippet)src/brand.ts exists on disk--scale 2 --pixel-format yuv420p (or remotion.config.ts sets both). 10-bit (yuv420p10le) is opt-in only; it breaks Win11 Films & TV and Discord inline playback.Storytelling audit (refuse to render if any fail — report to user and ask for a fix before proceeding):
hooks/hook-rules.md) pass on the HookTitle scene's textsrc/scenes/)max(scene.durationFrames) / min(scene.durationFrames) ≥ 2.0. Uniform-duration plans refuse to render.0.30 × total duration. If more than 30% of the video elapses before scene 3 begins, the value prop is buried.SceneBackground variants (prevents the "same color for 15 seconds" flatness)zIndex declaration. Verify any newly-added decorations against the rendered hero frame in Remotion Studio, not frame 0. See Layout Rule 7.chapters[]. Static emphasizedLines-only code dumps fail this check. See Code Scene Rules — Tell a story with chapters.chapters[] MUST have a per-chapter caption / title that swaps in lockstep with the focusLines change. A code highlight without a synchronized title saying why we're looking at it fails this check. See Code Scene Rules — Synchronized chapter narration.Stop the Remotion Studio background process first.
For a single-aspect video:
pnpm --dir marketing/<feature-slug>/remotion exec remotion render src/index.ts Main out/video.mp4 --codec h264 --crf 14 --image-format png --pixel-format yuv420p --scale 2
pnpm --dir marketing/<feature-slug>/remotion exec remotion still src/index.ts Main out/poster.jpg --frame 0 --image-format png --scale 2For multi-format (user picked 4 in Q2.2), render each composition:
pnpm --dir marketing/<feature-slug>/remotion exec remotion render src/index.ts MainLandscape out/video-landscape.mp4 --codec h264 --crf 14 --image-format png --pixel-format yuv420p --scale 2
pnpm --dir marketing/<feature-slug>/remotion exec remotion still src/index.ts MainLandscape out/poster-landscape.jpg --frame 0 --image-format png --scale 2
pnpm --dir marketing/<feature-slug>/remotion exec remotion render src/index.ts MainSquare out/video-square.mp4 --codec h264 --crf 14 --image-format png --pixel-format yuv420p --scale 2
pnpm --dir marketing/<feature-slug>/remotion exec remotion still src/index.ts MainSquare out/poster-square.jpg --frame 0 --image-format png --scale 2
pnpm --dir marketing/<feature-slug>/remotion exec remotion render src/index.ts MainVertical out/video-vertical.mp4 --codec h264 --crf 14 --image-format png --pixel-format yuv420p --scale 2
pnpm --dir marketing/<feature-slug>/remotion exec remotion still src/index.ts MainVertical out/poster-vertical.jpg --frame 0 --image-format png --scale 2Move artifacts from <project>/out/ to marketing/<feature-slug>/:
video.mp4 (or per-aspect files)poster.jpg (or per-aspect files)✓ Rendered: marketing/<feature-slug>/video.mp4 (30s, 16:9, <size>)
✓ Poster: marketing/<feature-slug>/poster.jpgAsk the user what to do with the scaffolded Remotion project:
"Video is ready. What do you want to do with the Remotion project at marketing/<feature-slug>/remotion/?>
1. Keep it — useful for re-rendering or variants later 2. Delete it — keep only the mp4 and poster 3. Archive — move to marketing/<feature-slug>/remotion.zip and delete the folder"Execute the chosen action. End.
The default failure mode of an AI-generated promo video is generic: a hook, three bullet points, a code block, a CTA. Every scene stacked vertically, same rhythm, same layout language as ten thousand other "new feature launch" reels. Viewers scroll past in under 2 seconds. Technically correct ≠ memorable.
These rules exist to push back against that default.
A vertical stack of bullet points with icons is the most overused and forgettable scene shape in dev-marketing video. When the story asks for a "list of pains / benefits / reasons", the skill MUST transform the list into a structure that visualizes what it's saying:
≠, ↯).Every beat should ideally have a form that only makes sense for that beat. If the same bullet shape could hold "our five pillars of happiness" or "three reasons our API is fast", it is too generic — redesign it.
Every promo video should have one recurring visual element tied to the subject matter — a "thread" that appears in 2–4 scenes (typically hook, problem, and CTA) and carries the theme.
The motif is derived in Phase 3.0 from the product's core verb. See references/visual-motifs.md for the full catalog (20+ verb → motif mappings) and the heuristic for deriving a motif when the verb isn't listed.
Key principles (full detail in the catalog):
When scaffolding, create a reusable component (e.g., src/audio-waveform.tsx, src/speed-streak.tsx, src/packet-flow.tsx) in the project root and import it into each relevant scene. Never rebuild the thread scene-by-scene.
The 9 bundled scenes (HookTitle, ProblemSetup, CodeSnippet, LibrarySwap, BeforeAfter, MetricCompare, BulletList, CTAEndScreen, UIShowcase) cover ~80% of cases and should be used when the story fits. They should NOT be the excuse for a lazy story.
If a beat's visual shape would be unique and memorable, create a Custom scene under src/scenes/custom/<Name>.tsx rather than bending a bundled scene into something it isn't. Examples that warrant Custom scenes:
The rule of thumb: if your first instinct is "I'll just use BulletList" and the content is interesting, stop. Either upgrade to a Custom scene or upgrade the bundled scene's internal storytelling (tilts, connection glyphs, staggered physics, per-item glyph variation).
Inside any scene that describes a pain or a benefit, prefer real, verifiable detail over abstract claims. This makes the video technically credible and visually unique because the evidence IS the design element.
| Abstract (generic) | Concrete (unique + credible) |
|---|---|
| "Every provider is different" | music_length_ms vs seconds_total vs duration shown side-by-side |
| "Our SDK is type-safe" | A screenshot-like TS diagnostic with red squiggles on the bad line |
| "10× faster" | Two progress bars racing, finishing at t=500ms and t=50ms |
| "Works anywhere" | Animated pills of the supported runtimes lighting up in sequence |
When pulling evidence from a PR or codebase, quote the actual symbol names, parameter names, or model ids that appear in the source. If a viewer who uses those libraries recognizes the exact string you're showing, you've won credibility and memorability in the same frame.
Every animation should deliver information, not just "look animated". If removing an animation would not change what the viewer understands, it is decoration and should be reconsidered.
Good:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.