動画編集単価をオーバーヘッドと時間から算出する
執筆: RenderComp チーム 編集方針
Remotion は動画をフレームインデックスの関数として扱います。あるフレームに表示される値は、プロップスと現在フレームだけで一意に決まります。この性質を単価計算に応用すると、オーバーヘッド・ツール費・稼働時間という3変数の計算過程をそのままコンポーネントとして書けます。
代理店が見積もりを誤る場面の多くは、Remotion Lambda のようなレンダリング費用を変動費として別立てにせず、月次固定費だけを積算するケースです。Lambda はレンダリング中にのみ課金され、レンダリングごとにコストを推定して表示する仕組みになっています。この推定値を TypeScript の型として受け取り、Remotion のアニメーションコンポーネントとして描画するのが本稿の実装です。
計算ロジックを型で表現する
単価式の構造を TypeScript の型として先に固めます。
// rateCalc.ts
export type RateInputs = {
monthlyFixedCost: number; // 月次固定費(円)
monthlyBillableHours: number; // 月の請求可能時間(時)
editMinPerOutputMin: number; // 出力1分あたりの編集時間(分)
profitMargin: number; // 利益率 0〜1
lambdaCostPerOutputMin: number; // Lambda コスト(円 / 出力分)
};
export type RateResult = {
overheadPerHour: number;
toolCostPerHour: number;
hourlyRate: number;
ratePerOutputMin: number;
};
editMinPerOutputMin は計算式全体のキーになる変数です。出力動画1分を作るために編集者が費やす時間(分)を表し、案件ごとに実測して入力します。
export function calcRate(inputs: RateInputs): RateResult {
const {
monthlyFixedCost,
monthlyBillableHours,
editMinPerOutputMin,
profitMargin,
lambdaCostPerOutputMin,
} = inputs;
// 時給ベースのオーバーヘッド
const overheadPerHour = monthlyFixedCost / monthlyBillableHours;
// 編集1時間で生み出せる出力分数に Lambda コストを掛ける
const outputMinPerEditHour = 60 / editMinPerOutputMin;
const toolCostPerHour = lambdaCostPerOutputMin * outputMinPerEditHour;
// 利益率を反映した目標時給
const hourlyRate = (overheadPerHour + toolCostPerHour) / (1 - profitMargin);
// 出力1分あたりの請求単価
const ratePerOutputMin = hourlyRate * (editMinPerOutputMin / 60);
return { overheadPerHour, toolCostPerHour, hourlyRate, ratePerOutputMin };
}
toolCostPerHour の計算は outputMinPerEditHour を経由しています。Lambda のコストは出力の分数に対して発生するため、編集1時間あたりの出力量を求めてからスケールする順序が必要です。editMinPerOutputMin が1より大きい案件(典型的な編集作業)で lambdaCostPerOutputMin * 60 と直接書くと、実際より多くの出力分数を前提にした過大なツールコストを時給に上乗せすることになります。
Lambda のコスト特性を入力値に反映する
AWS Lambda 関数はメモリを 128 MB から 10,240 MB の範囲で 1 MB 単位で設定でき、1 回の呼び出しは最大 900 秒(15 分)実行できます。Remotion Lambda はレンダリングを並列チャンクに分割してこの制限内に収めるため、動画の長さに関係なくレンダリングが完了します。
課金はコンピュート消費量(GB 秒)に対して発生します。多くのユーザーは数分間の動画を数セント程度でレンダリングしており、Remotion Lambda は各レンダリング完了後に推定コストを表示します。lambdaCostPerOutputMin の実績値はこの画面から読み取り、数件の出力分数で割った平均値を入力します。案件の複雑さ(エフェクト密度や解像度)が変わると 1 出力分あたりのコストも変わるため、案件種別ごとに値を持つのが現実的です。
Remotion コンポーネントで内訳を描画する
calcRate の結果を 4 行のリストとして順番にアニメーションする RateBreakdown コンポーネントを書きます。各行を Sequence でラップし、from を 30 フレームずつずらすことで、行ごとに 1 秒の間隔で現れます(30 fps 時)。
// RateBreakdown.tsx
import React from "react";
import {
AbsoluteFill,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { calcRate, type RateInputs } from "./rateCalc";
type RowItem = { label: string; value: number; unit: string };
const AnimatedRow: React.FC<{ row: RowItem }> = ({ row }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: "clamp",
});
// mass: 0.5 で慣性を下げ、stiffness: 120 で素早く収束させる
const translateX = spring({
frame,
fps,
from: -40,
to: 0,
config: { mass: 0.5, damping: 12, stiffness: 120 },
});
return (
<div
style={{
opacity,
transform: `translateX(${translateX}px)`,
display: "flex",
justifyContent: "space-between",
color: "#e0e0e0",
fontSize: 28,
marginBottom: 32,
}}
>
<span>{row.label}</span>
<span>
{Math.round(row.value).toLocaleString("ja-JP")} {row.unit}
</span>
</div>
);
};
export const RateBreakdown: React.FC<{ inputs: RateInputs }> = ({ inputs }) => {
const result = calcRate(inputs);
const rows: RowItem[] = [
{ label: "オーバーヘッド(時給)", value: result.overheadPerHour, unit: "円/時" },
{ label: "Lambda コスト(時給換算)", value: result.toolCostPerHour, unit: "円/時" },
{ label: "目標時給", value: result.hourlyRate, unit: "円/時" },
{ label: "出力1分あたり単価", value: result.ratePerOutputMin, unit: "円/分" },
];
return (
<AbsoluteFill
style={{ background: "#0f0f0f", padding: 60, fontFamily: "monospace" }}
>
{rows.map((row, i) => (
<Sequence key={row.label} from={i * 30}>
<AnimatedRow row={row} />
</Sequence>
))}
</AbsoluteFill>
);
};
spring の config は mass: 0.5, stiffness: 120, damping: 12 に設定しています。mass を下げると初動が速くなり、damping: 12 でオーバーシュートをほぼ消します。数値のスライドインには大きな弾みは不要なため、この設定で落ち着いた動きになります。
Root.tsx に登録する
// Root.tsx
import { Composition } from "remotion";
import { RateBreakdown } from "./RateBreakdown";
import { type RateInputs } from "./rateCalc";
const defaultInputs: RateInputs = {
monthlyFixedCost: 0,
monthlyBillableHours: 1,
editMinPerOutputMin: 1,
profitMargin: 0.3,
lambdaCostPerOutputMin: 0,
};
export const RemotionRoot: React.FC = () => (
<Composition
id="RateBreakdown"
component={RateBreakdown}
// 4行 × 30フレームのオフセット + 最終行の静止余白 60 フレーム
durationInFrames={4 * 30 + 60}
fps={30}
width={1280}
height={720}
defaultProps={{ inputs: defaultInputs }}
/>
);
defaultProps に RateInputs 型の値を渡すと、Remotion Studio のパラメータパネルから各フィールドを直接編集してプレビューできます。行数が変わる場合は durationInFrames を rows.length * 30 + 60 で計算し直します。
Remotion のライセンスは従業員3人以下の営利企業を無償利用の対象にしています。小規模な動画制作チームがこの構成を始める場合、初期の追加コストは Lambda の実行費用のみです。lambdaCostPerOutputMin の実績値収集は、最初の数件のレンダリング完了後に Remotion Lambda が表示する推定コストを記録するところから始まります。