Newer
Older
remotion_docker_devcontainer / voicevox-remotion-template / src / pizzaOvenProject01.ts
import React from "react";
import {
  AbsoluteFill,
  interpolate,
  Sequence,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";
import {
  characters,
  type SpeechEvent,
  type TimelineEvent,
} from "./data/pizza-oven-project-01/script";
import {
  audioFileForSpeech,
  hasAudioForSpeech,
  pizzaOvenProject01Scenario,
} from "./data/pizza-oven-project-01/timing";
import {roundedFontFamily} from "./fonts";
import {
  activeVQChronologicalScenarioSegmentForFrame,
  scheduleVQChronologicalScenario,
  VQLipSyncedStandeeImage,
  VQSpeechOverlay,
  VQWarmGradientBackground,
} from "./lib/VQRemotionLib";
import {getMouthForSpeechFrame} from "./lipsync/manifest";

const sayoAvatar = characters.sayo.avatar;
const PizzaOvenSpeechOverlay = VQSpeechOverlay<SpeechEvent>;

const clampInterpolation = {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
} as const;

const SayoStandee: React.FC<
  Readonly<{
    frame: number;
    fps: number;
    activeSpeech?: SpeechEvent;
    speakingLocalFrame: number;
  }>
> = ({frame, fps, activeSpeech, speakingLocalFrame}) => {
  const entrance = spring({
    frame,
    fps,
    config: {damping: 18, mass: 0.6},
  });
  const translateY = interpolate(entrance, [0, 1], [32, 0], clampInterpolation);
  const mouth = activeSpeech
    ? getMouthForSpeechFrame(activeSpeech.id, speakingLocalFrame, fps)
    : "rest";

  return React.createElement(
    "div",
    {
      style: {
        position: "absolute",
        right: 330,
        bottom: -82,
        width: 520,
        height: 720,
        display: "flex",
        justifyContent: "center",
        alignItems: "flex-end",
        transform: `translateY(${translateY}px)`,
        zIndex: 2,
      } satisfies React.CSSProperties,
    },
    React.createElement(VQLipSyncedStandeeImage, {
      imagePath: sayoAvatar.imagePath,
      mouthImageDir: sayoAvatar.mouthImageDir,
      mouth,
      width: "100%",
      height: "100%",
      maxHeight: "100%",
      filter: "drop-shadow(0 18px 40px rgba(31, 42, 68, 0.22))",
    })
  );
};

const TimelineOverlay: React.FC<Readonly<{event: TimelineEvent}>> = ({
  event,
}) => {
  const character = characters[event.character];

  return React.createElement(PizzaOvenSpeechOverlay, {
    speech: event,
    speakerName: character.displayName,
    accentColor: character.avatar.accentColor,
    hasAudio: hasAudioForSpeech,
    getAudioPath: audioFileForSpeech,
    subtitleOptions: {
      fontFamily: roundedFontFamily,
      fontSize: 40,
      lineHeight: 1.35,
      backgroundColor: "rgba(255, 255, 255, 0.9)",
    },
    containerStyle: {zIndex: 3},
  });
};

export const PizzaOvenProject01: React.FC = () => {
  const frame = useCurrentFrame();
  const {fps} = useVideoConfig();
  const scheduledEvents = scheduleVQChronologicalScenario(
    pizzaOvenProject01Scenario,
    fps
  );
  const activeSegment = activeVQChronologicalScenarioSegmentForFrame(
    scheduledEvents,
    frame
  );
  const isInsideActiveSegment = activeSegment
    ? frame < activeSegment.from + activeSegment.durationInFrames
    : false;
  const activeSpeech =
    activeSegment && isInsideActiveSegment ? activeSegment.event : undefined;
  const speakingLocalFrame =
    activeSegment && activeSpeech ? frame - activeSegment.from : 0;
  const sequences = scheduledEvents.map((scheduledEvent) =>
    React.createElement(
      Sequence,
      {
        key: scheduledEvent.event.id,
        from: scheduledEvent.from,
        durationInFrames: scheduledEvent.durationInFrames,
        premountFor: Math.min(fps, scheduledEvent.from),
      },
      React.createElement(TimelineOverlay, {event: scheduledEvent.event})
    )
  );

  return React.createElement(
    AbsoluteFill,
    {
      style: {
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
      },
    },
    React.createElement(VQWarmGradientBackground, null),
    React.createElement(SayoStandee, {
      frame,
      fps,
      activeSpeech,
      speakingLocalFrame,
    }),
    sequences
  );
};