Edit cards showed English words such as "crop · flip · extend" to every user. edit_image now takes an optional title written by the bot in the user's language, used for the card and the file name like edit_video. Without one, the card falls back to the repaint prompt or to language-neutral tokens only: sizes, ratios, degrees, arrows and format names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
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<typeof EditVideoStepSchema>;
|
|
|
|
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<typeof EditVideoInputSchema>;
|
|
|
|
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<typeof ExportVideoInputSchema>;
|