Add .gitignore to exclude all node packages and lock files
This commit is contained in:
20
skills/remotion-prompt-video/templates/package.json
Normal file
20
skills/remotion-prompt-video/templates/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "remotion-prompt-video",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "render.mjs",
|
||||
"scripts": {
|
||||
"render": "node render.mjs",
|
||||
"studio": "npx remotion studio src/index.ts"
|
||||
},
|
||||
"description": "Generate videos from text prompts using AI + Remotion",
|
||||
"dependencies": {
|
||||
"@remotion/bundler": "^4.0.422",
|
||||
"@remotion/cli": "^4.0.422",
|
||||
"@remotion/renderer": "^4.0.422",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"remotion": "^4.0.422",
|
||||
"zod": "^3.22.3"
|
||||
}
|
||||
}
|
||||
89
skills/remotion-prompt-video/templates/render.mjs
Normal file
89
skills/remotion-prompt-video/templates/render.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* render.mjs - Renders a video from a VideoScript JSON file.
|
||||
*
|
||||
* Usage: node render.mjs <script.json> [output.mp4]
|
||||
*/
|
||||
import { bundle } from "@remotion/bundler";
|
||||
import { renderMedia, selectComposition } from "@remotion/renderer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.error("Usage: node render.mjs <script.json> [output.mp4]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scriptPath = path.resolve(args[0]);
|
||||
const outputPath = args[1]
|
||||
? path.resolve(args[1])
|
||||
: path.join(__dirname, "out", "video.mp4");
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log("Loading script:", scriptPath);
|
||||
const script = JSON.parse(fs.readFileSync(scriptPath, "utf-8"));
|
||||
|
||||
console.log(`Video: "${script.title}"`);
|
||||
console.log(` Scenes: ${script.scenes.length}`);
|
||||
console.log(` Duration: ${script.totalDurationInSeconds}s`);
|
||||
console.log(` Resolution: ${script.width}x${script.height}`);
|
||||
console.log(` FPS: ${script.fps}`);
|
||||
|
||||
const totalFrames = script.totalDurationInSeconds * script.fps;
|
||||
|
||||
console.log("\nBundling Remotion project...");
|
||||
const bundleLocation = await bundle({
|
||||
entryPoint: path.resolve(__dirname, "src/index.ts"),
|
||||
webpackOverride: (config) => config,
|
||||
});
|
||||
|
||||
console.log("Selecting composition...");
|
||||
const composition = await selectComposition({
|
||||
serveUrl: bundleLocation,
|
||||
id: "PromptVideo",
|
||||
inputProps: { script },
|
||||
});
|
||||
|
||||
// Override composition metadata with our script values
|
||||
composition.durationInFrames = totalFrames;
|
||||
composition.fps = script.fps;
|
||||
composition.width = script.width;
|
||||
composition.height = script.height;
|
||||
|
||||
console.log(`Rendering ${totalFrames} frames to ${outputPath} ...`);
|
||||
|
||||
let lastProgress = 0;
|
||||
await renderMedia({
|
||||
composition,
|
||||
serveUrl: bundleLocation,
|
||||
codec: "h264",
|
||||
outputLocation: outputPath,
|
||||
inputProps: { script },
|
||||
onProgress: ({ progress }) => {
|
||||
const pct = Math.floor(progress * 100);
|
||||
if (pct >= lastProgress + 10) {
|
||||
lastProgress = pct;
|
||||
process.stdout.write(` ${pct}%\r`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const stats = fs.statSync(outputPath);
|
||||
console.log(`\nDone! ${outputPath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Render failed:", err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
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;
|
||||
}
|
||||
18
skills/remotion-prompt-video/templates/tsconfig.json
Normal file
18
skills/remotion-prompt-video/templates/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "out"]
|
||||
}
|
||||
Reference in New Issue
Block a user