Newer
Older
remotion_docker_devcontainer / voicevox-remotion-template / src / lib / VQRemotionLib / timeline.ts
export type VQSpeechOptions<Voice = unknown> = Readonly<{
  subtitle?: string;
  voicevox?: Partial<Voice>;
  durationSeconds?: number;
}>;

export type VQSpeechEvent<
  CharacterId extends string = string,
  Voice = unknown
> = Readonly<{
  type: "say";
  id: string;
  character: CharacterId;
  text: string;
  subtitle?: string;
  voicevox?: Partial<Voice>;
  durationSeconds?: number;
}>;

export type VQStillObjectFit =
  | "cover"
  | "contain"
  | "fill"
  | "none"
  | "scale-down";

export type VQStillEvent = Readonly<{
  type: "still";
  id: string;
  imagePath: string;
  durationSeconds?: number;
  fit?: VQStillObjectFit;
  objectPosition?: string;
  opacity?: number;
}>;

export type VQWaitEvent = Readonly<{
  type: "wait";
  id: string;
  durationSeconds: number;
}>;

export type VQWaitEventInput = Readonly<{
  type: "wait";
  durationSeconds: number;
}>;

export type VQCustomTimelineEvent<
  Type extends string,
  Payload extends object = object
> = Readonly<
  {
    type: Type;
  } & Payload
>;

export type VQTimelineEvent<
  CharacterId extends string = string,
  Voice = unknown,
  ExtraEvent extends Readonly<{type: string}> = never
> = VQSpeechEvent<CharacterId, Voice> | VQStillEvent | VQWaitEvent | ExtraEvent;

export type VQTimelineInputEvent<
  CharacterId extends string = string,
  Voice = unknown,
  ExtraEvent extends Readonly<{type: string}> = never
> =
  | VQSpeechEvent<CharacterId, Voice>
  | VQStillEvent
  | VQWaitEvent
  | VQWaitEventInput
  | ExtraEvent;

type VQTimelineInputEventBase = Readonly<{type: string}>;

export type VQNormalizeTimelineEvent<Event> =
  Event extends Readonly<{type: "wait"; durationSeconds: infer Duration}>
    ? Readonly<Omit<Event, "id"> & {id: string; durationSeconds: Duration}>
    : Event;

export type VQDefinedTimeline<
  Timeline extends readonly VQTimelineInputEventBase[]
> = {
  readonly [Index in keyof Timeline]: VQNormalizeTimelineEvent<Timeline[Index]>;
};

// 用途: VOICEVOX発話をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で say("id", "character", "text", options) として呼び出す。
// オプションや引数詳細: options には字幕・VOICEVOX上書き・音声未生成時の秒数を指定できる。
export const say = <CharacterId extends string, Voice = unknown>(
  id: string,
  character: CharacterId,
  text: string,
  options: VQSpeechOptions<Voice> = {}
): VQSpeechEvent<CharacterId, Voice> => ({
  type: "say",
  id,
  character,
  text,
  ...options,
});

// 用途: 静止画をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で still("id", "path", options) として呼び出す。
// オプションや引数詳細: options には表示秒数・object-fit・表示位置・透明度を指定できる。
export const still = (
  id: string,
  imagePath: string,
  options: Omit<VQStillEvent, "type" | "id" | "imagePath"> = {}
): VQStillEvent => ({
  type: "still",
  id,
  imagePath,
  ...options,
});

// 用途: 何も発話しない待機時間をタイムラインイベントとして定義する。
// 使用方法: script.ts の timeline 内で wait(1) のように秒数だけを指定し、defineVQTimelineでIDを確定する。
// オプションや引数詳細: durationSeconds は待機秒数で、IDはtimeline単位で wait-001 から自動採番される。
export const wait = (durationSeconds: number): VQWaitEventInput => ({
  type: "wait",
  durationSeconds,
});

// 用途: waitイベントの連番からタイムライン内IDを作る。
// 使用方法: defineVQTimeline内で未解決waitイベントを正規化するときに呼び出す。
// オプションや引数詳細: index は1始まりのwait連番で、3桁ゼロ埋めにする。
const waitEventId = (index: number) =>
  `wait-${String(index).padStart(3, "0")}`;

// 用途: タイムライン配列を確定し、未解決のwaitイベントへ安定したIDを付与する。
// 使用方法: script.ts で defineVQTimeline([say(...), wait(1)]) の形でtimelineを定義する。
// オプションや引数詳細: wait IDはこの関数に渡された配列の中だけで先頭から採番される。
export const defineVQTimeline = <
  const Timeline extends readonly VQTimelineInputEventBase[]
>(
  timeline: Timeline
): VQDefinedTimeline<Timeline> => {
  let waitEventIndex = 0;

  return timeline.map((event) => {
    if (event.type === "wait") {
      waitEventIndex += 1;

      if (!("id" in event)) {
        return {
          ...event,
          id: waitEventId(waitEventIndex),
        };
      }
    }

    return event;
  }) as VQDefinedTimeline<Timeline>;
};

// 用途: タイムラインイベントから発話イベントだけを型安全に判定する。
// 使用方法: event.type を直接比較したくない場所で isVQSpeechEvent(event) として使う。
// オプションや引数詳細: CharacterId・Voice・拡張イベント型は呼び出し元のTimelineEventに合わせて推論される。
export const isVQSpeechEvent = <
  CharacterId extends string = string,
  Voice = unknown,
  ExtraEvent extends Readonly<{type: string}> = never
>(
  event: VQTimelineEvent<CharacterId, Voice, ExtraEvent>
): event is VQSpeechEvent<CharacterId, Voice> => event.type === "say";