AI-video-lifeplan-demo/src/Composition.tsx

130 lines
3.7 KiB
TypeScript

import React, { useMemo } from "react";
import {
AbsoluteFill,
Audio,
Sequence,
useCurrentFrame,
useVideoConfig,
staticFile,
} from "remotion";
import type { RenderProps } from "../frontend/src/render-props-types";
import { DualChartView } from "./DualChartView";
import { Avatar } from "./Avatar";
export const MainVideo: React.FC<any> = (props) => {
const { charts, script, meta } = props;
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Convert script segments to frame-based timeline
const timeline = useMemo(() => {
let currentFrame = 0;
return script.map((segment: any) => {
const startFrame = currentFrame;
const durationInFrames = Math.round(segment.duration * fps);
currentFrame += durationInFrames;
return {
...segment,
startFrame,
durationInFrames,
};
});
}, [script, fps]);
// Find current segment for subtitle display
const currentSegment = timeline.find(
(seg: any) => frame >= seg.startFrame && frame < seg.startFrame + seg.durationInFrames
);
// Prepare cashflow data for DualChartView
const cashflowTimeline = useMemo(
() =>
charts.cashFlow.timeline.map((item: any) => {
const totalExpense =
item.totalExpense ??
item.expenses.living +
item.expenses.housing +
item.expenses.education +
item.expenses.tax +
item.expenses.event;
const netCashFlow = item.netCashFlow ?? item.income.total - totalExpense;
return {
...item,
totalExpense,
netCashFlow,
assetBalance: item.assetBalance ?? 0,
};
}),
[charts.cashFlow.timeline]
);
const cashflowData = useMemo(
() => ({
timeline: cashflowTimeline,
meta: charts.cashFlow.meta,
}),
[cashflowTimeline, charts.cashFlow.meta]
);
return (
<AbsoluteFill style={{ backgroundColor: "white" }}>
{/* Render each script segment as a Sequence */}
{timeline.map((segment: any) => (
<Sequence
key={segment.id}
from={segment.startFrame}
durationInFrames={segment.durationInFrames}
>
{/* Scene content based on segment type */}
{segment.type === "chart_intro" || segment.type === "chart_highlight" ? (
<DualChartView cashflowData={cashflowData} />
) : (
// For narration segments, show chart with overlay
<DualChartView cashflowData={cashflowData} />
)}
{/* Audio narration (if available) */}
{segment.audioPath && (
<Audio src={staticFile(segment.audioPath.replace(/\\/g, "/"))} />
)}
{/* Subtitle overlay */}
<AbsoluteFill
style={{
justifyContent: "flex-end",
alignItems: "center",
paddingBottom: 80,
}}
>
<div
style={{
backgroundColor: "rgba(0, 0, 0, 0.7)",
color: "white",
padding: "16px 32px",
borderRadius: 8,
fontSize: 28,
fontWeight: "500",
maxWidth: "80%",
textAlign: "center",
fontFamily: "Noto Sans JP, sans-serif",
}}
>
{segment.text}
</div>
</AbsoluteFill>
{/* Avatar (Virtual Human) - only speaks during narration segments */}
<Avatar
isSpeaking={segment.type === "narration" || segment.type === "chart_intro"}
position="bottom-right"
size={180}
/>
</Sequence>
))}
</AbsoluteFill>
);
};