import React from "react";
import {Video} from "@remotion/media";
import {
AbsoluteFill,
Sequence,
staticFile,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import {
audioFileForSpeech,
durationForSpeech,
hasAudioForSpeech,
PIZZA_KILN_GAP_FRAMES,
PIZZA_KILN_VIDEO_FRAMES,
} from "./data/pizza-kiln/timing";
import {
characters,
timeline,
type SpeechEvent,
} from "./data/pizza-kiln/script";
import {roundedFontFamily} from "./fonts";
import {
VQSpeechOverlay,
VQStageCornerStandee,
VQWarmGradientBackground,
vqSpeakingAvatarAnimations,
} from "./lib/VQRemotionLib";
import {getMouthForSpeechFrame} from "./lipsync/manifest";
const BACKGROUND_VIDEO_PATH = "video/pizza-kiln-background.mp4";
const pizzaSubtitleOptions = {
fontFamily: roundedFontFamily,
fontSize: 34,
lineHeight: 1.45,
backgroundColor: "rgba(255, 255, 255, 0.9)",
} as const;
const SayoStandee: React.FC<
Readonly<{
mode: "stage" | "corner";
speaking: boolean;
localFrame: number;
fps: number;
speechId?: string;
}>
> = ({mode, speaking, localFrame, fps, speechId}) => {
const {avatar} = characters.sayo;
const speakingAnimationType = avatar.speakingAnimationType ?? "none";
const translateY = speaking
? vqSpeakingAvatarAnimations[speakingAnimationType]({
frame: localFrame,
fps,
focused: true,
hasMultipleCharacters: false,
})
: 0;
const mouth =
speaking && speakingAnimationType === "rhubarbLipSync"
? getMouthForSpeechFrame(speechId, localFrame, fps)
: "rest";
return (
<VQStageCornerStandee
mode={mode}
imagePath={avatar.imagePath}
mouthImageDir={avatar.mouthImageDir}
mouth={mouth}
translateY={translateY}
zIndex={3}
/>
);
};
const SpeechOverlay: React.FC<Readonly<{speech: SpeechEvent}>> = ({speech}) => {
const character = characters[speech.character];
return (
<VQSpeechOverlay
speech={speech}
speakerName={character.displayName}
accentColor={character.avatar.accentColor}
hasAudio={hasAudioForSpeech}
getAudioPath={audioFileForSpeech}
subtitleOptions={pizzaSubtitleOptions}
containerStyle={{zIndex: 4}}
/>
);
};
export const PizzaKilnSayoComposition: React.FC = () => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const introSpeech = timeline[0];
const outroSpeech = timeline[1];
const introFrames = durationForSpeech(introSpeech, fps);
const outroFrames = durationForSpeech(outroSpeech, fps);
const videoFrom = introFrames + PIZZA_KILN_GAP_FRAMES;
const outroFrom = videoFrom + PIZZA_KILN_VIDEO_FRAMES + PIZZA_KILN_GAP_FRAMES;
const isVideoVisible =
frame >= videoFrom && frame < videoFrom + PIZZA_KILN_VIDEO_FRAMES;
const isOutro = frame >= outroFrom;
const activeSpeech =
frame < introFrames ? introSpeech : isOutro ? outroSpeech : undefined;
const speechLocalFrame = activeSpeech === outroSpeech ? frame - outroFrom : frame;
return (
<AbsoluteFill style={{backgroundColor: "#1a1a1a"}}>
<VQWarmGradientBackground />
<Sequence
from={videoFrom}
durationInFrames={PIZZA_KILN_VIDEO_FRAMES}
premountFor={Math.min(fps, videoFrom)}
>
<Video
muted
objectFit="cover"
src={staticFile(BACKGROUND_VIDEO_PATH)}
style={{
width: "100%",
height: "100%",
}}
/>
</Sequence>
<SayoStandee
mode={isVideoVisible ? "corner" : "stage"}
speaking={Boolean(activeSpeech)}
localFrame={speechLocalFrame}
fps={fps}
speechId={activeSpeech?.id}
/>
<Sequence durationInFrames={introFrames} premountFor={0}>
<SpeechOverlay speech={introSpeech} />
</Sequence>
<Sequence
from={outroFrom}
durationInFrames={outroFrames}
premountFor={Math.min(fps, outroFrom)}
>
<SpeechOverlay speech={outroSpeech} />
</Sequence>
</AbsoluteFill>
);
};