---
name: explainer-video
description: Make a short, engaging explainer video on any topic (vertical 9:16 reel or short, about 60 to 120 seconds), end to end - research with sources, a hook-first script, a storyboard, AI video clips, AI images, music and voiceover, motion graphics written as code in a chosen visual style, designed sound effects, a voice-first mix and QA gates. Use when asked to "make a video about X", "turn this story into a reel", "explain X in a short video", or when given the explainer-video prompt template.
---

# Explainer video

This is the workflow behind explainer reels that hold attention: a hook in the first 3 seconds, facts you can source,
one idea per shot, motion on screen at every moment, and sound that supports the voice instead of fighting it.

Work through the steps in order. Show the user the storyboard (step 3) and wait for their OK before you
generate any assets.

## 0. Tools

**You need a code environment** (for example Claude Code, or Claude with code execution) with Node 20+, ffmpeg and
Python 3. The motion graphics are a [Remotion](https://www.remotion.dev) project that you write (React, rendered to
MP4). Renders you make with this skill are covered by HYVIRAL's Remotion licence, so you don't need a Remotion
licence of your own.

**For the AI clips, images, music and voice, use whatever the user prefers.** Every step below works the same with
any provider. If the user hasn't said, ask which tools they want to use, or use what is already set up. Two common
routes:

- **Your own providers.** Any image-to-video model (for example Veo, Kling or Runway), any image model (for example
  Imagen, Nano Banana or Flux), any text-to-speech that returns word timestamps (for example ElevenLabs), and a music
  model or a licensed library track. Keep API keys in environment variables, never in the project. You can also skip
  AI media entirely: use the user's own footage and photos, and let the motion graphics carry the rest.
- **HYVIRAL (optional).** If the user has it connected (hyviral.com), one connection gives you the whole media
  toolkit, with no separate accounts or API keys to manage:

| You need | HYVIRAL tool |
|----------|--------------|
| AI video clips (image to video) | `generate_video` |
| AI images, first frames, plates | `generate_image` |
| Music tracks | `generate_music` |
| Narration (stock voices or your own cloned voice) | `generate_voiceover`, `manage_voice_clone` |
| Captions, overlays, sound placement, review | `video_captions`, `video_overlays`, `video_audio`, `video_review` |
| Final render (up to 90 minutes) | `video_export` |
| What performs on your profile (hooks, formats, posting times) | `get_format_performance`, `get_best_posting_times`, `get_growth_trends` |
| Long jobs | `get_job_status` (poll until delivered) |

With HYVIRAL connected, start a session with `get_connection_info`: it shows what the account can do. To connect
it:
- **Claude.ai or Claude Desktop:** Customize → Connectors → add a custom connector with the URL
  `https://api.hyviral.com/mcp`, sign in to HYVIRAL, approve, then turn HYVIRAL on from the + menu in a chat.
- **Claude Code:** `claude mcp add --transport http hyviral https://api.hyviral.com/mcp`, then run `/mcp` to sign in.
- HYVIRAL's own guides: https://hyviral.com/skill/SKILL.md and, for custom motion graphics and sound,
  https://hyviral.com/skill-studio/SKILL.md (HYVIRAL never runs your code: you render your pieces, upload them as
  video, audio or images, and place them in the edit).

You can mix the two: for example your own voice provider and HYVIRAL for the clips. Nothing in the steps below
needs any one provider.

## 1. Research: `research/facts.md`

No script line without a fact behind it. For each claim write: the claim with its number exactly as reported, the
source (a primary source such as an agency, regulator, company, court or study, or two independent reputable
outlets) with the link, and when it was reported. Then three short lists: **not known yet**, **where sources
disagree** (and which version you use), and **left out on purpose** (rumours, speculation). Re-check the facts on
the day you deliver.

## 2. Script

One narration line per shot. Scenes group shots that are recorded as one voice take.

- **Length:** about 2.6 to 3 words per second, so ~230 words for 90 s, ~300 for 2 minutes.
- **Hook (0 to 3 s):** a striking moving first frame and a first sentence of 12 words or fewer that opens a question
  or makes a promise ("At thirty-five thousand feet, something went wrong."). Never open on a title card or a logo.
- **Structure:** hook → context (who, what, where) → build (the mechanism or sequence, one idea per shot) → turn (the
  answer to the hook) → resolution (what it means, what is still unknown, what happens next) → call to action.
