Files
cycle-atlas/.claude/skills/remotion-video/templates/captions.md
keithranaandClaude Sonnet 5.5 0b42fdeae1 Add remotion-video skill (#1)
* Add remotion-video skill

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yhnsd9RDz7jDSUUX7oEcH

* Add Phase 2 templates to remotion-video skill

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yhnsd9RDz7jDSUUX7oEcH

* Fix captions template background; document browser flag after test render

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yhnsd9RDz7jDSUUX7oEcH

* Add audio/voiceover template (Phase 3), tested with real render

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yhnsd9RDz7jDSUUX7oEcH

* Document font loading and its network requirement; captions-over-video verified

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yhnsd9RDz7jDSUUX7oEcH

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 10:42:46 +11:00

1.6 KiB

Template: Captions

Words appear in time with speech. Give each word a start time in seconds.

src/Captions.tsx

import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";

type Word = { text: string; start: number; end: number }; // seconds

export const Captions = ({ words, background = "#111" }: { words: Word[]; background?: string }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const t = frame / fps;

  // Show a window of up to 5 words around the current one.
  const current = words.findIndex((w) => t >= w.start && t < w.end);
  const i = current === -1 ? 0 : current;
  const visible = words.slice(Math.floor(i / 5) * 5, Math.floor(i / 5) * 5 + 5);

  return (
    <AbsoluteFill style={{ backgroundColor: background, justifyContent: "flex-end", alignItems: "center", paddingBottom: 200 }}>
      <div style={{ fontSize: 72, fontWeight: 800, color: "white", textShadow: "0 4px 12px black", textAlign: "center", maxWidth: "80%" }}>
        {visible.map((w, k) => (
          <span key={k} style={{ color: t >= w.start && t < w.end ? "#ffd400" : "white", marginRight: 16 }}>
            {w.text}
          </span>
        ))}
      </div>
    </AbsoluteFill>
  );
};

Tips

  • Get word times from a transcription tool (e.g. @remotion/captions or Whisper) and save them as JSON in public/.
  • background is the backdrop when there is no video. Pass "transparent" when layering over a video.
  • Layer it over the video: <AbsoluteFill><OffthreadVideo src={staticFile("clip.mp4")} /><Captions words={words} background="transparent" /></AbsoluteFill>.