Documents/technical/Software/videos/src/shared/episodeScriptSpec.ts
ts • 11,277 bytes
import { z } from "zod";
const authorKeySchema = z.string()
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, "Use letters, numbers, underscores, or hyphens, starting with a letter.");
const nonEmptyStringSchema = z.string().trim().min(1);
const secondsSchema = z.number().finite();
const durationSecondsSchema = secondsSchema.nonnegative();
const anchorSchema = z.enum(["canvas", "camera"]);
const objectLayoutShape = {
height: z.number().finite().positive(),
originX: z.number().finite().optional(),
originY: z.number().finite().optional(),
rotation: z.number().finite().default(0),
scale: z.number().finite().positive().default(1),
width: z.number().finite().positive(),
x: z.number().finite(),
y: z.number().finite(),
};
const objectLayoutSchema = z.object(objectLayoutShape).strict();
const objectLayoutUpdateSchema = objectLayoutSchema.partial().refine(
(layout) => Object.keys(layout).length > 0,
"Provide at least one layout change.",
);
const mediaLayoutSchema = z.object({
height: z.number().finite().positive().optional(),
originX: z.number().finite().optional(),
originY: z.number().finite().optional(),
rotation: z.number().finite().default(0),
scale: z.number().finite().positive().default(1),
width: z.number().finite().positive().optional(),
x: z.number().finite(),
y: z.number().finite(),
}).strict().superRefine((layout, context) => {
if (layout.width === undefined && layout.height === undefined) {
context.addIssue({
code: "custom",
message: "Provide exactly one media dimension: layout.width or layout.height.",
});
}
if (layout.width !== undefined && layout.height !== undefined) {
context.addIssue({
code: "custom",
message: "Provide only one media dimension: layout.width or layout.height, not both.",
});
}
});
const mediaLayoutUpdateSchema = z.object({
height: z.number().finite().positive().optional(),
originX: z.number().finite().optional(),
originY: z.number().finite().optional(),
rotation: z.number().finite().optional(),
scale: z.number().finite().positive().optional(),
width: z.number().finite().positive().optional(),
x: z.number().finite().optional(),
y: z.number().finite().optional(),
}).strict().superRefine((layout, context) => {
if (Object.keys(layout).length === 0) {
context.addIssue({ code: "custom", message: "Provide at least one media layout change." });
}
if (layout.width !== undefined && layout.height !== undefined) {
context.addIssue({
code: "custom",
message: "Provide only one media dimension: layout.width or layout.height, not both.",
});
}
});
const graphLayoutSchema = z.object({
x: z.number().finite(),
y: z.number().finite(),
}).strict();
const cameraLayoutSchema = z.object({
height: z.number().finite().positive(),
width: z.number().finite().positive(),
x: z.number().finite(),
y: z.number().finite(),
}).strict();
export const claimSideSchema = z.enum(["pro", "con"])
.transform((side) => side === "pro" ? "pro-main" as const : "con-main" as const);
const actionTimingShape = {
blocking: z.boolean().optional(),
durationSeconds: durationSecondsSchema.optional(),
label: nonEmptyStringSchema.optional(),
offsetSeconds: secondsSchema.optional(),
};
const cssStyleValueSchema = z.union([z.string(), z.number().finite()]);
const cssStyleSchema = z.record(z.string(), cssStyleValueSchema).refine(
(style) => ![
"bottom",
"height",
"left",
"right",
"rotate",
"rotation",
"scale",
"top",
"transformOrigin",
"width",
].some((property) => property in style),
"Use numeric layout fields for object position, size, scale, rotation, and transform origin.",
);
const mediaSourceSchema = z.string().refine(
(source) => source.startsWith("media/")
&& source.length > "media/".length
&& !source.split("/").includes(".."),
"Use a media path relative to the episode JSON file, such as media/example.png.",
);
const objectAddShape = {
...actionTimingShape,
anchor: anchorSchema.default("canvas"),
durationSeconds: z.literal(0).optional(),
key: authorKeySchema,
layout: objectLayoutSchema,
style: cssStyleSchema.optional(),
};
const objectUpdateShape = {
...actionTimingShape,
key: authorKeySchema,
layout: objectLayoutUpdateSchema.optional(),
style: cssStyleSchema.optional(),
};
const objectAddBaseSchema = z.object(objectAddShape).strict();
const objectUpdateBaseSchema = z.object(objectUpdateShape).strict();
const confidenceTargetSchema = authorKeySchema;
const relevanceTargetSchema = z.object({
relevanceOf: authorKeySchema,
}).strict();
const claimTargetSchema = z.union([
confidenceTargetSchema,
relevanceTargetSchema,
]);
const graphClaimPresentationShape = {
showScore: z.boolean().optional(),
textReveal: z.boolean().optional(),
};
const scoreboardSchema = z.object({
anchor: anchorSchema.default("camera"),
height: z.number().finite().positive(),
numberWidth: z.number().finite().positive(),
thermometerWidth: z.number().finite().positive(),
x: z.number().finite(),
y: z.number().finite(),
}).strict();
const newGraphClaimSchema = z.object({
key: authorKeySchema,
side: claimSideSchema,
target: claimTargetSchema,
text: nonEmptyStringSchema,
...graphClaimPresentationShape,
}).strict();
const graphClaimStateSchema = z.object({
key: authorKeySchema,
side: claimSideSchema.optional(),
target: claimTargetSchema.optional(),
text: nonEmptyStringSchema.optional(),
...graphClaimPresentationShape,
}).strict();
const graphCreateActionSchema = z.object({
...actionTimingShape,
anchor: anchorSchema.default("canvas"),
claims: z.array(newGraphClaimSchema).optional(),
hideScores: z.boolean().optional(),
key: authorKeySchema,
layout: graphLayoutSchema,
mainClaim: z.object({
key: authorKeySchema,
text: nonEmptyStringSchema,
...graphClaimPresentationShape,
}).strict(),
scoreboard: scoreboardSchema.optional(),
type: z.literal("graph.create"),
}).strict();
const graphAddClaimActionSchema = z.object({
...actionTimingShape,
graph: authorKeySchema,
key: authorKeySchema,
side: claimSideSchema,
target: confidenceTargetSchema,
text: nonEmptyStringSchema,
...graphClaimPresentationShape,
type: z.literal("graph.addClaim"),
}).strict();
const graphSetActionSchema = z.object({
...actionTimingShape,
claims: z.array(graphClaimStateSchema).min(1),
graph: authorKeySchema,
type: z.literal("graph.set"),
}).strict();
const graphPatchActionSchema = z.object({
...actionTimingShape,
claims: z.array(graphClaimStateSchema),
graph: authorKeySchema,
removeConnectionsFrom: z.array(authorKeySchema),
type: z.literal("graph.patch"),
}).strict();
const cameraTargetTimingShape = {
offsetSeconds: secondsSchema.default(0),
};
const cameraObjectsTargetSchema = z.object({
...cameraTargetTimingShape,
objects: z.array(nonEmptyStringSchema).min(1),
"x%": z.number().finite().optional(),
"y%": z.number().finite().optional(),
"zoom%": z.number().finite().positive().optional(),
}).strict();
const cameraLayoutTargetSchema = z.object({
...cameraTargetTimingShape,
layout: cameraLayoutSchema,
}).strict();
const cameraTargetSchema = z.union([cameraObjectsTargetSchema, cameraLayoutTargetSchema]);
const cameraMoveActionSchema = z.object({
...actionTimingShape,
target: cameraTargetSchema,
type: z.literal("camera.move"),
}).strict();
const cameraCutActionSchema = z.object({
...actionTimingShape,
target: cameraTargetSchema,
type: z.literal("camera.cut"),
}).strict();
const mediaAddActionSchema = z.object({
...objectAddShape,
layout: mediaLayoutSchema,
source: mediaSourceSchema,
type: z.literal("media.add"),
}).strict();
const mediaUpdateActionSchema = z.object({
...objectUpdateShape,
layout: mediaLayoutUpdateSchema.optional(),
source: mediaSourceSchema.optional(),
type: z.literal("media.update"),
}).strict().refine(
(action) => action.source !== undefined || action.layout !== undefined || Object.keys(action.style ?? {}).length > 0,
"Provide a source, layout change, or at least one style change.",
);
const balanceAddActionSchema = z.object({
...objectAddShape,
scorePercent: z.number().finite().min(-100).max(100),
type: z.literal("balance.add"),
}).strict();
const balanceUpdateActionSchema = z.object({
...objectUpdateShape,
scorePercent: z.number().finite().min(-100).max(100).optional(),
type: z.literal("balance.update"),
}).strict().refine(
(action) => action.scorePercent !== undefined || action.layout !== undefined || Object.keys(action.style ?? {}).length > 0,
"Provide a scorePercent, layout change, or at least one style change.",
);
const captionsShowActionSchema = z.object({
...actionTimingShape,
anchor: anchorSchema.default("camera"),
durationSeconds: durationSecondsSchema.positive(),
position: z.literal("center").optional(),
text: nonEmptyStringSchema,
type: z.literal("captions.show"),
}).strict();
const waitActionSchema = z.object({
...actionTimingShape,
durationSeconds: durationSecondsSchema.positive(),
type: z.literal("wait"),
}).strict();
export const episodeActionSchema = z.discriminatedUnion("type", [
graphCreateActionSchema,
graphAddClaimActionSchema,
graphSetActionSchema,
graphPatchActionSchema,
cameraMoveActionSchema,
cameraCutActionSchema,
mediaAddActionSchema,
mediaUpdateActionSchema,
balanceAddActionSchema,
balanceUpdateActionSchema,
captionsShowActionSchema,
waitActionSchema,
]);
const defaultsSchema = z.object({
"camera.move": z.object({
durationSeconds: durationSecondsSchema.positive().optional(),
}).strict().optional(),
"graph.addClaim": z.object({
durationSeconds: durationSecondsSchema.positive().optional(),
}).strict().optional(),
"graph.create": z.object({
durationSeconds: durationSecondsSchema.optional(),
}).strict().optional(),
}).strict();
export const episodeScriptSpecSchema = z.object({
schemaVersion: z.literal(3),
script: z.array(episodeActionSchema).min(1),
settings: z.object({
composition: z.object({
fps: z.number().int().positive(),
height: z.number().int().positive(),
id: nonEmptyStringSchema,
width: z.number().int().positive(),
}).strict(),
defaults: defaultsSchema.optional(),
}).strict(),
}).strict();
export type ClaimSide = z.infer<typeof claimSideSchema>;
export type Anchor = z.infer<typeof anchorSchema>;
export type EpisodeAction = z.infer<typeof episodeActionSchema>;
export type EpisodeScriptSpecInput = z.input<typeof episodeScriptSpecSchema>;
export type EpisodeScriptSpec = z.infer<typeof episodeScriptSpecSchema>;
export type GraphAddClaimAction = z.infer<typeof graphAddClaimActionSchema>;
export type GraphClaimState = z.infer<typeof graphClaimStateSchema>;
export type GraphCreateAction = z.infer<typeof graphCreateActionSchema>;
export type GraphPatchAction = z.infer<typeof graphPatchActionSchema>;
export type GraphSetAction = z.infer<typeof graphSetActionSchema>;
export type CssStyle = z.infer<typeof cssStyleSchema>;
export type GraphLayout = z.infer<typeof graphLayoutSchema>;
export type MediaLayout = z.infer<typeof mediaLayoutSchema>;
export type MediaLayoutUpdate = z.infer<typeof mediaLayoutUpdateSchema>;
export type ObjectLayout = z.infer<typeof objectLayoutSchema>;
export type ObjectAdd = z.infer<typeof objectAddBaseSchema>;
export type ObjectUpdate = z.infer<typeof objectUpdateBaseSchema>;
export type ScoreboardLayout = z.infer<typeof scoreboardSchema>;