- **Language:** short sentences, active voice, everyday words. Write numbers as spoken in the narration ("thirty-five
  thousand feet") and show digits on screen. Attribute every non-obvious claim ("according to the NTSB").
- **Explain with something familiar.** A paper plane explains an elevator; a foam ball and a bowling ball explain
  how falling pieces drift. One concrete comparison beats three abstract sentences.
- **Stay literal.** A sensational hook is fine; a false one is not.

## 3. Storyboard (before generating anything)

Build one HTML page and show it to the user. For every shot: a concept frame (a quick AI image or a sketch is fine), the
timecode, the narration line, the on-screen text, the animation beats, the sound cues, the music note, and the exact
prompt for each AI asset. Add the fact sheet, the music plan and the effects plan. Wait for approval.

## 4. Assets

**Voice.** Record each scene as one take, two takes per scene, and keep the take whose transcript matches the script
word for word (run Whisper over it). You need word timings to land animations on words: take them from the TTS if it
returns them, or from Whisper word timestamps.

**AI video (keep it to 2 or 3 clips).** Use them only for what graphics cannot show: weather, a vehicle in motion,
hands, a real-world object. Start each clip from an approved still (image to video). Rules for usable takes:
- End photoreal prompts with: "Photorealistic, indistinguishable from real footage shot on a cinema camera: natural
  motion blur, real atmospheric haze, accurate reflections, fine film grain, muted colour, no CGI look."
- Negatives: text, captions, logos, registration numbers, markings, extra objects, people or faces (unless wanted),
  fire, smoke, explosion, debris, CGI, cartoon.
- If you need an action (a dive, a turn), start from a first frame that is already in that action. A model asked to
  invent the motion tends to drift into something else.
- Generate 8 s and use only the clean window. Check every take frame by frame (a contact sheet every 0.2 s): engines
  that sprout orange glows that read as fire, objects that morph mid-shot, stray text, extra limbs. If a clip is
  clean for 2 s and you need 3 s, play it slower (not below 0.7x) rather than using the bad part.
- At most two generations per shot; if both fail, rethink the first frame instead of rerolling.

**AI images (2 or 3).** Photos taped into the page, plates, backgrounds, first frames. Same photoreal rules.

**Music.** Two or three tracks at most. Reusing a track is fine. Choose takes by warmth (darker beats brighter) and
check them for hidden vocals. Plan where music stops: a dead stop on a key line is a strong effect.

**Sound effects.** A quiet sound for every move on screen (a stroke drawing, a card landing, a page turning). Warm and
soft: lowpass recorded effects to about 1.2-2 kHz; bright effects are annoying even when short.

## 5. Timeline

Place each scene's take, cut each shot on its first word (a few frames early), and compute every shot's start and
length in frames (24 fps). Rules:
- **No dead air.** At most 1.0 s between narration lines, at most 1.2 s for a silent insert (always moving, with
  music under it), never a black screen before the end card. Silence mid-video shows up as a drop in retention.
- To tighten, shorten the pauses or time-stretch the voice a few percent (inaudible). Never cut words.

## 6. Motion graphics (Remotion)

One React component per shot, each filling its whole shot. A `useWord('word')` helper that returns the frame a word
starts lets every beat land on the narration.

**Layout (1080×1920):** keep 250 px at the top, 440 px at the bottom and 130 px on the right clear of anything
important (the app's buttons sit there). Headings live around y 300-470, graphics in 500-1270, captions at the
bottom ending at y 1460.

**Rules that keep it watchable:**
- **Never an empty opening.** Each shot shows its main element within half a second. Draw the layout dim first, then
  light each part on its word.
- **Nothing static.** Every shot has a slow move or a moving element. Ease out, no bounce.
- **Numbers roll** like instruments and hold for 1.5 s after they land.
- **Captions** at the bottom, word by word, digits for numbers, the current word highlighted.
- **The flicker rule:** never put SVG `<text>` inside an HTML element whose CSS transform changes every frame (a
  camera push, a rotating card). Chrome lays the text out against a stale transform on random frames. Write labels
  in HTML, or animate the zoom inside the SVG.
- **Pre-scale photos** to their display size; a huge image under a moving transform renders differently run to run.
- **Determinism:** render the video twice at your real concurrency and compare the two files frame by frame. Any
  difference is a flicker your viewers will see.

## 7. Sound and mix

Voice first:
- music at least 16 LU under the voice while it speaks (duck it about 10 dB under speech);
- the loudest effect 6.5 to 10 LU under the voice, warm (little energy above 3 kHz);
- master at -14 LUFS, limited at -2.6 dBFS (Instagram and TikTok normalise to about this).

## 8. Render and QA gates (all must pass)

1. Facts: every number on screen and every narrated claim matches the fact sheet.
2. Script: a Whisper pass over the final mix finds no masked words.
3. Picture: a contact sheet of every shot at 12%, 50% and 92%: nothing collides with the captions, no empty opening.
4. Determinism: two renders, identical frame by frame.
5. AI takes: every AI frame used was checked for glows, morphing, text and faces.
6. Sound: the loudness numbers above.
7. Length and pace as planned, no dead air.
8. Fresh facts: re-checked on delivery day; the end card says "facts as of <date>".

Deliver the MP4, a cover frame (the hook, never a moment of harm), the script, the fact sheet, and a post caption
(two or three plain sentences, "facts as of", the sources, 5 or 6 hashtags). Remind the user to turn on the
platform's AI label for posts with AI footage.

## Restraint (news about harm)

No real victims depicted by AI, no gore, no speculation about cause or blame, no unconfirmed casualty numbers. If you
show a dramatic moment (a dive, a storm), cut before harm. Say plainly what investigators do not know yet.

## Styles

Pick one style and keep every element in it: palette, fonts, how things move, how scenes change, the caption look and
the sound palette.

| Style | Look | Motion and transitions | Sound |
|-------|------|------------------------|-------|
| **Notebook Doodle** | Lined paper `#FBF8EF`, blue ballpoint `#22409A`, one red pen `#D6392F`, yellow highlighter, sticky notes, taped photos. Font: Kalam | Strokes draw on and "boil" (re-jitter) at 8 fps, highlighter swipes, sticky notes slap on, page flips between sections | Pen scratches, page turns, tape, sticky notes; lo-fi beats |
| **Night Desk** | Newsroom at night: `#0B1118` navy, cyan `#7FD3E6`, amber `#E3A54A`. Barlow Condensed + IBM Plex Mono | Slow pushes, self-drawing lines, odometer numbers, maps and charts on real data; straight cuts | Low drones, soft ticks; dark documentary score |
| **Daylight Keynote** | Bright product stage: `#F5F5F7`, `#1D1D1F`, one orange `#FF5A1F`. Inter Tight | Cards slide and settle, slow parallax, huge clean numbers; soft cross-zooms | Clean UI clicks; airy electronic |
| **Swiss Grid** | White, black, red `#E30613`, a 12-column grid. Archivo Black + Inter | Blocks slide along grid lines, text masks, giant numerals; hard cuts on the beat | Crisp clicks; minimal techno |
| **Vintage Textbook** | 1960s science book: cream `#EFE4C8`, ink `#2E2A24`, muted red and teal. Libre Caslon + Courier Prime | Slow pans over engraved plates, labels draw in with leader lines; page turns | Page turns, slide-projector clicks; library music |
| **Thermal Receipt** | The video prints on a receipt: `#F7F7F2`, black mono type, one pink. IBM Plex Mono | Lines type in as the receipt feeds, totals tick up, tear-off at the end | Printer feed, tear; minimal beats |
| **Editorial Collage** | Magazine cut-outs, torn paper, big serif headlines: off-white, black, red, marigold | Cut-outs slap in, torn strips reveal, marker circles | Paper slaps, camera shutters; boom-bap |
| **Ink Wash** | Sumi ink on rice paper, one red seal | Ink blooms, brush strokes draw in, long holds (works for serious stories) | Brush, paper, quiet room; sparse piano |

## Appendix: Notebook Doodle starter (Remotion)

Strokes are SVG paths that draw on by trimming their point list and boil every 3 frames. All randomness uses
Remotion's `random(seed)`, so two renders match. Labels are HTML (see the flicker rule).

```tsx
import React from 'react';
import { random, useCurrentFrame, interpolate, Easing } from 'remotion';
type Pt = [number, number];
const ease = Easing.bezier(0.22, 1, 0.36, 1);
export const k = (f: number, a: number, b: number, from = 0, to = 1) =>
  interpolate(f, [a, b], [from, to], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: ease });
const jit = (s: string, a: number) => (random(s) - 0.5) * 2 * a;
const resample = (p: Pt[], step = 26): Pt[] => { const o: Pt[] = [p[0]]; for (let i = 1; i < p.length; i++) {
  const [x0, y0] = p[i - 1], [x1, y1] = p[i], n = Math.max(1, Math.round(Math.hypot(x1 - x0, y1 - y0) / step));
  for (let j = 1; j <= n; j++) o.push([x0 + ((x1 - x0) * j) / n, y0 + ((y1 - y0) * j) / n]); } return o; };
const trim = (p: Pt[], t: number): Pt[] => { if (t >= 1) return p; const L = p.slice(1).map((q, i) => Math.hypot(q[0] - p[i][0], q[1] - p[i][1]));
  let want = L.reduce((a, b) => a + b, 0) * t; const o: Pt[] = [p[0]];
  for (let i = 1; i < p.length; i++) { if (want >= L[i - 1]) { o.push(p[i]); want -= L[i - 1]; continue; }
    const u = L[i - 1] ? want / L[i - 1] : 0; o.push([p[i - 1][0] + (p[i][0] - p[i - 1][0]) * u, p[i - 1][1] + (p[i][1] - p[i - 1][1]) * u]); break; } return o; };
const smooth = (p: Pt[]) => { if (p.length < 2) return ''; const q = [p[0], ...p, p[p.length - 1]]; let d = `M${q[1][0]} ${q[1][1]}`;
  for (let i = 1; i < q.length - 2; i++) { const [a, b, c, e] = [q[i - 1], q[i], q[i + 1], q[i + 2]];
    d += `C${b[0] + (c[0] - a[0]) / 6} ${b[1] + (c[1] - a[1]) / 6} ${c[0] - (e[0] - b[0]) / 6} ${c[1] - (e[1] - b[1]) / 6} ${c[0]} ${c[1]}`; } return d; };

/** a ballpoint stroke; p = how much is drawn (0..1) */
export const Ink: React.FC<{ pts: Pt[]; seed: string; p?: number; color?: string; w?: number; dash?: string }> =
  ({ pts, seed, p = 1, color = '#22409A', w = 5, dash }) => {
    const tick = Math.floor(useCurrentFrame() / 3);               // boil at 8 fps
    if (p <= 0) return null;
    const base = resample(pts).map(([x, y], i): Pt => [x + jit(`${seed}x${i}`, 2) + jit(`${seed}bx${i}t${tick}`, 0.9),
      y + jit(`${seed}y${i}`, 2) + jit(`${seed}by${i}t${tick}`, 0.9)]);
    return <path d={smooth(trim(base, p))} fill="none" stroke={color} strokeWidth={w} strokeLinecap="round" strokeLinejoin="round" strokeDasharray={dash} />;
  };

/** handwriting that writes on left to right (HTML, so no flicker) */
export const Hand: React.FC<{ x: number; y: number; at: number; dur?: number; size?: number; color?: string; children: React.ReactNode }> =
  ({ x, y, at, dur = 14, size = 46, color = '#22409A', children }) => {
    const p = k(useCurrentFrame(), at, at + dur);
    return p <= 0 ? null : <div style={{ position: 'absolute', left: x, top: y, fontFamily: 'Kalam', fontWeight: 700, fontSize: size, color,
      whiteSpace: 'nowrap', clipPath: p < 1 ? `inset(-20% ${(1 - p) * 100}% -20% -5%)` : undefined }}>{children}</div>;
  };

/** highlighter swipe (multiply blend) */
export const Hi: React.FC<{ x: number; y: number; w: number; h?: number; at: number }> = ({ x, y, w, h = 40, at }) => {
  const p = k(useCurrentFrame(), at, at + 8);
  return p <= 0 ? null : <div style={{ position: 'absolute', left: x, top: y, width: w * p, height: h, background: 'rgba(255,233,40,0.62)',
    mixBlendMode: 'multiply', borderRadius: h / 2, transform: 'rotate(-1.2deg)' }} />;
};

/** sticky note that slaps on */
export const Sticky: React.FC<{ x: number; y: number; w: number; h: number; at: number; rot?: number; children?: React.ReactNode }> =
  ({ x, y, w, h, at, rot = -3, children }) => {
    const p = k(useCurrentFrame(), at, at + 6);
    return p <= 0 ? null : <div style={{ position: 'absolute', left: x, top: y, width: w, height: h, background: 'linear-gradient(170deg,#FFE67A,#FFDD55)',
      boxShadow: '0 10px 22px rgba(60,45,20,0.22)', opacity: p, transform: `rotate(${rot}deg) scale(${1.18 - 0.18 * p})` }}>{children}</div>;
  };
```

Paper: a `#FBF8EF` background with blue rule lines every 60 px and a faint red margin line at x 84. A page flip is the
outgoing shot kept ~11 frames longer on its own sheet, rotating away around its left edge
(`perspective(2400px) rotateY(-104deg)`) over the next shot. A hand-drawn arrow is an `Ink` body plus two short head
strokes drawn after it.
