Add .gitignore to exclude all node packages and lock files
This commit is contained in:
97
skills/remotion-prompt-video/templates/src/Root.tsx
Normal file
97
skills/remotion-prompt-video/templates/src/Root.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from "react";
|
||||
import { Composition } from "remotion";
|
||||
import { VideoComposition } from "./components/Video";
|
||||
import { VideoScript } from "./lib/types";
|
||||
|
||||
// Default script used for preview in the Remotion Studio
|
||||
const defaultScript: VideoScript = {
|
||||
title: "Sample Video",
|
||||
description: "A sample video for preview",
|
||||
fps: 30,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
totalDurationInSeconds: 9,
|
||||
scenes: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Welcome",
|
||||
text: "Remotion Prompt Video",
|
||||
subtitle: "Create videos from text prompts",
|
||||
durationInSeconds: 3,
|
||||
style: {
|
||||
backgroundColor: "#1a1a2e",
|
||||
textColor: "#ffffff",
|
||||
fontSize: 72,
|
||||
fontFamily: "Arial",
|
||||
textAlign: "center",
|
||||
},
|
||||
animation: { entrance: "zoomIn", exit: "fadeOut" },
|
||||
backgroundGradient: {
|
||||
from: "#0f0c29",
|
||||
to: "#302b63",
|
||||
direction: "to bottom right",
|
||||
},
|
||||
icon: "🎬",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Feature",
|
||||
text: "AI-Powered Video Creation",
|
||||
subtitle: "Just type a prompt and get a video",
|
||||
durationInSeconds: 3,
|
||||
style: {
|
||||
backgroundColor: "#16213e",
|
||||
textColor: "#e0e0e0",
|
||||
fontSize: 64,
|
||||
fontFamily: "Georgia",
|
||||
textAlign: "center",
|
||||
},
|
||||
animation: { entrance: "slideUp", exit: "slideDown" },
|
||||
backgroundGradient: {
|
||||
from: "#0f3460",
|
||||
to: "#533483",
|
||||
direction: "to right",
|
||||
},
|
||||
icon: "✨",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Outro",
|
||||
text: "Get Started Today",
|
||||
subtitle: "remotion-prompt-video",
|
||||
durationInSeconds: 3,
|
||||
style: {
|
||||
backgroundColor: "#1a1a2e",
|
||||
textColor: "#ffffff",
|
||||
fontSize: 72,
|
||||
fontFamily: "Arial",
|
||||
textAlign: "center",
|
||||
},
|
||||
animation: { entrance: "fadeIn", exit: "fadeOut" },
|
||||
backgroundGradient: {
|
||||
from: "#302b63",
|
||||
to: "#24243e",
|
||||
direction: "to bottom",
|
||||
},
|
||||
icon: "🚀",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const RemotionRoot: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Composition
|
||||
id="PromptVideo"
|
||||
component={VideoComposition}
|
||||
durationInFrames={defaultScript.totalDurationInSeconds * defaultScript.fps}
|
||||
fps={defaultScript.fps}
|
||||
width={defaultScript.width}
|
||||
height={defaultScript.height}
|
||||
defaultProps={{
|
||||
script: defaultScript,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
259
skills/remotion-prompt-video/templates/src/components/Scene.tsx
Normal file
259
skills/remotion-prompt-video/templates/src/components/Scene.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
interpolate,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
spring,
|
||||
} from "remotion";
|
||||
import { Scene as SceneType } from "../lib/types";
|
||||
|
||||
interface SceneProps {
|
||||
scene: SceneType;
|
||||
}
|
||||
|
||||
export const SceneComponent: React.FC<SceneProps> = ({ scene }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
const totalFrames = scene.durationInSeconds * fps;
|
||||
|
||||
// Entrance animation (first 20% of scene)
|
||||
const entranceDuration = Math.floor(totalFrames * 0.2);
|
||||
// Exit animation (last 20% of scene)
|
||||
const exitStart = totalFrames - Math.floor(totalFrames * 0.2);
|
||||
|
||||
// Calculate entrance opacity and transform
|
||||
const getEntranceStyles = (): React.CSSProperties => {
|
||||
const progress = Math.min(frame / entranceDuration, 1);
|
||||
const springProgress = spring({
|
||||
frame: Math.min(frame, entranceDuration),
|
||||
fps,
|
||||
config: { damping: 15, stiffness: 100, mass: 0.8 },
|
||||
});
|
||||
|
||||
switch (scene.animation.entrance) {
|
||||
case "fadeIn":
|
||||
return { opacity: interpolate(frame, [0, entranceDuration], [0, 1], { extrapolateRight: "clamp" }) };
|
||||
case "slideUp":
|
||||
return {
|
||||
opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }),
|
||||
transform: `translateY(${interpolate(springProgress, [0, 1], [80, 0])}px)`,
|
||||
};
|
||||
case "slideLeft":
|
||||
return {
|
||||
opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }),
|
||||
transform: `translateX(${interpolate(springProgress, [0, 1], [100, 0])}px)`,
|
||||
};
|
||||
case "slideRight":
|
||||
return {
|
||||
opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }),
|
||||
transform: `translateX(${interpolate(springProgress, [0, 1], [-100, 0])}px)`,
|
||||
};
|
||||
case "zoomIn":
|
||||
return {
|
||||
opacity: interpolate(frame, [0, entranceDuration * 0.5], [0, 1], { extrapolateRight: "clamp" }),
|
||||
transform: `scale(${interpolate(springProgress, [0, 1], [0.5, 1])})`,
|
||||
};
|
||||
case "typewriter":
|
||||
return { opacity: 1 };
|
||||
case "none":
|
||||
default:
|
||||
return { opacity: 1 };
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate exit styles
|
||||
const getExitStyles = (): React.CSSProperties => {
|
||||
if (frame < exitStart) return {};
|
||||
|
||||
const exitProgress = (frame - exitStart) / (totalFrames - exitStart);
|
||||
|
||||
switch (scene.animation.exit) {
|
||||
case "fadeOut":
|
||||
return { opacity: interpolate(exitProgress, [0, 1], [1, 0]) };
|
||||
case "slideDown":
|
||||
return {
|
||||
opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }),
|
||||
transform: `translateY(${interpolate(exitProgress, [0, 1], [0, 80])}px)`,
|
||||
};
|
||||
case "slideLeft":
|
||||
return {
|
||||
opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }),
|
||||
transform: `translateX(${interpolate(exitProgress, [0, 1], [0, -100])}px)`,
|
||||
};
|
||||
case "slideRight":
|
||||
return {
|
||||
opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }),
|
||||
transform: `translateX(${interpolate(exitProgress, [0, 1], [0, 100])}px)`,
|
||||
};
|
||||
case "zoomOut":
|
||||
return {
|
||||
opacity: interpolate(exitProgress, [0.5, 1], [1, 0], { extrapolateLeft: "clamp" }),
|
||||
transform: `scale(${interpolate(exitProgress, [0, 1], [1, 1.5])})`,
|
||||
};
|
||||
case "none":
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// Typewriter effect for text
|
||||
const getDisplayText = (text: string): string => {
|
||||
if (scene.animation.entrance !== "typewriter") return text;
|
||||
const charsToShow = Math.floor(
|
||||
interpolate(frame, [0, entranceDuration * 1.5], [0, text.length], {
|
||||
extrapolateRight: "clamp",
|
||||
})
|
||||
);
|
||||
return text.slice(0, charsToShow);
|
||||
};
|
||||
|
||||
// Background
|
||||
const backgroundStyle: React.CSSProperties = scene.backgroundGradient
|
||||
? {
|
||||
background: `linear-gradient(${scene.backgroundGradient.direction}, ${scene.backgroundGradient.from}, ${scene.backgroundGradient.to})`,
|
||||
}
|
||||
: {
|
||||
backgroundColor: scene.style.backgroundColor,
|
||||
};
|
||||
|
||||
const entranceStyles = getEntranceStyles();
|
||||
const exitStyles = getExitStyles();
|
||||
|
||||
// Merge entrance and exit
|
||||
const combinedStyles: React.CSSProperties = {
|
||||
...entranceStyles,
|
||||
...(frame >= exitStart ? exitStyles : {}),
|
||||
};
|
||||
|
||||
// Subtle background animation
|
||||
const bgScale = interpolate(frame, [0, totalFrames], [1, 1.05], {
|
||||
extrapolateRight: "clamp",
|
||||
});
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
...backgroundStyle,
|
||||
transform: `scale(${bgScale})`,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Decorative elements */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-20%",
|
||||
right: "-10%",
|
||||
width: "40%",
|
||||
height: "40%",
|
||||
borderRadius: "50%",
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
filter: "blur(60px)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "-15%",
|
||||
left: "-10%",
|
||||
width: "35%",
|
||||
height: "35%",
|
||||
borderRadius: "50%",
|
||||
background: "rgba(255,255,255,0.03)",
|
||||
filter: "blur(40px)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: scene.style.textAlign === "left" ? "flex-start" : scene.style.textAlign === "right" ? "flex-end" : "center",
|
||||
justifyContent: "center",
|
||||
padding: "80px",
|
||||
maxWidth: "85%",
|
||||
textAlign: scene.style.textAlign,
|
||||
...combinedStyles,
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
{scene.icon && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: scene.style.fontSize * 1.2,
|
||||
marginBottom: "20px",
|
||||
filter: "drop-shadow(0 4px 8px rgba(0,0,0,0.3))",
|
||||
}}
|
||||
>
|
||||
{scene.icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title (small label above main text) */}
|
||||
{scene.title && scene.title !== scene.text && (
|
||||
<div
|
||||
style={{
|
||||
color: scene.style.textColor,
|
||||
fontSize: Math.max(scene.style.fontSize * 0.35, 18),
|
||||
fontFamily: scene.style.fontFamily,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "3px",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: "16px",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{scene.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main text */}
|
||||
<div
|
||||
style={{
|
||||
color: scene.style.textColor,
|
||||
fontSize: scene.style.fontSize,
|
||||
fontFamily: scene.style.fontFamily,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
textShadow: "0 2px 20px rgba(0,0,0,0.3)",
|
||||
letterSpacing: "-0.5px",
|
||||
}}
|
||||
>
|
||||
{getDisplayText(scene.text)}
|
||||
{scene.animation.entrance === "typewriter" && frame < entranceDuration * 1.5 && (
|
||||
<span
|
||||
style={{
|
||||
opacity: Math.sin(frame * 0.3) > 0 ? 1 : 0,
|
||||
marginLeft: "2px",
|
||||
}}
|
||||
>
|
||||
|
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
{scene.subtitle && (
|
||||
<div
|
||||
style={{
|
||||
color: scene.style.textColor,
|
||||
fontSize: Math.max(scene.style.fontSize * 0.45, 20),
|
||||
fontFamily: scene.style.fontFamily,
|
||||
fontWeight: 400,
|
||||
marginTop: "20px",
|
||||
opacity: 0.8,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{getDisplayText(scene.subtitle)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { AbsoluteFill, Sequence } from "remotion";
|
||||
import { VideoScript } from "../lib/types";
|
||||
import { SceneComponent } from "./Scene";
|
||||
|
||||
interface VideoProps {
|
||||
script: VideoScript;
|
||||
}
|
||||
|
||||
export const VideoComposition: React.FC<VideoProps> = ({ script }) => {
|
||||
let currentFrame = 0;
|
||||
|
||||
return (
|
||||
<AbsoluteFill style={{ backgroundColor: "#000" }}>
|
||||
{script.scenes.map((scene) => {
|
||||
const startFrame = currentFrame;
|
||||
const durationInFrames = scene.durationInSeconds * script.fps;
|
||||
currentFrame += durationInFrames;
|
||||
|
||||
return (
|
||||
<Sequence
|
||||
key={scene.id}
|
||||
from={startFrame}
|
||||
durationInFrames={durationInFrames}
|
||||
name={scene.title || `Scene ${scene.id}`}
|
||||
>
|
||||
<SceneComponent scene={scene} />
|
||||
</Sequence>
|
||||
);
|
||||
})}
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
4
skills/remotion-prompt-video/templates/src/index.ts
Normal file
4
skills/remotion-prompt-video/templates/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { registerRoot } from "remotion";
|
||||
import { RemotionRoot } from "./Root";
|
||||
|
||||
registerRoot(RemotionRoot);
|
||||
50
skills/remotion-prompt-video/templates/src/lib/types.ts
Normal file
50
skills/remotion-prompt-video/templates/src/lib/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Types for the prompt-to-video pipeline.
|
||||
* The LLM generates a VideoScript, which Remotion renders into a video.
|
||||
*/
|
||||
|
||||
export interface SceneStyle {
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
textAlign: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
export interface SceneAnimation {
|
||||
entrance: "fadeIn" | "slideUp" | "slideLeft" | "slideRight" | "zoomIn" | "typewriter" | "none";
|
||||
exit: "fadeOut" | "slideDown" | "slideLeft" | "slideRight" | "zoomOut" | "none";
|
||||
}
|
||||
|
||||
export interface Scene {
|
||||
id: number;
|
||||
title: string;
|
||||
text: string;
|
||||
subtitle?: string;
|
||||
durationInSeconds: number;
|
||||
style: SceneStyle;
|
||||
animation: SceneAnimation;
|
||||
backgroundGradient?: {
|
||||
from: string;
|
||||
to: string;
|
||||
direction: "to right" | "to bottom" | "to bottom right" | "to top right";
|
||||
};
|
||||
icon?: string; // emoji icon
|
||||
}
|
||||
|
||||
export interface VideoScript {
|
||||
title: string;
|
||||
description: string;
|
||||
scenes: Scene[];
|
||||
fps: number;
|
||||
width: number;
|
||||
height: number;
|
||||
totalDurationInSeconds: number;
|
||||
}
|
||||
|
||||
export interface GenerateRequest {
|
||||
prompt: string;
|
||||
style?: "modern" | "minimal" | "bold" | "playful" | "corporate";
|
||||
aspectRatio?: "16:9" | "9:16" | "1:1";
|
||||
maxScenes?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user