diff --git a/.cursor/rules/cli-scripts.mdc b/.cursor/rules/cli-scripts.mdc deleted file mode 100644 index a107aeab..00000000 --- a/.cursor/rules/cli-scripts.mdc +++ /dev/null @@ -1,12 +0,0 @@ ---- -description: CLI Scripts -globs: -alwaysApply: false ---- -- For CLI scripts, use TypeScript -- Use tsx vs ts-node -- If we need complex CLI arg parsing use Clipanion & Typanion -- Use execa for running commands. We use version 4 because not all repo's are ESM compatible yet -- Use dotenv to load secrets -- Have one main() or run() method that catches fails and exit(1) the process. Then have less excessive try/catches throughout the entire script -- Use `import { randomUUID } from 'node:crypto'` vs `import { v4 as uuidv4 } from 'uuid'` diff --git a/.cursor/rules/coding-style.mdc b/.cursor/rules/coding-style.mdc index da97b098..7fda7a28 100644 --- a/.cursor/rules/coding-style.mdc +++ b/.cursor/rules/coding-style.mdc @@ -1,14 +1,19 @@ --- -description: Coding Style -globs: -alwaysApply: false +description: +globs: +alwaysApply: true --- +Coding style: + - Favor `async run() {` over `run = async () => {` inside ES6 classes - Favor `if (!(err instanceof Error)) { throw new Error(`Was thrown a non-error: ${err}`) }` inside `catch` blocks to ensure the `error` is always an instance of `Error` - Favor using real paths (`../lib/schemas.ts`) over aliases (`@/app/lib/schemas`). - Favor `for (const comment of comments) {` over `comments.forEach((comment) => {` - Favor named exports over default exports, with the exception of Next.js pages -- Do not wrap each function body and function call in `try`/`catch` blocks. It pollutes the code. Assume we will always have an e.g. `main().catch((err) => { console.error(err); process.exit(1) })` to catch us. +- Do not wrap each function body and function call in `try`/`catch` blocks. It pollutes the code. Assume we will always have an e.g. `main().catch((err) => { console.error(err); process.exit(1) })` to catch us. I repeat: Avoid over-use of try-catch such as `try { // foo } catch (err) { console.error('error while foo'); throw err }`, assume we catch errors on a higher level and do not need the extra explananation. - Before creating new files and new code, see if we can leverage existing work, maybe slighty adapt that without breaking BC, to keep things DRY. - Favor early exits, so quickly `continue`, `return false` (or `throw` if needed), over nesting everything in positive conditions, creating christmas trees. +- Use Prettier with 100 char line width, single quotes for JS/TS, semi: false +- Use descriptive names: PascalCase for components/types, camelCase for variables/methods/schemas +- Alphabetize imports, group by source type (built-in/external/internal) diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc index 1f258b54..af795b3c 100644 --- a/.cursor/rules/general.mdc +++ b/.cursor/rules/general.mdc @@ -1,10 +1,13 @@ --- description: General -globs: +globs: alwaysApply: true --- +General: + - Do not touch `.env` files! - Favor Yarn (4) over npm - Never run any dev server yourself. I have one running that auto-reloads on changes. - Avoid blocking the conversation with terminal commands. For example: A) most of my git commands run through pagers, so pipe their output to `cat` to avoid blocking the terminal. B) You can use `tail` for logs, but be smart and use `-n` instead of `-f`, or the conversation will block +- Use the `gh` tool to interact with GitHub (search/view an Issue, create a PR). diff --git a/.cursor/rules/next.mdc b/.cursor/rules/next.mdc deleted file mode 100644 index 6b51e7aa..00000000 --- a/.cursor/rules/next.mdc +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: Next.js -globs: -alwaysApply: false ---- -- In case of Next.js, we use the `./src` directory, version 15, the App Router, and we have - shadcn/ui installed (`npx shadcn@latest add `). Assume we have a dev server running so - never start one unless specifically prompted. -- In case of Next.js, our config file is `next.config.ts`. Don't work in js or mjs alternatives. diff --git a/.cursor/rules/typescript.mdc b/.cursor/rules/typescript.mdc index bf2e28de..962f81ad 100644 --- a/.cursor/rules/typescript.mdc +++ b/.cursor/rules/typescript.mdc @@ -1,8 +1,10 @@ --- -description: TypeScript -globs: -alwaysApply: false +description: +globs: +alwaysApply: true --- +For Typescript: + - Favor `contentGapItemSchema = z.object()` over `ContentGapItemSchema = z.object()` - Favor `from './PosterboyCommand.ts'` over `from './PosterboyCommand'` - Favor `return ideas.filter(isPresent)` over `ideas.filter((idea): idea is Idea => idea !== null)` diff --git a/src/alphalib/types/assemblyStatus.ts b/src/alphalib/types/assemblyStatus.ts index e7a66816..30a08ed7 100644 --- a/src/alphalib/types/assemblyStatus.ts +++ b/src/alphalib/types/assemblyStatus.ts @@ -515,3 +515,72 @@ export const assemblyStatusSchema = z.union([ ]) export type AssemblyStatus = z.infer + +/** + * Type guard to check if an assembly has an error + */ +export function hasError( + assembly: AssemblyStatus | undefined | null, +): assembly is AssemblyStatus & { error: string } { + return ( + Boolean(assembly) && assembly != null && typeof assembly === 'object' && 'error' in assembly + ) +} + +/** + * Type guard to check if an assembly has an ok status + */ +export function hasOk( + assembly: AssemblyStatus | undefined | null, +): assembly is AssemblyStatus & { ok: string } { + return Boolean(assembly) && assembly != null && typeof assembly === 'object' && 'ok' in assembly +} + +/** + * Returns the error value if it exists or undefined + */ +export function getError(assembly: AssemblyStatus | undefined | null): string | undefined { + return assembly && assembly != null && typeof assembly === 'object' && 'error' in assembly + ? String(assembly.error) + : undefined +} + +/** + * Returns the ok value if it exists or undefined + */ +export function getOk(assembly: AssemblyStatus | undefined | null): string | undefined { + return assembly && assembly != null && typeof assembly === 'object' && 'ok' in assembly + ? String(assembly.ok) + : undefined +} + +/** + * This type and these functions below are compatibility helpers for + * working with partial assembly status objects during the transition + * from the old types to the new Zod-based schema. + */ +export type PartialAssemblyStatus = Partial + +export function hasErrorPartial( + assembly: PartialAssemblyStatus | undefined | null, +): assembly is PartialAssemblyStatus & { error: string } { + return ( + Boolean(assembly) && + assembly != null && + typeof assembly === 'object' && + 'error' in assembly && + Boolean(assembly.error) + ) +} + +export function hasOkPartial( + assembly: PartialAssemblyStatus | undefined | null, +): assembly is PartialAssemblyStatus & { ok: string } { + return ( + Boolean(assembly) && + assembly != null && + typeof assembly === 'object' && + 'ok' in assembly && + Boolean(assembly.ok) + ) +} diff --git a/src/alphalib/types/robots/_instructions-primitives.ts b/src/alphalib/types/robots/_instructions-primitives.ts index 3d56f849..a6fc99a2 100644 --- a/src/alphalib/types/robots/_instructions-primitives.ts +++ b/src/alphalib/types/robots/_instructions-primitives.ts @@ -1174,3 +1174,117 @@ export const filterCondition = z.union([ ]), ), ]) + +/** + * Parameters specific to the /video/encode robot. Useful for typing robots that pass files to /video/encode. + */ +export const videoEncodeSpecificInstructionsSchema = robotFFmpegVideo + .extend({ + resize_strategy: resize_strategy.describe(` +See the [available resize strategies](/docs/transcoding/image-manipulation/image-resize/#resize-strategies). +`), + zoom: z.boolean().default(true).describe(` +If this is set to \`false\`, smaller videos will not be stretched to the desired width and height. For details about the impact of zooming for your preferred resize strategy, see the list of available [resize strategies](/docs/transcoding/image-manipulation/image-resize/#resize-strategies). +`), + crop: unsafeCoordinatesSchema.optional().describe(` +Specify an object containing coordinates for the top left and bottom right corners of the rectangle to be cropped from the original video(s). Values can be integers for absolute pixel values or strings for percentage based values. + +For example: + +\`\`\`json + +{ + "x1": 80, + "y1": 100, + "x2": "60%", + "y2": "80%" +} + +\`\`\` + +This will crop the area from \`(80, 100)\` to \`(600, 800)\` from a 1000×1000 pixels video, which is a square whose width is 520px and height is 700px. If \`crop\` is set, the width and height parameters are ignored, and the \`resize_strategy\` is set to \`crop\` automatically. + +You can also use a JSON string of such an object with coordinates in similar fashion: \`"{ "x1": , "y1": , "x2": , "y2": }"\` +`), + background: color_with_alpha.default('#00000000').describe(` +The background color of the resulting video the \`"rrggbbaa"\` format (red, green, blue, alpha) when used with the \`"pad"\` resize strategy. The default color is black. +`), + rotate: z + // We can’t use enum. + // See https://github.com/colinhacks/zod/issues/2686 + .union([ + z.literal(0), + z.literal(90), + z.literal(180), + z.literal(270), + z.literal(360), + z.literal(false), + ]) + .optional().describe(` +Forces the video to be rotated by the specified degree integer. Currently, only multiples of \`90\` are supported. We automatically correct the orientation of many videos when the orientation is provided by the camera. This option is only useful for videos requiring rotation because it was not detected by the camera. If you set \`rotate\` to \`false\` no rotation is performed, even if the metadata contains such instructions. +`), + hint: z.boolean().default(false).describe(` +Enables hinting for mp4 files, for RTP/RTSP streaming. +`), + turbo: z.boolean().default(false).describe(` +Splits the video into multiple chunks so that each chunk can be encoded in parallel before all encoded chunks are stitched back together to form the result video. This comes at the expense of extra Priority Job Slots and may prove to be counter-productive for very small video files. +`), + chunk_duration: z.number().int().min(1).optional().describe(` +Allows you to specify the duration of each chunk when \`turbo\` is set to \`true\`. This means you can take advantage of that feature while using fewer Priority Job Slots. For instance, the longer each chunk is, the fewer Encoding Jobs will need to be used. +`), + watermark_url: z.string().default('').describe(` +A URL indicating a PNG image to be overlaid above this image. You can also [supply the watermark via another Assembly Step](/docs/transcoding/video-encoding/video-encode/#watermark-parameters-video-encode). +`), + watermark_position: z.union([positionSchema, z.array(positionSchema)]).default('center') + .describe(` +The position at which the watermark is placed. + +An array of possible values can also be specified, in which case one value will be selected at random, such as \`[ "center", "left", "bottom-left", "bottom-right" ]\`. + +This setting puts the watermark in the specified corner. To use a specific pixel offset for the watermark, you will need to add the padding to the image itself. +`), + watermark_x_offset: z.number().int().default(0).describe(` +The x-offset in number of pixels at which the watermark will be placed in relation to the position it has due to \`watermark_position\`. + +Values can be both positive and negative and yield different results depending on the \`watermark_position\` parameter. Positive values move the watermark closer to the image's center point, whereas negative values move the watermark further away from the image's center point. +`), + watermark_y_offset: z.number().int().default(0).describe(` +The y-offset in number of pixels at which the watermark will be placed in relation to the position it has due to \`watermark_position\`. + +Values can be both positive and negative and yield different results depending on the \`watermark_position\` parameter. Positive values move the watermark closer to the image's center point, whereas negative values move the watermark further away from the image's center point. +`), + watermark_size: percentageSchema.optional().describe(` +The size of the watermark, as a percentage, such as \`"50%"\`. How the watermark is resized greatly depends on the \`watermark_resize_strategy\`. +`), + watermark_resize_strategy: z.enum(['area', 'fit', 'stretch']).default('fit').describe(` +To explain how the resize strategies work, let's assume our target video size is 800×800 pixels and our watermark image is 400×300 pixels. Let's also assume, the \`watermark_size\` parameter is set to \`"25%"\`. + +For the \`"fit"\` resize strategy, the watermark is scaled so that the longer side of the watermark takes up 25% of the corresponding video side. And the other side is scaled according to the aspect ratio of the watermark image. So with our watermark, the width is the longer side, and 25% of the video size would be 200px. Hence, the watermark would be resized to 200×150 pixels. If the \`watermark_size\` was set to \`"50%"\`", it would be resized to 400×300 pixels (so just left at its original size). + +For the \`"stretch"\` resize strategy, the watermark image is stretched (meaning, it is resized without keeping its aspect ratio in mind) so that both sides take up 25% of the corresponding video side. Since our video is 800×800 pixels, for a watermark size of 25% the watermark would be resized to 200×200 pixels. Its height would appear stretched, because keeping the aspect ratio in mind it would be resized to 200×150 pixels instead. + +For the \`"area"\` resize strategy, the watermark is resized (keeping its aspect ratio in check) so that it covers \`"xx%"\` of the video's surface area. The value from \`watermark_size\` is used for the percentage area size. +`), + watermark_start_time: z.number().default(0).describe(` +The delay in seconds from the start of the video for the watermark to appear. By default the watermark is immediately shown. +`), + watermark_duration: z.number().default(-1).describe(` +The duration in seconds for the watermark to be shown. Can be used together with \`watermark_start_time\` to create nice effects. The default value is \`-1.0\`, which means that the watermark is shown for the entire duration of the video. +`), + watermark_opacity: z.number().min(0).max(1).default(1).describe(` +The opacity of the watermark. Valid values are between \`0\` (invisible) and \`1.0\` (full visibility). +`), + segment: z.boolean().default(false).describe(` +Splits the file into multiple parts, to be used for Apple's [HTTP Live Streaming](https://developer.apple.com/resources/http-streaming/). +`), + segment_duration: z.number().int().min(1).default(10).describe(` +Specifies the length of each HTTP segment. This is optional, and the default value as recommended by Apple is \`10\`. Do not change this value unless you have a good reason. +`), + segment_prefix: z.string().default('').describe(` +The prefix used for the naming. For example, a prefix of \`"segment_"\` would produce files named \`"segment_0.ts"\`, \`"segment_1.ts"\` and so on. This is optional, and defaults to the base name of the input file. Also see the related \`segment_name\` parameter. +`), + segment_name: z.string().default('').describe(` +The name used for the final segment. Available variables are \`\${segment_prefix}\`, \`\${segment_number}\` and \`\${segment_id}\` (which is a UUIDv4 without dashes). +`), + }) + .strict() diff --git a/src/alphalib/types/robots/image-resize.ts b/src/alphalib/types/robots/image-resize.ts index 61f937a1..b21b3e96 100644 --- a/src/alphalib/types/robots/image-resize.ts +++ b/src/alphalib/types/robots/image-resize.ts @@ -69,8 +69,35 @@ Width of the result in pixels. If not specified, will default to the width of th height: complexHeightSchema.optional().describe(` Height of the new image, in pixels. If not specified, will default to the height of the input image. `), - resize_strategy: z.enum(['crop', 'fillcrop', 'fit', 'min_fit', 'pad', 'stretch']).default('fit') - .describe(` + resize_strategy: z + .union([ + z.literal('crop') + .describe(`Cuts an area out of an image, discarding any overlapping parts. If the source image is smaller than the crop frame, it will be zoomed. This strategy is implied when you specify coordinates in the \`crop\` parameter, and cannot be used without it. + +To crop around human faces, see [🤖/image/facedetect](https://transloadit.com/docs/transcoding/artificial-intelligence/image-facedetect/) instead.`), + z.literal('fillcrop') + .describe(`Scales the image to fit into our 100×100 target while preserving aspect ratio, while trimming away any excess surface. This means both sides will become exactly 100 pixels, at the tradeoff of destroying parts of the image. + +By default the resulting image is horizontally/vertically centered to fill the target rectangle. Use the \`gravity\` parameter to change where to crop the image, such as \`"bottom\`" or \`"left\`".`), + z.literal('fit') + .describe(`Uses the larger side of the original image as a base for the resize. Aspect ratio is preserved. Either side will become at most 100 pixels. + +For example: resizing a 400×300 image into 100×100, would produce a 100×75 image.`), + z.literal('min_fit') + .describe(`Uses the **smaller** side of the original image as a base for the resize. After resizing, the larger side will have a larger value than specified. Aspect ratio is preserved. Either side will become at least 100 pixels. + +For example: resizing a 400×300 image into 100×100, would produce a 133×100 image.`), + z.literal('pad') + .describe(`Scales the image to fit while preserving aspect ratio. Both sides of the resized image become exactly 100 pixels, and any remaining surface is filled with a background color. + +In this example, the background color is determined by the [Assembly Variable](https://transloadit.com/docs/topics/assembly-instructions/#assembly-variables) \`\${file.meta.average_color}\`. If you set \`zoom\` to \`false\` (default is \`true\`), smaller images will be centered horizontally and vertically, and have the background padding all around them.`), + z + .literal('stretch') + .describe( + 'Ignores aspect ratio, resizing the image to the exact width and height specified. This may result in a stretched or distorted image.', + ), + ]) + .default('fit').describe(` See the list of available [resize strategies](/docs/transcoding/image-manipulation/image-resize/#resize-strategies). `), zoom: z.boolean().default(true).describe(` diff --git a/src/alphalib/types/robots/video-encode.ts b/src/alphalib/types/robots/video-encode.ts index 7b5630d2..45316b1a 100644 --- a/src/alphalib/types/robots/video-encode.ts +++ b/src/alphalib/types/robots/video-encode.ts @@ -1,15 +1,10 @@ import { z } from 'zod' import { - color_with_alpha, - robotFFmpegVideo, - percentageSchema, - positionSchema, - resize_strategy, robotBase, robotUse, - unsafeCoordinatesSchema, interpolateRobot, + videoEncodeSpecificInstructionsSchema, } from './_instructions-primitives.ts' import type { RobotMeta } from './_instructions-primitives.ts' @@ -46,115 +41,9 @@ export const meta: RobotMeta = { export const robotVideoEncodeInstructionsSchema = robotBase .merge(robotUse) - .merge(robotFFmpegVideo) + .merge(videoEncodeSpecificInstructionsSchema) .extend({ robot: z.literal('/video/encode'), - resize_strategy: resize_strategy.describe(` -See the [available resize strategies](/docs/transcoding/image-manipulation/image-resize/#resize-strategies). -`), - zoom: z.boolean().default(true).describe(` -If this is set to \`false\`, smaller videos will not be stretched to the desired width and height. For details about the impact of zooming for your preferred resize strategy, see the list of available [resize strategies](/docs/transcoding/image-manipulation/image-resize/#resize-strategies). -`), - crop: unsafeCoordinatesSchema.optional().describe(` -Specify an object containing coordinates for the top left and bottom right corners of the rectangle to be cropped from the original video(s). Values can be integers for absolute pixel values or strings for percentage based values. - -For example: - -\`\`\`json - -{ - "x1": 80, - "y1": 100, - "x2": "60%", - "y2": "80%" -} - -\`\`\` - -This will crop the area from \`(80, 100)\` to \`(600, 800)\` from a 1000×1000 pixels video, which is a square whose width is 520px and height is 700px. If \`crop\` is set, the width and height parameters are ignored, and the \`resize_strategy\` is set to \`crop\` automatically. - -You can also use a JSON string of such an object with coordinates in similar fashion: \`"{ "x1": , "y1": , "x2": , "y2": }"\` -`), - background: color_with_alpha.default('#00000000').describe(` -The background color of the resulting video the \`"rrggbbaa"\` format (red, green, blue, alpha) when used with the \`"pad"\` resize strategy. The default color is black. -`), - rotate: z - // We can’t use enum. - // See https://github.com/colinhacks/zod/issues/2686 - .union([ - z.literal(0), - z.literal(90), - z.literal(180), - z.literal(270), - z.literal(360), - z.literal(false), - ]) - .optional().describe(` -Forces the video to be rotated by the specified degree integer. Currently, only multiples of \`90\` are supported. We automatically correct the orientation of many videos when the orientation is provided by the camera. This option is only useful for videos requiring rotation because it was not detected by the camera. If you set \`rotate\` to \`false\` no rotation is performed, even if the metadata contains such instructions. -`), - hint: z.boolean().default(false).describe(` -Enables hinting for mp4 files, for RTP/RTSP streaming. -`), - turbo: z.boolean().default(false).describe(` -Splits the video into multiple chunks so that each chunk can be encoded in parallel before all encoded chunks are stitched back together to form the result video. This comes at the expense of extra Priority Job Slots and may prove to be counter-productive for very small video files. -`), - chunk_duration: z.number().int().min(1).optional().describe(` -Allows you to specify the duration of each chunk when \`turbo\` is set to \`true\`. This means you can take advantage of that feature while using fewer Priority Job Slots. For instance, the longer each chunk is, the fewer Encoding Jobs will need to be used. -`), - watermark_url: z.string().default('').describe(` -A URL indicating a PNG image to be overlaid above this image. You can also [supply the watermark via another Assembly Step](/docs/transcoding/video-encoding/video-encode/#watermark-parameters-video-encode). -`), - watermark_position: z.union([positionSchema, z.array(positionSchema)]).default('center') - .describe(` -The position at which the watermark is placed. - -An array of possible values can also be specified, in which case one value will be selected at random, such as \`[ "center", "left", "bottom-left", "bottom-right" ]\`. - -This setting puts the watermark in the specified corner. To use a specific pixel offset for the watermark, you will need to add the padding to the image itself. -`), - watermark_x_offset: z.number().int().default(0).describe(` -The x-offset in number of pixels at which the watermark will be placed in relation to the position it has due to \`watermark_position\`. - -Values can be both positive and negative and yield different results depending on the \`watermark_position\` parameter. Positive values move the watermark closer to the image's center point, whereas negative values move the watermark further away from the image's center point. -`), - watermark_y_offset: z.number().int().default(0).describe(` -The y-offset in number of pixels at which the watermark will be placed in relation to the position it has due to \`watermark_position\`. - -Values can be both positive and negative and yield different results depending on the \`watermark_position\` parameter. Positive values move the watermark closer to the image's center point, whereas negative values move the watermark further away from the image's center point. -`), - watermark_size: percentageSchema.optional().describe(` -The size of the watermark, as a percentage, such as \`"50%"\`. How the watermark is resized greatly depends on the \`watermark_resize_strategy\`. -`), - watermark_resize_strategy: z.enum(['area', 'fit', 'stretch']).default('fit').describe(` -To explain how the resize strategies work, let's assume our target video size is 800×800 pixels and our watermark image is 400×300 pixels. Let's also assume, the \`watermark_size\` parameter is set to \`"25%"\`. - -For the \`"fit"\` resize strategy, the watermark is scaled so that the longer side of the watermark takes up 25% of the corresponding video side. And the other side is scaled according to the aspect ratio of the watermark image. So with our watermark, the width is the longer side, and 25% of the video size would be 200px. Hence, the watermark would be resized to 200×150 pixels. If the \`watermark_size\` was set to \`"50%"\`", it would be resized to 400×300 pixels (so just left at its original size). - -For the \`"stretch"\` resize strategy, the watermark image is stretched (meaning, it is resized without keeping its aspect ratio in mind) so that both sides take up 25% of the corresponding video side. Since our video is 800×800 pixels, for a watermark size of 25% the watermark would be resized to 200×200 pixels. Its height would appear stretched, because keeping the aspect ratio in mind it would be resized to 200×150 pixels instead. - -For the \`"area"\` resize strategy, the watermark is resized (keeping its aspect ratio in check) so that it covers \`"xx%"\` of the video's surface area. The value from \`watermark_size\` is used for the percentage area size. -`), - watermark_start_time: z.number().default(0).describe(` -The delay in seconds from the start of the video for the watermark to appear. By default the watermark is immediately shown. -`), - watermark_duration: z.number().default(-1).describe(` -The duration in seconds for the watermark to be shown. Can be used together with \`watermark_start_time\` to create nice effects. The default value is \`-1.0\`, which means that the watermark is shown for the entire duration of the video. -`), - watermark_opacity: z.number().min(0).max(1).default(1).describe(` -The opacity of the watermark. Valid values are between \`0\` (invisible) and \`1.0\` (full visibility). -`), - segment: z.boolean().default(false).describe(` -Splits the file into multiple parts, to be used for Apple's [HTTP Live Streaming](https://developer.apple.com/resources/http-streaming/). -`), - segment_duration: z.number().int().min(1).default(10).describe(` -Specifies the length of each HTTP segment. This is optional, and the default value as recommended by Apple is \`10\`. Do not change this value unless you have a good reason. -`), - segment_prefix: z.string().default('').describe(` -The prefix used for the naming. For example, a prefix of \`"segment_"\` would produce files named \`"segment_0.ts"\`, \`"segment_1.ts"\` and so on. This is optional, and defaults to the base name of the input file. Also see the related \`segment_name\` parameter. -`), - segment_name: z.string().default('').describe(` -The name used for the final segment. Available variables are \`\${segment_prefix}\`, \`\${segment_number}\` and \`\${segment_id}\` (which is a UUIDv4 without dashes). -`), }) .strict() diff --git a/src/alphalib/types/robots/video-ondemand.ts b/src/alphalib/types/robots/video-ondemand.ts index 42c3ec57..e0f1a2de 100644 --- a/src/alphalib/types/robots/video-ondemand.ts +++ b/src/alphalib/types/robots/video-ondemand.ts @@ -5,7 +5,7 @@ import { interpolateRobot, robotBase, robotUse, - robotFFmpegVideo, + videoEncodeSpecificInstructionsSchema, } from './_instructions-primitives.ts' export const meta: RobotMeta = { @@ -68,7 +68,7 @@ export const robotVideoOndemandInstructionsSchema = robotBase .extend({ robot: z.literal('/video/ondemand'), variants: z - .record(robotFFmpegVideo) + .record(videoEncodeSpecificInstructionsSchema) .describe( 'Defines the variants the video player can choose from. The keys are the names of the variant as they will appear in the generated playlists and URLs.', ),