Catalog

Edit Props (live)

AUDIO
config
config
config
config
config
config

Source

121 lines
import 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_longform

Tutorial Longform — Andy's hand-crafted Remotion scene. 7 props. audioSrc:AUDIO.

Workflow Inputs (1)

  • Audio Src
    audioSrc
    AUDIO

Config Fields (6)

  • fps
  • chapters
  • durationSec
  • recordingSrc
  • talkingHeadSrc
  • wordTimestamps

Meta

Updated
4/21/2026