Generating Remotion Compositions Programmatically
Remotion compositions can be generated as React/TypeScript files from script data.
File Structure
Compositions are saved as .tsx files:
src/compositions/{articleId}.tsx
Composition Template
import { AbsoluteFill, Sequence } from 'remotion';
import { Slide } from '../components/Slide';
import type { Scene } from '../types';
export const Article6564191: React.FC = () => {
const scenes: Scene[] = [
{
sceneNumber: 1,
headline: "AIが変えるアニメ業界",
narration: "人工知能の台頭により...",
visualDescription: "Futuristic animation studio...",
duration: 12.5,
durationInFrames: 375,
animationConfig: { /* ... */ },
assets: { /* ... */ },
},
// ... more scenes
];
let currentFrame = 0;
return (
<AbsoluteFill>
{scenes.map((scene, index) => {
const sequence = (
<Sequence
key={scene.sceneNumber}
from={currentFrame}
durationInFrames={scene.durationInFrames}
>
<Slide scene={scene} />
</Sequence>
);
currentFrame += scene.durationInFrames - 30; // 30-frame overlap
return sequence;
})}
</AbsoluteFill>
);
};
export const article6564191Config = {
durationInFrames: 1446,
fps: 30,
size: { width: 1080, height: 1920 },
};
Generator Function
import fs from 'fs';
import path from 'path';
export function generateComposition(articleId: string, script: VideoScript): void {
const scenes = script.scenes;
// Calculate total duration with 30-frame overlaps
let totalFrames = 0;
scenes.forEach((scene, index) => {
totalFrames += scene.durationInFrames;
if (index < scenes.length - 1) {
totalFrames -= 30; // Subtract overlap
}
});
// Generate component code
const componentCode = `
import { AbsoluteFill, Sequence } from 'remotion';
import { Slide } from '../components/Slide';
import type { Scene } from '../types';
export const Article${articleId}: React.FC = () => {
const scenes: Scene[] = ${JSON.stringify(scenes, null, 2)};
let currentFrame = 0;
return (
<AbsoluteFill>
{scenes.map((scene, index) => {
const sequence = (
<Sequence
key={scene.sceneNumber}
from={currentFrame}
durationInFrames={scene.durationInFrames}
>
<Slide scene={scene} />
</Sequence>
);
currentFrame += scene.durationInFrames - 30;
return sequence;
})}
</AbsoluteFill>
);
};
export const article${articleId}Config = {
durationInFrames: ${totalFrames},
fps: 30,
size: { width: 1080, height: 1920 },
};
`;
// Write to file
const outputPath = path.join('src/compositions', `${articleId}.tsx`);
fs.writeFileSync(outputPath, componentCode);
}
Scene Component (Slide)
// src/components/Slide.tsx
export const Slide: React.FC<{ scene: Scene }> = ({ scene }) => {
const { animationConfig, assets } = scene;
return (
<div
style={{
opacity: calculateOpacity(scene), // For cross-fade
backgroundColor: '#000',
}}
>
<BackgroundAnimation
config={animationConfig?.background}
imageSrc={staticFile(`${assets?.imageUuid}.png`)}
/>
<SlideTitle
config={animationConfig?.headline}
text={scene.headline}
/>
<AudioPlayer src={staticFile(`${assets?.audioUuid}.wav`)} />
</div>
);
};
Using Static Files
Assets stored in public/ directory:
import { staticFile } from 'remotion';
// File: public/abc123def456.png
const imageSrc = staticFile('abc123def456.png');
Frame Timing
- FPS: 30 frames per second
- Cross-fade: 30 frames (1 second)
- Scene overlap: 30 frames between consecutive scenes
- Scene duration: audio frames + 30 frames
// Scene 1: Frame 0-431 (audio ends at 401, fades out 401-431)
// Scene 2: Frame 401-913 (fades in 401-431, audio ends at 883, fades out 883-913)
// Scene 3: Frame 883-1446 (fades in 883-913, ...)