Green screen pipeline: new tools, AnimatedBackground, caption burn fixes

Add green_screen_processor (auto-detect + rembg fallback) and
green_screen_composite (4 layout presets with alpha compositing) tools
to automate the full keying-to-composite pipeline.

Remotion: add AnimatedBackground with gradient mesh and floating orbs
to Explainer, fix caption burn tool (remove entry point arg, auto-detect
dimensions, extend TalkingHead duration to 300s).

Update scene-director, compose-director, and asset-director skill docs
with green screen workflow steps and component constraints.
This commit is contained in:
calesthio
2026-04-02 11:54:30 -07:00
parent 942244ca54
commit a7e5f7498b
11 changed files with 2087 additions and 31 deletions
@@ -0,0 +1,260 @@
{
"cuts": [
{
"id": "scene-1-hook",
"source": "",
"in_seconds": 0,
"out_seconds": 5.5,
"type": "hero_title",
"text": "STOP PAYING",
"subtitle": "Multiple EMIs Every Month",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-2-control",
"source": "",
"in_seconds": 5.5,
"out_seconds": 9.5,
"type": "callout",
"text": "Your loans are managing YOU",
"callout_type": "warning",
"backgroundColor": "#0F172A",
"accentColor": "#F59E0B"
},
{
"id": "scene-3-rates",
"source": "",
"in_seconds": 9.5,
"out_seconds": 15.5,
"type": "bar_chart",
"chartData": [
{"label": "Credit Cards", "value": 24},
{"label": "Personal", "value": 16},
{"label": "App Loans", "value": 36},
{"label": "NBFC", "value": 18}
],
"title": "Interest Rates You're Paying (%)",
"backgroundColor": "#0F172A",
"accentColor": "#F59E0B",
"chartColors": ["#EF4444", "#F59E0B", "#8B5CF6", "#EC4899"]
},
{
"id": "scene-4-fear",
"source": "",
"in_seconds": 15.5,
"out_seconds": 20,
"type": "stat_reveal",
"text": "Every Month",
"subtitle": "Will I have anything left?",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-5-reveal",
"source": "",
"in_seconds": 20,
"out_seconds": 22,
"type": "hero_title",
"text": "WHAT NO ONE",
"subtitle": "Tells You",
"backgroundColor": "#0F172A",
"accentColor": "#22D3EE"
},
{
"id": "scene-6-inquiries",
"source": "",
"in_seconds": 22,
"out_seconds": 40.5,
"type": "progress_bar",
"progress": 95,
"label": "Loan Inquiries Piling Up",
"progressLabel": "DANGER ZONE",
"progressColor": "#EF4444",
"animationStyle": "step",
"progressSegments": [
{"value": 25, "color": "#F59E0B", "label": "Bank"},
{"value": 25, "color": "#F97316", "label": "NBFC"},
{"value": 25, "color": "#EF4444", "label": "Digital"},
{"value": 20, "color": "#DC2626", "label": "Random"}
],
"backgroundColor": "#0F172A"
},
{
"id": "scene-7-score-drop",
"source": "",
"in_seconds": 41,
"out_seconds": 46.5,
"type": "line_chart",
"chartSeries": [
{
"name": "Credit Score",
"data": [
{"x": 0, "y": 750},
{"x": 1, "y": 720},
{"x": 2, "y": 680},
{"x": 3, "y": 620}
],
"color": "#EF4444"
}
],
"title": "Your Credit Score Drops",
"xLabel": "Each Inquiry",
"yLabel": "Score",
"showGrid": true,
"showMarkers": true,
"chartAnimation": "draw",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-8-risk",
"source": "",
"in_seconds": 47,
"out_seconds": 49.5,
"type": "callout",
"text": "Lenders See You As HIGH RISK",
"callout_type": "warning",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-9-wrong",
"source": "",
"in_seconds": 50,
"out_seconds": 53.5,
"type": "stat_card",
"stat": "❌",
"subtitle": "Wrong approach wastes your time",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-9-right",
"source": "",
"in_seconds": 53.5,
"out_seconds": 57,
"type": "stat_card",
"stat": "✅",
"subtitle": "Right approach gets approval",
"backgroundColor": "#0F172A",
"accentColor": "#10B981"
},
{
"id": "scene-10-matters",
"source": "",
"in_seconds": 60,
"out_seconds": 66,
"type": "callout",
"text": "WHO you approach matters as much as WHAT you apply for",
"callout_type": "tip",
"backgroundColor": "#0F172A",
"accentColor": "#10B981"
},
{
"id": "scene-11-creditwin",
"source": "",
"in_seconds": 66,
"out_seconds": 80,
"type": "hero_title",
"text": "CREDIT WIN",
"subtitle": "Your Consolidation Partner",
"backgroundColor": "#0F172A",
"accentColor": "#22D3EE"
},
{
"id": "scene-12-oneplan",
"source": "",
"in_seconds": 81,
"out_seconds": 84.5,
"type": "kpi_grid",
"chartData": [
{"label": "EMI", "value": "1"},
{"label": "Due Date", "value": "1"},
{"label": "Clear Plan", "value": "1"}
],
"title": "From Chaos to Control",
"backgroundColor": "#0F172A",
"accentColor": "#22D3EE",
"chartColors": ["#22D3EE", "#10B981", "#A78BFA"]
},
{
"id": "scene-13a-years",
"source": "",
"in_seconds": 84.5,
"out_seconds": 89,
"type": "stat_card",
"stat": "15+",
"subtitle": "Years of Experience",
"backgroundColor": "#0F172A",
"accentColor": "#F59E0B"
},
{
"id": "scene-13b-borrowers",
"source": "",
"in_seconds": 89,
"out_seconds": 94,
"type": "stat_card",
"stat": "10,000+",
"subtitle": "Borrowers Served",
"backgroundColor": "#0F172A",
"accentColor": "#22D3EE"
},
{
"id": "scene-13c-saved",
"source": "",
"in_seconds": 94,
"out_seconds": 99,
"type": "stat_card",
"stat": "₹5K-15K",
"subtitle": "Saved Per Month on EMIs",
"backgroundColor": "#0F172A",
"accentColor": "#10B981"
},
{
"id": "scene-14-qualify",
"source": "",
"in_seconds": 100,
"out_seconds": 118,
"type": "stat_card",
"stat": "₹60,000+",
"subtitle": "Salary? You Qualify for Consolidation",
"backgroundColor": "#0F172A",
"accentColor": "#22D3EE"
},
{
"id": "scene-15-before",
"source": "",
"in_seconds": 118,
"out_seconds": 121,
"type": "stat_card",
"stat": "5 EMIs",
"subtitle": "Chaos → Exit This",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-15-after",
"source": "",
"in_seconds": 121,
"out_seconds": 124,
"type": "stat_card",
"stat": "1 EMI",
"subtitle": "Control → Your New Reality",
"backgroundColor": "#0F172A",
"accentColor": "#10B981"
},
{
"id": "scene-16-cta",
"source": "",
"in_seconds": 124,
"out_seconds": 127,
"type": "hero_title",
"text": "APPLY NOW",
"subtitle": "CreditWin.in",
"backgroundColor": "#0F172A",
"accentColor": "#10B981"
}
],
"overlays": []
}
@@ -0,0 +1,55 @@
{
"cuts": [
{
"id": "scene-1-hook",
"source": "",
"in_seconds": 0,
"out_seconds": 5.5,
"type": "hero_title",
"text": "STOP PAYING",
"subtitle": "Multiple EMIs Every Month",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
},
{
"id": "scene-2-control",
"source": "",
"in_seconds": 5.5,
"out_seconds": 9.5,
"type": "callout",
"text": "Your loans are managing YOU",
"callout_type": "warning",
"backgroundColor": "#0F172A",
"accentColor": "#F59E0B"
},
{
"id": "scene-3-rates",
"source": "",
"in_seconds": 9.5,
"out_seconds": 15.5,
"type": "bar_chart",
"chartData": [
{"label": "Credit Cards", "value": 24},
{"label": "Personal", "value": 16},
{"label": "App Loans", "value": 36},
{"label": "NBFC", "value": 18}
],
"title": "Interest Rates You're Paying (%)",
"backgroundColor": "#0F172A",
"accentColor": "#F59E0B",
"chartColors": ["#EF4444", "#F59E0B", "#8B5CF6", "#EC4899"]
},
{
"id": "scene-4-fear",
"source": "",
"in_seconds": 15.5,
"out_seconds": 20,
"type": "stat_reveal",
"text": "Every Month",
"subtitle": "Will I have anything left?",
"backgroundColor": "#0F172A",
"accentColor": "#EF4444"
}
],
"overlays": []
}
+98 -2
View File
@@ -44,6 +44,97 @@ const { fontFamily } = loadFont("normal", {
subsets: ["latin"],
});
// ---------------------------------------------------------------------------
// Animated Background — Gradient Mesh + Floating Orbs
// ---------------------------------------------------------------------------
const AnimatedBackground: React.FC<{ style?: "fintech" | "default" }> = ({
style: bgStyle = "default",
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
const progress = frame / durationInFrames;
// Slow-moving gradient angles
const angle1 = 135 + Math.sin(frame / (fps * 8)) * 30;
const angle2 = 225 + Math.cos(frame / (fps * 6)) * 25;
// Color stops shift over time
const shift = Math.sin(frame / (fps * 12)) * 0.15;
const gradient = `
radial-gradient(ellipse at ${30 + Math.sin(frame / (fps * 10)) * 20}% ${40 + Math.cos(frame / (fps * 8)) * 20}%,
rgba(15, 23, 60, 1) 0%, transparent 60%),
radial-gradient(ellipse at ${70 + Math.cos(frame / (fps * 7)) * 20}% ${60 + Math.sin(frame / (fps * 9)) * 25}%,
rgba(30, 10, 60, 0.8) 0%, transparent 55%),
radial-gradient(ellipse at ${50 + Math.sin(frame / (fps * 14)) * 30}% ${20 + Math.cos(frame / (fps * 11)) * 15}%,
rgba(0, 40, 60, 0.6) 0%, transparent 50%),
linear-gradient(${angle1}deg, #060918 0%, #0B1026 40%, #0F0A2E 70%, #080D1F 100%)
`;
// Floating orbs
const orbs = [
{ x: 20, y: 30, size: 300, color: "rgba(34, 211, 238, 0.08)", speedX: 7, speedY: 11 },
{ x: 70, y: 60, size: 250, color: "rgba(139, 92, 246, 0.1)", speedX: 9, speedY: 8 },
{ x: 40, y: 80, size: 200, color: "rgba(16, 185, 129, 0.07)", speedX: 13, speedY: 6 },
{ x: 80, y: 20, size: 350, color: "rgba(245, 158, 11, 0.06)", speedX: 11, speedY: 14 },
{ x: 10, y: 70, size: 180, color: "rgba(236, 72, 153, 0.05)", speedX: 8, speedY: 10 },
];
return (
<AbsoluteFill style={{ background: gradient }}>
{/* Floating glow orbs */}
{orbs.map((orb, i) => {
const ox = orb.x + Math.sin(frame / (fps * orb.speedX)) * 15;
const oy = orb.y + Math.cos(frame / (fps * orb.speedY)) * 12;
return (
<div
key={i}
style={{
position: "absolute",
left: `${ox}%`,
top: `${oy}%`,
width: orb.size,
height: orb.size,
borderRadius: "50%",
background: orb.color,
filter: `blur(${orb.size * 0.4}px)`,
transform: "translate(-50%, -50%)",
willChange: "transform",
}}
/>
);
})}
{/* Subtle grid overlay */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage: `
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)
`,
backgroundSize: "60px 60px",
opacity: 0.5 + Math.sin(frame / (fps * 20)) * 0.2,
}}
/>
{/* Top gradient fade for depth */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "30%",
background: "linear-gradient(to bottom, rgba(6,9,24,0.4), transparent)",
}}
/>
</AbsoluteFill>
);
};
// ---------------------------------------------------------------------------
// Types — aligned with edit_decisions artifact schema
// ---------------------------------------------------------------------------
@@ -353,7 +444,9 @@ const SceneRenderer: React.FC<{ cut: Cut }> = ({ cut }) => {
};
// Resolve the scene element based on cut type, then wrap with backgroundImage if set
const bgColor = cut.backgroundImage ? "transparent" : cut.backgroundColor;
// Use transparent bg so the animated gradient background shows through
const rawBg = cut.backgroundImage ? "transparent" : cut.backgroundColor;
const bgColor = (rawBg === "#0F172A" || rawBg === "#0f172a") ? "transparent" : rawBg;
// Explicit component types
if (cut.type === "text_card" && cut.text) {
@@ -539,7 +632,10 @@ export const Explainer: React.FC<ExplainerProps> = ({
const { fps, durationInFrames } = useVideoConfig();
return (
<AbsoluteFill style={{ background: "#0F172A", fontFamily }}>
<AbsoluteFill style={{ background: "#060918", fontFamily }}>
{/* Layer 0: Animated gradient background */}
<AnimatedBackground style="fintech" />
{/* Layer 1: Visual scenes */}
{cuts.map((cut) => {
const from = Math.round(cut.in_seconds * fps);
+2 -1
View File
@@ -65,13 +65,14 @@ export const Root: React.FC = () => {
<Composition
id="TalkingHead"
component={TalkingHead}
durationInFrames={30 * 30}
durationInFrames={30 * 300}
fps={30}
width={1080}
height={1920}
defaultProps={{
videoSrc: "",
captions: [],
overlays: [],
wordsPerPage: 4,
fontSize: 52,
highlightColor: "#22D3EE",
+319 -1
View File
@@ -1,9 +1,304 @@
import { AbsoluteFill, OffthreadVideo } from "remotion";
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<string, React.CSSProperties> = {
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 (
<TextCard
text={overlay.text}
fontSize={overlay.fontSize}
color={overlay.color}
backgroundColor={bgColor}
/>
);
}
if (overlay.type === "stat_card" && overlay.stat) {
return (
<StatCard
stat={overlay.stat}
subtitle={overlay.subtitle}
accentColor={overlay.accentColor}
backgroundColor={bgColor}
/>
);
}
if (overlay.type === "callout" && overlay.text) {
return (
<CalloutBox
text={overlay.text}
type={overlay.callout_type}
title={overlay.title}
borderColor={overlay.accentColor}
backgroundColor={overlay.backgroundColor}
textColor={overlay.color}
containerBackgroundColor={bgColor}
/>
);
}
if (
overlay.type === "comparison" &&
overlay.leftLabel &&
overlay.rightLabel
) {
return (
<ComparisonCard
leftLabel={overlay.leftLabel}
rightLabel={overlay.rightLabel}
leftValue={overlay.leftValue || ""}
rightValue={overlay.rightValue || ""}
title={overlay.title}
backgroundColor={bgColor}
textColor={overlay.color}
/>
);
}
if (overlay.type === "bar_chart" && overlay.chartData) {
return (
<BarChart
data={overlay.chartData}
title={overlay.title}
colors={overlay.chartColors}
animationStyle={(overlay.chartAnimation as any) || "grow-up"}
showValues={overlay.showValues}
backgroundColor={bgColor}
/>
);
}
if (overlay.type === "line_chart" && overlay.chartSeries) {
return (
<LineChart
series={overlay.chartSeries}
title={overlay.title}
colors={overlay.chartColors}
animationStyle={(overlay.chartAnimation as any) || "draw"}
showGrid={overlay.showGrid}
showMarkers={overlay.showMarkers}
showLegend={overlay.showLegend}
backgroundColor={bgColor}
/>
);
}
if (overlay.type === "pie_chart" && overlay.chartData) {
return (
<PieChart
data={overlay.chartData}
title={overlay.title}
colors={overlay.chartColors}
animationStyle={(overlay.chartAnimation as any) || "expand"}
donut={overlay.donut}
centerLabel={overlay.centerLabel}
centerValue={overlay.centerValue}
showLegend={overlay.showLegend}
backgroundColor={bgColor}
/>
);
}
if (overlay.type === "kpi_grid" && overlay.chartData) {
return (
<KPIGrid
metrics={overlay.chartData}
title={overlay.title}
columns={overlay.columns}
colors={overlay.chartColors}
animationStyle={(overlay.chartAnimation as any) || "count-up"}
backgroundColor={bgColor}
/>
);
}
if (overlay.type === "hero_title" && overlay.text) {
return <HeroTitle title={overlay.text} subtitle={overlay.subtitle} />;
}
if (overlay.type === "section_title" && overlay.text) {
return (
<SectionTitle
title={overlay.text}
subtitle={overlay.subtitle}
accentColor={overlay.accentColor}
position="top-left"
/>
);
}
if (overlay.type === "stat_reveal" && overlay.text) {
return (
<StatReveal
stat={overlay.text}
label={overlay.subtitle}
accentColor={overlay.accentColor}
position="bottom-right"
/>
);
}
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 (
<div
style={{
...posStyle,
opacity,
overflow: "hidden",
borderRadius: isFullOverlay ? 0 : 16,
boxShadow: isFullOverlay
? "none"
: "0 8px 32px rgba(0, 0, 0, 0.4)",
}}
>
{isFullOverlay && (
<AbsoluteFill style={{ background: "rgba(0, 0, 0, 0.7)" }} />
)}
<OverlayContent overlay={overlay} />
</div>
);
};
// ---------------------------------------------------------------------------
// Main TalkingHead composition
// ---------------------------------------------------------------------------
export interface TalkingHeadProps {
[key: string]: unknown;
videoSrc: string;
captions: WordCaption[];
overlays?: TalkingHeadOverlay[];
wordsPerPage?: number;
fontSize?: number;
highlightColor?: string;
@@ -12,16 +307,39 @@ export interface TalkingHeadProps {
export const TalkingHead: React.FC<TalkingHeadProps> = ({
videoSrc,
captions,
overlays,
wordsPerPage = 4,
fontSize = 52,
highlightColor = "#22D3EE",
}) => {
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: "#000" }}>
{/* Layer 1: Video background */}
<OffthreadVideo
src={videoSrc}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
{/* 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 (
<Sequence
key={overlay.id || `overlay-${i}`}
from={from}
durationInFrames={duration}
>
<PositionedOverlay overlay={overlay} />
</Sequence>
);
})}
{/* Layer 3: Captions (topmost — always visible above overlays) */}
<CaptionOverlay
words={captions}
wordsPerPage={wordsPerPage}