130 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
};
|