AIエージェント生成のRemotionコードを検証する
執筆: RenderComp チーム 編集方針
RemotionはReactコードを動画ファイルに変換します。useCurrentFrame()が返す整数値を入力として受け取り、同じフレーム番号からは常に同じピクセル列を生成する、決定論的な計算モデルです。この特性のおかげで、アニメーションのロジックはバージョン管理に乗り、コードレビューの対象になります。
AIエージェントが生成したRemotionコードは、コンポーネントの構造が整っていてもfpsの決め打ちやspring()の引数省略が混入します。interpolate()の外挿設定の欠落も同じ理由で発生しやすいパターンです。Remotionのプレビューはフレーム単位で再生できますが、数フレームのずれや値の範囲逸脱は目視だけでは判別が難しい局面があります。
npx remotion renderを実行する前に、フレーム計算の整合性とspring()・interpolate()の引数、Sequenceのタイミングを静的に確認することで、レンダー後に発覚するずれを防げます。
fpsをuseVideoConfig()から取得しているか
AIが生成したコードでもっとも頻繁に見られる問題のひとつは、fpsを数値リテラルで決め打ちすることです。RemotionのコンポジションはfpsをRoot.tsx側で設定できるため、コンポーネント内で30を直書きすると、コンポジション設定を変更したときにアニメーションの速度がずれます。
import { useCurrentFrame, useVideoConfig } from "remotion";
// AIが生成しがちなコード
const FadeInBad: React.FC = () => {
const frame = useCurrentFrame();
// 30fps固定を前提にした計算
const opacity = frame / (1 * 30); // 1秒でフェードインのつもり
return <div style={{ opacity }} />;
};
// 修正後
const FadeInGood: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig(); // コンポジション設定から取得
const opacity = frame / (1 * fps); // fpsが変わっても1秒でフェードイン
return <div style={{ opacity }} />;
};
useVideoConfig()はコンポジションのfps、durationInFrames、width、heightを返します。durationInFramesを基準にしたタイミング計算やwidth・heightを使ったレイアウト計算でも、数値リテラルを直書きしているケースは同様に修正が必要です。
spring()のfps引数を省略していないか
spring()はRemotionが提供するスプリングアニメーション関数で、フレーム番号を渡すと0〜1の範囲で値が収束していきます。fps引数を渡さないと物理計算の時間軸がコンポジションの設定と一致しないため、60fpsのコンポジションでは設計より速くアニメーションが収束します。
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
// fpsを省略した誤り
const ScaleUpBad: React.FC = () => {
const frame = useCurrentFrame();
const scale = spring({
frame,
config: { stiffness: 100, damping: 10 },
// fps がない: Remotionの型定義では必須パラメータ
});
return <div style={{ transform: `scale(${scale})` }} />;
};
// 修正後
const ScaleUpGood: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
frame,
fps, // コンポジションのfpsに同期
config: {
stiffness: 120, // 高いほど速く収束。実用域は80〜200
damping: 14, // 低いほど振動が残る。10〜20が標準的
mass: 1,
},
delay: 6, // 6フレーム後に開始(30fpsなら約0.2秒)
});
return <div style={{ transform: `scale(${scale})` }} />;
};
AIが生成したコードでfps引数が存在するときも、useVideoConfig()からではなく数値リテラルで渡しているケースがあります。コードレビューではspring(を全件検索してfpsの由来を確認します。
interpolate()の外挿設定を確認する
interpolate()のデフォルト動作は、入力範囲の外側で出力値を線形に延長します。frameが入力範囲を外れた場合、opacityが1を超えたり0未満になったりします。CSSはopacityを0〜1にクランプしますが、transformやborderRadiusなど他のプロパティに使う場合は意図しない値が視覚に影響します。
import { interpolate, useCurrentFrame } from "remotion";
// 外挿を制限していない例
const FadeOutBad: React.FC = () => {
const frame = useCurrentFrame();
// frame > 29 になると opacity < 0 になる
const opacity = interpolate(frame, [0, 29], [1, 0]);
return <div style={{ opacity }} />;
};
// 修正後
const FadeOutGood: React.FC = () => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 29], [1, 0], {
extrapolateLeft: "clamp", // 入力範囲の左側で出力を固定
extrapolateRight: "clamp", // 入力範囲の右側で出力を固定
});
return <div style={{ opacity }} />;
};
extrapolateLeft: "clamp"とextrapolateRight: "clamp"を指定すれば、frameが範囲外に出ても出力は[1, 0]の区間内に収まります。opacity、scale、borderRadiusなど出力範囲が厳密に決まる値にinterpolate()を使う場合は、clampを必ず指定します。
Sequenceのフレーム計算を手で追う
複数のSequenceを組み合わせるとき、AIは各コンポーネントの表示期間を個別に計算することがあります。結果としてfromとdurationInFramesの組み合わせが意図したタイムラインと食い違い、重複期間が意図的なものかどうかコードから判断できなくなります。
import { AbsoluteFill, Sequence } from "remotion";
// 確認が必要な例: 数値リテラルだけでは意図が追いにくい
const TimelineBad: React.FC = () => {
return (
<AbsoluteFill>
{/* 0〜89フレーム */}
<Sequence from={0} durationInFrames={90}>
<ComponentA />
</Sequence>
{/* 60〜179フレーム: Aとの30フレーム重複が意図的かどうか不明 */}
<Sequence from={60} durationInFrames={120}>
<ComponentB />
</Sequence>
</AbsoluteFill>
);
};
// 定数で意図を明示した修正例
const TimelineGood: React.FC = () => {
const INTRO = 90; // イントロ: 3秒 at 30fps
const OVERLAP = 15; // トランジション重複: 0.5秒 at 30fps
const MAIN = 120; // メインコンテンツ: 4秒 at 30fps
return (
<AbsoluteFill>
{/* 0〜89フレーム: イントロ */}
<Sequence from={0} durationInFrames={INTRO}>
<ComponentA />
</Sequence>
{/* 75〜194フレーム: メイン(15フレームのオーバーラップ) */}
<Sequence from={INTRO - OVERLAP} durationInFrames={MAIN}>
<ComponentB />
</Sequence>
</AbsoluteFill>
);
};
レビュー時には各Sequenceの終了フレームをfrom + durationInFramesで計算し、コンポジションのdurationInFramesと照合します。from + durationInFramesがコンポジションの総フレーム数を超えている場合、RemotionはそのSequenceを途中でクリップします。
型チェックをレンダーの前に走らせる
RemotionのコードベースはTypeScriptを標準で使うため、tsc --noEmitを実行するだけでも多くの引数誤りを事前に検出できます。
# 型チェックのあとにレンダーを実行する
npx tsc --noEmit && npx remotion render src/index.ts MyComposition out/video.mp4
spring()のfps引数はRemotionのTypeScript型定義で必須のため、省略するとコンパイルエラーになります。一方、interpolate()のextrapolateLeftとextrapolateRightは省略可能な引数で出力の型はnumberのままなので、型チェックだけでは範囲外の値を検出できません。型チェックが届かない部分は、プレビューでフレームを0番と最終フレームに移動させ、表示が崩れていないことを確認します。