← Catalog
Edit Props (live)
AUDIO
config
config
config
config
config
config
Source
121 linesimport React from "react";
import {
AbsoluteFill,
OffthreadVideo,
Sequence,
Audio,
staticFile,
useVideoConfig,
} from "remotion";
import { KineticCaption } from "./KineticCaption";
/**
* Long-form YouTube tutorial composition (1920x1080).
*
* Phase 1: plays the user's screen-recording MP4 as-is and overlays kinetic
* captions synced to the Whisper transcript. No trims, no chapter cards — the
* first deliverable is "raw recording + captions at 16:9".
*
* Phase 2+ will use `chapters[]` to slice the recording via <Sequence startFrom/
* endAt>, insert chapter-title cards between them, and (optionally) add a Bro
* talking-head corner inset.
*/
export interface WordTimestamp {
word: string;
start: number;
end: number;
emphasis?: boolean;
}
export interface Chapter {
chapter_title: string;
keep_from: number;
keep_to: number;
}
export interface TutorialLongformProps {
recordingSrc: string;
wordTimestamps: WordTimestamp[];
chapters: Chapter[];
durationSec: number;
fps: number;
talkingHeadSrc?: string | null;
audioSrc?: string | null; // optional — Phase 1 uses recording's own audio track
}
export const TutorialLongform: React.FC<TutorialLongformProps> = ({
recordingSrc,
wordTimestamps,
chapters,
durationSec,
talkingHeadSrc,
audioSrc,
}) => {
const { fps } = useVideoConfig();
const resolveSrc = (p: string) =>
p.startsWith("http") || p.startsWith("data:") ? p : staticFile(p);
// Phase 1: single chapter spanning the whole recording. Rendering the
// <OffthreadVideo> directly (no Sequence trim) is simpler and avoids
// frame-drop artifacts at the boundary.
const singleChapter = !chapters || chapters.length <= 1;
return (
<AbsoluteFill style={{ backgroundColor: "#000" }}>
{singleChapter ? (
<OffthreadVideo
src={resolveSrc(recordingSrc)}
muted={!!audioSrc}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
/>
) : (
chapters.map((ch, i) => {
const startFrame = Math.round(ch.keep_from * fps);
const durFrames = Math.max(1, Math.round((ch.keep_to - ch.keep_from) * fps));
return (
<Sequence key={i} from={startFrame} durationInFrames={durFrames}>
<OffthreadVideo
src={resolveSrc(recordingSrc)}
startFrom={Math.round(ch.keep_from * fps)}
endAt={Math.round(ch.keep_to * fps)}
muted={!!audioSrc}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
/>
</Sequence>
);
})
)}
{/* Optional explicit audio track override (Phase 2+ — e.g. cleaned-up VO) */}
{audioSrc ? <Audio src={resolveSrc(audioSrc)} /> : null}
{/* Kinetic captions — aspect-agnostic, positions at fraction of viewport height */}
<KineticCaption words={wordTimestamps || []} defaultZone="bottom" />
{/* Optional Bro talking-head corner inset (top-right, 22% width). Off by
default for Phase 1 — the user IS the voice. */}
{talkingHeadSrc ? (
<div
style={{
position: "absolute",
top: 40,
right: 40,
width: "22%",
aspectRatio: "9 / 16",
borderRadius: 18,
overflow: "hidden",
boxShadow: "0 12px 40px rgba(0,0,0,0.5)",
}}
>
<OffthreadVideo
src={resolveSrc(talkingHeadSrc)}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
) : null}
</AbsoluteFill>
);
};
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: TutorialLongform
// 7 props detected · 1 port(s)
Tutorial Longform
tutorial_longformTutorial Longform — Andy's hand-crafted Remotion scene. 7 props. audioSrc:AUDIO.
Workflow Inputs (1)
- Audio Src
audioSrcAUDIO
Config Fields (6)
fpschaptersdurationSecrecordingSrctalkingHeadSrcwordTimestamps
Meta
- Updated
- 4/21/2026