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";