reelcn
GitHubBack to landing

Talking head short

Vertical talking-head clip with a three-second hook and word-by-word captions, running as long as the video.

TalkingHeadShort, defaults
TalkingHeadShort, defaults thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/talking-head-short.json

Files land in src/reelcn/ with the items it builds on. Dependencies: @remotion/media, remotion, zod.

Register the composition

<Composition
  id="TalkingHeadShort"
  component={TalkingHeadShort}
  schema={talkingHeadShortSchema}
  defaultProps={{ ...talkingHeadShortDefaults, video: staticFile("take-3.mp4"), captions }}
  calculateMetadata={talkingHeadShortMetadata}
  width={1080}
  height={1920}
  fps={30}
  durationInFrames={1}
/>

Render

npx remotion render TalkingHeadShort # 16:9
npx remotion render TalkingHeadShort --width=1080 --height=1920 # 9:16

Props

PropTypeDescription
captions*{ text: string; startMs: number; endMs: number; timestampMs: number | null; confidence: number | null; }[]Word-level captions; reelcn-transcribe writes them from the video.
hook*stringLine shown over the first three seconds.
emphasize*string[]Words painted in the emphasis color.
theme"daylight" | "midnight" | "mono" | "ledger" | "afterglow" | "chromewave" | "neon" | "paper" | "sunset"Theme preset. Inherits the surrounding `ThemeProvider` when omitted.
brand{ accent?: string; logo?: string; font?: string; }
sfxbooleanPlays a soft whoosh as each scene starts.
musicstringBackground music file, looped under everything at 25% volume.
videostringThe recording. A moving background stands in when omitted.

Built from

Use

  • Turning a recorded explanation or interview answer into a short
  • Captioned clips for feeds that autoplay muted

Avoid

  • Captions over a layout you build yourself. Use captions

Make it with an agent

With the reelcn agent skill installed, ask for the video in plain words, such as "make a launch video for Relay with reelcn". The skill picks a template, sets its props, reviews a contact sheet and renders.

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/talking-head-short.tsx
/**
 * @title Talking Head Short
 * @category templates
 * @description Vertical talking-head clip with a three-second hook and word-by-word captions, running as long as the video.
 * @duration data-driven
 * @use Turning a recorded explanation or interview answer into a short
 * @use Captioned clips for feeds that autoplay muted
 * @avoid Captions over a layout you build yourself. Use `captions`
 * @tags short, captions, talking head, creator, template
 * @example
 * <Composition
 *   id="TalkingHeadShort"
 *   component={TalkingHeadShort}
 *   schema={talkingHeadShortSchema}
 *   defaultProps={{ ...talkingHeadShortDefaults, video: staticFile("take-3.mp4"), captions }}
 *   calculateMetadata={talkingHeadShortMetadata}
 *   width={1080}
 *   height={1920}
 *   fps={30}
 *   durationInFrames={1}
 * />
 */
import { Video } from "@remotion/media";
import { AbsoluteFill, type CalculateMetadataFunction, Sequence, useVideoConfig } from "remotion";
import { z } from "zod";
import { Captions } from "./captions";
import { useViewport } from "./core";
import { GradientMesh } from "./gradient-mesh";
import { HookTitle } from "./hook-title";
import { captionSchema, captionsSeconds, STORY_FPS } from "./story";
import { mediaSeconds, TemplateFrame, templateSchema } from "./storyboard";

export const talkingHeadShortSchema = templateSchema.extend({
  /** The recording. A moving background stands in when omitted. */
  video: z.string().optional(),
  /** Word-level captions; `reelcn-transcribe` writes them from the video. */
  captions: z.array(captionSchema),
  /** Line shown over the first three seconds. */
  hook: z.string(),
  /** Words painted in the emphasis color. */
  emphasize: z.array(z.string()),
});

export type TalkingHeadShortProps = z.infer<typeof talkingHeadShortSchema>;

const SAMPLE: [string, number, number][] = [
  ["Here", 0, 300],
  ["is", 300, 450],
  ["the", 450, 600],
  ["one", 600, 900],
  ["thing", 900, 1250],
  ["that", 1250, 1450],
  ["keeps", 1450, 1800],
  ["people", 1800, 2200],
  ["watching.", 2200, 2800],
];

export const talkingHeadShortDefaults: TalkingHeadShortProps = {
  hook: "Most videos lose you in three seconds",
  captions: SAMPLE.map(([text, startMs, endMs]) => ({
    text: `${text} `,
    startMs,
    endMs,
    timestampMs: startMs,
    confidence: 1,
  })),
  emphasize: ["watching."],
};

export function TalkingHeadShort({ video, captions, hook, emphasize, ...fields }: TalkingHeadShortProps) {
  const { fps } = useVideoConfig();
  const { safe } = useViewport();
  return (
    <TemplateFrame fields={fields}>
      <AbsoluteFill>
        {video ? <Video src={video} objectFit="cover" style={{ width: "100%", height: "100%" }} /> : <GradientMesh />}
      </AbsoluteFill>
      <Sequence durationInFrames={3 * fps}>
        <AbsoluteFill style={{ alignItems: "center", paddingTop: safe.top }}>
          <HookTitle text={hook} />
        </AbsoluteFill>
      </Sequence>
      <Captions captions={captions} emphasize={emphasize} />
    </TemplateFrame>
  );
}

/** As long as the video; without one, as long as the captions. */
export const talkingHeadShortMetadata: CalculateMetadataFunction<TalkingHeadShortProps> = async ({ props }) => ({
  fps: STORY_FPS,
  durationInFrames: Math.ceil(
    STORY_FPS * (props.video ? await mediaSeconds(props.video) : captionsSeconds(props.captions, 6)),
  ),
});

On this page