import * as z from "zod"; import { Id } from "./ids.js"; import { MediaFitSchema, MediaPresetIdSchema } from "./media.js"; /** * Inputs of the `edit_video` and `export_video` media operations, which are also the arguments * of the tools with those names. Times are seconds on the timeline left by the previous steps. */ const Seconds = z.number().min(0).max(86_400); const TimeRangeSchema = z .object({ start: Seconds, end: Seconds }) .refine((range) => range.end > range.start, "end must be after start"); /** Written by the bot in the user's language; shown on the media card and used as file name. */ export const MediaTitleSchema = z.string().trim().min(1).max(120); export const EditVideoStepSchema = z.discriminatedUnion("op", [ z .object({ op: z.literal("trim"), start: Seconds, end: Seconds }) .refine((step) => step.end > step.start, "end must be after start"), /** Keeps these parts and cuts everything between them. */ z.object({ op: z.literal("keep"), segments: z.array(TimeRangeSchema).min(1).max(50) }), /** Appends these videos, fitted to the current frame. */ z.object({ op: z.literal("concat"), artifact_ids: z.array(Id).min(1).max(20) }), z.object({ op: z.literal("reframe"), preset: MediaPresetIdSchema, fit: MediaFitSchema.default("crop"), }), z.object({ op: z.literal("speed"), factor: z.number().min(0.25).max(4) }), /** Linear gain: 0.5 halves the loudness, 2 doubles it. */ z.object({ op: z.literal("volume"), gain: z.number().min(0).max(8) }), z.object({ op: z.literal("mute") }), /** Burns in an SRT or VTT artifact. */ z.object({ op: z.literal("subtitles"), artifact_id: Id }), /** Last step only: the result is the audio as M4A. */ z.object({ op: z.literal("extract_audio") }), /** Cover frame at this second of the result; as the only kind of step, the result is that image. */ z.object({ op: z.literal("poster"), at: Seconds }), ]); export type EditVideoStep = z.infer; export const EditVideoInputSchema = z .object({ artifact_id: Id, steps: z.array(EditVideoStepSchema).min(1).max(20), title: MediaTitleSchema, }) .superRefine((input, context) => { const ops = input.steps.map((step) => step.op); const extract = ops.indexOf("extract_audio"); if (extract >= 0 && extract !== ops.length - 1) { context.addIssue({ code: "custom", path: ["steps", extract], message: "extract_audio must be the last step", }); } if (extract >= 0 && ops.includes("poster")) { context.addIssue({ code: "custom", path: ["steps"], message: "poster cannot be combined with extract_audio", }); } if (ops.filter((op) => op === "poster").length > 1) { context.addIssue({ code: "custom", path: ["steps"], message: "use at most one poster step" }); } }); export type EditVideoInput = z.infer; export const ExportVideoInputSchema = z.object({ artifact_id: Id, presets: z.array(MediaPresetIdSchema).min(1).max(6), fit: MediaFitSchema.default("crop"), title: MediaTitleSchema.optional(), }); export type ExportVideoInput = z.infer;