import { AbsoluteFill, OffthreadVideo, Sequence, interpolate, useCurrentFrame, useVideoConfig, } from "remotion"; import { CaptionOverlay, WordCaption } from "./components/CaptionOverlay"; import { TextCard } from "./components/TextCard"; import { StatCard } from "./components/StatCard"; import { CalloutBox } from "./components/CalloutBox"; import { ComparisonCard } from "./components/ComparisonCard"; import { BarChart } from "./components/charts/BarChart"; import { LineChart } from "./components/charts/LineChart"; import { PieChart } from "./components/charts/PieChart"; import { KPIGrid } from "./components/charts/KPIGrid"; import { HeroTitle } from "./components/HeroTitle"; import { SectionTitle } from "./components/SectionTitle"; import { StatReveal } from "./components/StatReveal"; // --------------------------------------------------------------------------- // Overlay types for talking-head video // --------------------------------------------------------------------------- export interface TalkingHeadOverlay { id?: string; type: string; in_seconds: number; out_seconds: number; position?: | "lower_third" | "upper_third" | "left_panel" | "right_panel" | "full_overlay"; // Component-specific props (same as Explainer Cut) text?: string; stat?: string; subtitle?: string; callout_type?: "info" | "warning" | "tip" | "quote"; title?: string; leftLabel?: string; rightLabel?: string; leftValue?: string; rightValue?: string; chartData?: any[]; chartSeries?: any[]; chartColors?: string[]; chartAnimation?: string; donut?: boolean; centerLabel?: string; centerValue?: string; showGrid?: boolean; showValues?: boolean; showLegend?: boolean; showMarkers?: boolean; columns?: 2 | 3 | 4; // Styling backgroundColor?: string; color?: string; accentColor?: string; fontSize?: number; } // --------------------------------------------------------------------------- // Position presets for 9:16 (1080x1920) frame // --------------------------------------------------------------------------- const POSITION_STYLES: Record = { lower_third: { position: "absolute", bottom: 320, // Above caption area (~1600px) left: 40, right: 40, height: 480, }, upper_third: { position: "absolute", top: 80, left: 40, right: 40, height: 480, }, left_panel: { position: "absolute", top: 200, left: 40, width: 480, bottom: 400, }, right_panel: { position: "absolute", top: 200, right: 40, width: 480, bottom: 400, }, full_overlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, }, }; // --------------------------------------------------------------------------- // Overlay component dispatcher — maps overlay type to Remotion component // --------------------------------------------------------------------------- const OverlayContent: React.FC<{ overlay: TalkingHeadOverlay }> = ({ overlay, }) => { const bgColor = overlay.backgroundColor || "#0F172A"; if (overlay.type === "text_card" && overlay.text) { return ( ); } if (overlay.type === "stat_card" && overlay.stat) { return ( ); } if (overlay.type === "callout" && overlay.text) { return ( ); } if ( overlay.type === "comparison" && overlay.leftLabel && overlay.rightLabel ) { return ( ); } if (overlay.type === "bar_chart" && overlay.chartData) { return ( ); } if (overlay.type === "line_chart" && overlay.chartSeries) { return ( ); } if (overlay.type === "pie_chart" && overlay.chartData) { return ( ); } if (overlay.type === "kpi_grid" && overlay.chartData) { return ( ); } if (overlay.type === "hero_title" && overlay.text) { return ; } if (overlay.type === "section_title" && overlay.text) { return ( ); } if (overlay.type === "stat_reveal" && overlay.text) { return ( ); } return null; }; // --------------------------------------------------------------------------- // Positioned overlay wrapper — handles position + fade in/out // --------------------------------------------------------------------------- const PositionedOverlay: React.FC<{ overlay: TalkingHeadOverlay }> = ({ overlay, }) => { const frame = useCurrentFrame(); const { durationInFrames } = useVideoConfig(); // Fade in over 8 frames (~0.27s), fade out over 8 frames const fadeIn = interpolate(frame, [0, 8], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const fadeOut = interpolate( frame, [durationInFrames - 8, durationInFrames], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" } ); const opacity = fadeIn * fadeOut; const position = overlay.position || "lower_third"; const posStyle = POSITION_STYLES[position] || POSITION_STYLES.lower_third; const isFullOverlay = position === "full_overlay"; return (
{isFullOverlay && ( )}
); }; // --------------------------------------------------------------------------- // Main TalkingHead composition // --------------------------------------------------------------------------- export interface TalkingHeadProps { [key: string]: unknown; videoSrc: string; captions: WordCaption[]; overlays?: TalkingHeadOverlay[]; wordsPerPage?: number; fontSize?: number; highlightColor?: string; } export const TalkingHead: React.FC = ({ videoSrc, captions, overlays, wordsPerPage = 4, fontSize = 52, highlightColor = "#22D3EE", }) => { const { fps } = useVideoConfig(); return ( {/* Layer 1: Video background */} {/* Layer 2: Overlays (charts, stats, callouts, etc.) */} {overlays?.map((overlay, i) => { const from = Math.round(overlay.in_seconds * fps); const duration = Math.round( (overlay.out_seconds - overlay.in_seconds) * fps ); return ( ); })} {/* Layer 3: Captions (topmost — always visible above overlays) */} ); };