vargai 0.4.0-alpha62 → 0.4.0-alpha63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -70,7 +70,7 @@
70
70
  "zod": "^4.2.1"
71
71
  },
72
72
  "sideEffects": false,
73
- "version": "0.4.0-alpha62",
73
+ "version": "0.4.0-alpha63",
74
74
  "exports": {
75
75
  ".": "./src/index.ts",
76
76
  "./ai": "./src/ai-sdk/index.ts",
@@ -175,9 +175,20 @@ const IMAGE_MODELS: Record<string, string> = {
175
175
  "recraft-v3": "fal-ai/recraft/v3/text-to-image",
176
176
  "nano-banana-pro": "fal-ai/nano-banana-pro",
177
177
  "nano-banana-pro/edit": "fal-ai/nano-banana-pro/edit",
178
+ "nano-banana-2": "fal-ai/nano-banana-2/edit",
179
+ "nano-banana-2/edit": "fal-ai/nano-banana-2/edit",
178
180
  "seedream-v4.5/edit": "fal-ai/bytedance/seedream/v4.5/edit",
181
+ // Qwen Image 2 - text-to-image and image-to-image editing (standard + pro)
182
+ "qwen-image-2": "fal-ai/qwen-image-2/text-to-image",
183
+ "qwen-image-2/edit": "fal-ai/qwen-image-2/edit",
184
+ "qwen-image-2-pro": "fal-ai/qwen-image-2/pro/text-to-image",
185
+ "qwen-image-2-pro/edit": "fal-ai/qwen-image-2/pro/edit",
179
186
  // Qwen Image Edit 2511 Multiple Angles - camera angle adjustment
180
187
  "qwen-angles": "fal-ai/qwen-image-edit-2511-multiple-angles",
188
+ // Recraft V4 Pro - text-to-image
189
+ "recraft-v4-pro": "fal-ai/recraft/v4/pro/text-to-image",
190
+ // Reve - image editing
191
+ "reve/edit": "fal-ai/reve/edit",
181
192
  };
182
193
 
183
194
  // Models that use image_size instead of aspect_ratio
@@ -186,11 +197,19 @@ const IMAGE_SIZE_MODELS = new Set([
186
197
  "flux-dev",
187
198
  "flux-pro",
188
199
  "seedream-v4.5/edit",
200
+ "qwen-image-2",
201
+ "qwen-image-2/edit",
202
+ "qwen-image-2-pro",
203
+ "qwen-image-2-pro/edit",
204
+ "recraft-v4-pro",
189
205
  ]);
190
206
 
191
207
  // Qwen Angles model - image-to-image with camera angle adjustment
192
208
  const QWEN_ANGLES_MODEL = "qwen-angles";
193
209
 
210
+ // Models that use singular image_url instead of image_urls array
211
+ const SINGULAR_IMAGE_URL_MODELS = new Set(["reve/edit"]);
212
+
194
213
  // Map aspect ratio to image_size for Qwen Angles (base dimension 1024)
195
214
  const ASPECT_RATIO_TO_QWEN_SIZE: Record<
196
215
  string,
@@ -848,7 +867,13 @@ class FalImageModel implements ImageModelV3 {
848
867
  modelId: this.modelId,
849
868
  fileHashes,
850
869
  });
851
- input.image_urls = await pMap(files, fileToUrl, { concurrency: 2 });
870
+ const imageUrls = await pMap(files, fileToUrl, { concurrency: 2 });
871
+ // Reve uses singular image_url instead of image_urls array
872
+ if (SINGULAR_IMAGE_URL_MODELS.has(this.modelId)) {
873
+ input.image_url = imageUrls[0];
874
+ } else {
875
+ input.image_urls = imageUrls;
876
+ }
852
877
  }
853
878
 
854
879
  if (isQwenAngles && !input.image_urls) {
@@ -6,8 +6,12 @@ export { definition as elevenlabsTts } from "./elevenlabs";
6
6
  export { definition as flux } from "./flux";
7
7
  export { definition as kling } from "./kling";
8
8
  export { definition as llama } from "./llama";
9
+ export { definition as nanoBanana2 } from "./nano-banana-2";
9
10
  export { definition as nanoBananaPro } from "./nano-banana-pro";
10
11
  export { definition as omnihuman } from "./omnihuman";
12
+ export { definition as qwenImage2 } from "./qwen-image-2";
13
+ export { definition as recraftV4 } from "./recraft-v4";
14
+ export { definition as reve } from "./reve";
11
15
  export { definition as sonauto } from "./sonauto";
12
16
  export { definition as soul } from "./soul";
13
17
  export { definition as veedFabric } from "./veed-fabric";
@@ -19,8 +23,12 @@ import { definition as elevenlabsDefinition } from "./elevenlabs";
19
23
  import { definition as fluxDefinition } from "./flux";
20
24
  import { definition as klingDefinition } from "./kling";
21
25
  import { definition as llamaDefinition } from "./llama";
26
+ import { definition as nanoBanana2Definition } from "./nano-banana-2";
22
27
  import { definition as nanoBananaProDefinition } from "./nano-banana-pro";
23
28
  import { definition as omnihumanDefinition } from "./omnihuman";
29
+ import { definition as qwenImage2Definition } from "./qwen-image-2";
30
+ import { definition as recraftV4Definition } from "./recraft-v4";
31
+ import { definition as reveDefinition } from "./reve";
24
32
  import { definition as sonautoDefinition } from "./sonauto";
25
33
  import { definition as soulDefinition } from "./soul";
26
34
  import { definition as veedFabricDefinition } from "./veed-fabric";
@@ -31,6 +39,10 @@ export const allModels = [
31
39
  klingDefinition,
32
40
  fluxDefinition,
33
41
  nanoBananaProDefinition,
42
+ nanoBanana2Definition,
43
+ qwenImage2Definition,
44
+ recraftV4Definition,
45
+ reveDefinition,
34
46
  wanDefinition,
35
47
  omnihumanDefinition,
36
48
  veedFabricDefinition,
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Nano Banana 2 image editing model (Google's next-gen image generation/editing)
3
+ * Edit-only model requiring image_urls input
4
+ */
5
+
6
+ import { z } from "zod";
7
+ import type { ModelDefinition, ZodSchema } from "../../core/schema/types";
8
+
9
+ // Nano Banana 2 resolution options (includes 0.5K unlike nano-banana-pro)
10
+ const nanoBanana2ResolutionSchema = z.enum(["0.5K", "1K", "2K", "4K"]);
11
+
12
+ // Nano Banana 2 aspect ratio options (supports "auto" unlike nano-banana-pro)
13
+ const nanoBanana2AspectRatioSchema = z.enum([
14
+ "auto",
15
+ "21:9",
16
+ "16:9",
17
+ "3:2",
18
+ "4:3",
19
+ "5:4",
20
+ "1:1",
21
+ "4:5",
22
+ "3:4",
23
+ "2:3",
24
+ "9:16",
25
+ ]);
26
+
27
+ // Output format options
28
+ const nanoBanana2OutputFormatSchema = z.enum(["png", "jpeg", "webp"]);
29
+
30
+ // Safety tolerance level (string enum "1"-"6", unlike nano-banana-pro's semantic filter)
31
+ const nanoBanana2SafetyToleranceSchema = z.enum(["1", "2", "3", "4", "5", "6"]);
32
+
33
+ // Input schema with Zod
34
+ const nanoBanana2InputSchema = z.object({
35
+ prompt: z.string().describe("Text description for image editing"),
36
+ image_urls: z
37
+ .array(z.string().url())
38
+ .describe(
39
+ "Input image URLs for image-to-image editing. Required for this model.",
40
+ ),
41
+ resolution: nanoBanana2ResolutionSchema
42
+ .default("1K")
43
+ .describe(
44
+ "Output resolution: 0.5K (512px), 1K (1024px), 2K (2048px), or 4K",
45
+ ),
46
+ aspect_ratio: nanoBanana2AspectRatioSchema
47
+ .default("auto")
48
+ .describe("Output aspect ratio. 'auto' preserves input aspect ratio."),
49
+ output_format: nanoBanana2OutputFormatSchema
50
+ .default("png")
51
+ .describe("Output image format"),
52
+ safety_tolerance: nanoBanana2SafetyToleranceSchema
53
+ .default("4")
54
+ .describe("Safety tolerance level: 1 (most strict) to 6 (least strict)"),
55
+ num_images: z
56
+ .number()
57
+ .int()
58
+ .min(1)
59
+ .max(4)
60
+ .default(1)
61
+ .describe("Number of images to generate (1-4)"),
62
+ seed: z
63
+ .number()
64
+ .int()
65
+ .optional()
66
+ .describe("Seed for the random number generator"),
67
+ limit_generations: z
68
+ .boolean()
69
+ .default(true)
70
+ .describe(
71
+ "Limit generations from each round of prompting to 1. May affect quality.",
72
+ ),
73
+ enable_web_search: z
74
+ .boolean()
75
+ .default(false)
76
+ .describe(
77
+ "Enable web search to use latest information for image generation",
78
+ ),
79
+ });
80
+
81
+ // Output schema with Zod
82
+ const nanoBanana2OutputSchema = z.object({
83
+ images: z.array(
84
+ z.object({
85
+ url: z.string(),
86
+ file_name: z.string().optional(),
87
+ content_type: z.string().optional(),
88
+ }),
89
+ ),
90
+ description: z.string().optional(),
91
+ });
92
+
93
+ // Schema object for the definition
94
+ const schema: ZodSchema<
95
+ typeof nanoBanana2InputSchema,
96
+ typeof nanoBanana2OutputSchema
97
+ > = {
98
+ input: nanoBanana2InputSchema,
99
+ output: nanoBanana2OutputSchema,
100
+ };
101
+
102
+ export const definition: ModelDefinition<typeof schema> = {
103
+ type: "model",
104
+ name: "nano-banana-2",
105
+ description:
106
+ "Google Nano Banana 2 - next-gen image editing model. Requires image_urls for all operations.",
107
+ providers: ["fal"],
108
+ defaultProvider: "fal",
109
+ providerModels: {
110
+ fal: "fal-ai/nano-banana-2/edit",
111
+ },
112
+ schema,
113
+ };
114
+
115
+ export default definition;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Qwen Image 2 generation and editing model
3
+ * Next-generation unified generation-and-editing model from Alibaba
4
+ * Supports both text-to-image and image-to-image editing
5
+ * Available in standard and pro tiers
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import type { ModelDefinition, ZodSchema } from "../../core/schema/types";
10
+
11
+ // Image size can be an enum string or an object with width/height
12
+ const qwenImage2ImageSizeSchema = z.union([
13
+ z.enum([
14
+ "square_hd",
15
+ "square",
16
+ "landscape_4_3",
17
+ "landscape_16_9",
18
+ "portrait_4_3",
19
+ "portrait_16_9",
20
+ ]),
21
+ z.object({
22
+ width: z.number().int().min(512).max(2048),
23
+ height: z.number().int().min(512).max(2048),
24
+ }),
25
+ ]);
26
+
27
+ // Output format options
28
+ const qwenImage2OutputFormatSchema = z.enum(["png", "jpeg", "webp"]);
29
+
30
+ // Input schema with Zod
31
+ const qwenImage2InputSchema = z.object({
32
+ prompt: z
33
+ .string()
34
+ .describe(
35
+ "Text description for generation or editing. Supports Chinese and English.",
36
+ ),
37
+ negative_prompt: z
38
+ .string()
39
+ .default("")
40
+ .describe("Content to avoid in the generated image. Max 500 characters."),
41
+ image_size: qwenImage2ImageSizeSchema
42
+ .optional()
43
+ .describe(
44
+ "Output image size. Can be an enum (e.g. 'square_hd') or {width, height} object. Pixels must be between 512x512 and 2048x2048.",
45
+ ),
46
+ image_urls: z
47
+ .array(z.string().url())
48
+ .optional()
49
+ .describe(
50
+ "Reference images for editing (1-6 images). Order matters: reference as 'image 1', 'image 2' in prompt. Required for /edit endpoints.",
51
+ ),
52
+ enable_prompt_expansion: z
53
+ .boolean()
54
+ .default(true)
55
+ .describe("Enable LLM prompt optimization for better results"),
56
+ seed: z
57
+ .number()
58
+ .int()
59
+ .min(0)
60
+ .max(2147483647)
61
+ .optional()
62
+ .describe("Random seed for reproducibility"),
63
+ enable_safety_checker: z
64
+ .boolean()
65
+ .default(true)
66
+ .describe("Enable content moderation for input and output"),
67
+ num_images: z
68
+ .number()
69
+ .int()
70
+ .min(1)
71
+ .max(6)
72
+ .default(1)
73
+ .describe("Number of images to generate (1-4 for t2i, 1-6 for edit)"),
74
+ output_format: qwenImage2OutputFormatSchema
75
+ .default("png")
76
+ .describe("Output image format"),
77
+ });
78
+
79
+ // Output schema with Zod
80
+ const qwenImage2OutputSchema = z.object({
81
+ images: z.array(
82
+ z.object({
83
+ url: z.string(),
84
+ file_name: z.string().optional(),
85
+ content_type: z.string().optional(),
86
+ }),
87
+ ),
88
+ seed: z.number().int().optional(),
89
+ });
90
+
91
+ // Schema object for the definition
92
+ const schema: ZodSchema<
93
+ typeof qwenImage2InputSchema,
94
+ typeof qwenImage2OutputSchema
95
+ > = {
96
+ input: qwenImage2InputSchema,
97
+ output: qwenImage2OutputSchema,
98
+ };
99
+
100
+ export const definition: ModelDefinition<typeof schema> = {
101
+ type: "model",
102
+ name: "qwen-image-2",
103
+ description:
104
+ "Qwen Image 2.0 - next-gen unified generation-and-editing model. Supports text-to-image and image-to-image editing in standard and pro tiers.",
105
+ providers: ["fal"],
106
+ defaultProvider: "fal",
107
+ providerModels: {
108
+ fal: "fal-ai/qwen-image-2/text-to-image",
109
+ },
110
+ schema,
111
+ };
112
+
113
+ export default definition;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Recraft V4 Pro image generation model
3
+ * Built for brand systems and production-ready workflows
4
+ * Text-to-image only
5
+ */
6
+
7
+ import { z } from "zod";
8
+ import type { ModelDefinition, ZodSchema } from "../../core/schema/types";
9
+
10
+ // Image size can be an enum string or an object with width/height
11
+ const recraftV4ImageSizeSchema = z.union([
12
+ z.enum([
13
+ "square_hd",
14
+ "square",
15
+ "landscape_4_3",
16
+ "landscape_16_9",
17
+ "portrait_4_3",
18
+ "portrait_16_9",
19
+ ]),
20
+ z.object({
21
+ width: z.number().int(),
22
+ height: z.number().int(),
23
+ }),
24
+ ]);
25
+
26
+ // RGB color schema
27
+ const rgbColorSchema = z.object({
28
+ r: z.number().int().min(0).max(255),
29
+ g: z.number().int().min(0).max(255),
30
+ b: z.number().int().min(0).max(255),
31
+ });
32
+
33
+ // Output format - Recraft V4 outputs webp by default
34
+ const recraftV4OutputFormatSchema = z.enum(["png", "jpeg", "webp"]);
35
+
36
+ // Input schema with Zod
37
+ const recraftV4InputSchema = z.object({
38
+ prompt: z.string().describe("Text description for image generation"),
39
+ image_size: recraftV4ImageSizeSchema
40
+ .default("square_hd")
41
+ .describe(
42
+ "Output image size. Can be an enum (e.g. 'landscape_16_9') or {width, height} object.",
43
+ ),
44
+ colors: z
45
+ .array(rgbColorSchema)
46
+ .default([])
47
+ .describe("Array of preferable RGB colors for the generated image"),
48
+ background_color: rgbColorSchema
49
+ .optional()
50
+ .describe("Preferable background color of the generated image"),
51
+ enable_safety_checker: z
52
+ .boolean()
53
+ .default(true)
54
+ .describe("Enable content safety checker"),
55
+ output_format: recraftV4OutputFormatSchema
56
+ .optional()
57
+ .describe("Output image format"),
58
+ });
59
+
60
+ // Output schema with Zod
61
+ const recraftV4OutputSchema = z.object({
62
+ images: z.array(
63
+ z.object({
64
+ url: z.string(),
65
+ file_name: z.string().optional(),
66
+ file_size: z.number().optional(),
67
+ content_type: z.string().optional(),
68
+ }),
69
+ ),
70
+ });
71
+
72
+ // Schema object for the definition
73
+ const schema: ZodSchema<
74
+ typeof recraftV4InputSchema,
75
+ typeof recraftV4OutputSchema
76
+ > = {
77
+ input: recraftV4InputSchema,
78
+ output: recraftV4OutputSchema,
79
+ };
80
+
81
+ export const definition: ModelDefinition<typeof schema> = {
82
+ type: "model",
83
+ name: "recraft-v4-pro",
84
+ description:
85
+ "Recraft V4 Pro - professional text-to-image model built for brand systems and production-ready workflows. Strong composition, refined lighting, realistic materials.",
86
+ providers: ["fal"],
87
+ defaultProvider: "fal",
88
+ providerModels: {
89
+ fal: "fal-ai/recraft/v4/pro/text-to-image",
90
+ },
91
+ schema,
92
+ };
93
+
94
+ export default definition;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Reve image editing model
3
+ * Upload an existing image and transform it via a text prompt
4
+ * Edit-only model using singular image_url (not image_urls array)
5
+ */
6
+
7
+ import { z } from "zod";
8
+ import type { ModelDefinition, ZodSchema } from "../../core/schema/types";
9
+
10
+ // Output format options
11
+ const reveOutputFormatSchema = z.enum(["png", "jpeg", "webp"]);
12
+
13
+ // Input schema with Zod
14
+ const reveInputSchema = z.object({
15
+ prompt: z
16
+ .string()
17
+ .describe("Text description of how to edit the provided image"),
18
+ image_url: z
19
+ .string()
20
+ .url()
21
+ .describe(
22
+ "URL of the reference image to edit. Supports PNG, JPEG, WebP, AVIF, and HEIF formats.",
23
+ ),
24
+ num_images: z
25
+ .number()
26
+ .int()
27
+ .min(1)
28
+ .max(4)
29
+ .default(1)
30
+ .describe("Number of images to generate (1-4)"),
31
+ output_format: reveOutputFormatSchema
32
+ .default("png")
33
+ .describe("Output image format"),
34
+ });
35
+
36
+ // Output schema with Zod
37
+ const reveOutputSchema = z.object({
38
+ images: z.array(
39
+ z.object({
40
+ url: z.string(),
41
+ file_name: z.string().optional(),
42
+ content_type: z.string().optional(),
43
+ }),
44
+ ),
45
+ });
46
+
47
+ // Schema object for the definition
48
+ const schema: ZodSchema<typeof reveInputSchema, typeof reveOutputSchema> = {
49
+ input: reveInputSchema,
50
+ output: reveOutputSchema,
51
+ };
52
+
53
+ export const definition: ModelDefinition<typeof schema> = {
54
+ type: "model",
55
+ name: "reve",
56
+ description:
57
+ "Reve edit model - upload an existing image and transform it via a text prompt. Uses singular image_url input.",
58
+ providers: ["fal"],
59
+ defaultProvider: "fal",
60
+ providerModels: {
61
+ fal: "fal-ai/reve/edit",
62
+ },
63
+ schema,
64
+ };
65
+
66
+ export default definition;
@@ -54,6 +54,23 @@ export class FalProvider extends BaseProvider {
54
54
  return "fal-ai/nano-banana-pro/edit";
55
55
  }
56
56
  }
57
+ // Nano Banana 2: always route to /edit endpoint (edit-only model)
58
+ if (model === "fal-ai/nano-banana-2") {
59
+ return "fal-ai/nano-banana-2/edit";
60
+ }
61
+ // Qwen Image 2: route to /edit endpoint when image_urls are provided
62
+ if (model === "fal-ai/qwen-image-2/text-to-image") {
63
+ const imageUrls = inputs.image_urls as string[] | undefined;
64
+ if (imageUrls && imageUrls.length > 0) {
65
+ return "fal-ai/qwen-image-2/edit";
66
+ }
67
+ }
68
+ if (model === "fal-ai/qwen-image-2/pro/text-to-image") {
69
+ const imageUrls = inputs.image_urls as string[] | undefined;
70
+ if (imageUrls && imageUrls.length > 0) {
71
+ return "fal-ai/qwen-image-2/pro/edit";
72
+ }
73
+ }
57
74
  return model;
58
75
  }
59
76
 
@@ -11,11 +11,13 @@ import type {
11
11
  PositionObject,
12
12
  SizeValue,
13
13
  TitleLayer,
14
+ VideoLayer,
14
15
  } from "../../ai-sdk/providers/editly/types";
15
16
  import type { PackshotProps, VargElement } from "../types";
16
17
  import type { RenderContext } from "./context";
17
18
  import { renderImage } from "./image";
18
19
  import { createBlinkingButton } from "./packshot/blinking-button";
20
+ import { renderVideo } from "./video";
19
21
 
20
22
  /**
21
23
  * Resolve an FFmpegOutput to a string path/URL via the backend.
@@ -118,8 +120,23 @@ export async function renderPackshot(
118
120
  type: "fill-color" as const,
119
121
  color: props.background,
120
122
  });
123
+ } else if (props.background.type === "video") {
124
+ const bgFile = await renderVideo(
125
+ props.background as VargElement<"video">,
126
+ ctx,
127
+ );
128
+ const bgPath = await ctx.backend.resolvePath(bgFile);
129
+ const videoLayer: VideoLayer = {
130
+ type: "video",
131
+ path: bgPath,
132
+ resizeMode: "cover",
133
+ };
134
+ layers.push(videoLayer);
121
135
  } else {
122
- const bgFile = await renderImage(props.background, ctx);
136
+ const bgFile = await renderImage(
137
+ props.background as VargElement<"image">,
138
+ ctx,
139
+ );
123
140
  const bgPath = await ctx.backend.resolvePath(bgFile);
124
141
  layers.push({
125
142
  type: "image" as const,
@@ -1,5 +1,6 @@
1
1
  import type { ImageModelV3 } from "@ai-sdk/provider";
2
2
  import { generateImage, wrapImageModel } from "ai";
3
+ import pMap from "p-map";
3
4
  import { type CacheStorage, withCache } from "../../ai-sdk/cache";
4
5
  import type { File, File as VargFile } from "../../ai-sdk/file";
5
6
  import { fileCache } from "../../ai-sdk/file-cache";
@@ -9,7 +10,6 @@ import {
9
10
  placeholderFallbackMiddleware,
10
11
  wrapVideoModel,
11
12
  } from "../../ai-sdk/middleware";
12
-
13
13
  import { editly, localBackend } from "../../ai-sdk/providers/editly";
14
14
  import type {
15
15
  AudioTrack,
@@ -236,15 +236,42 @@ export async function renderRoot(
236
236
  }
237
237
  }
238
238
 
239
- const clipResults = await Promise.allSettled(
240
- clipElements.map((clipElement) => renderClip(clipElement, ctx)),
239
+ const concurrency =
240
+ options.concurrency === undefined
241
+ ? Number.POSITIVE_INFINITY
242
+ : options.concurrency;
243
+
244
+ if (
245
+ concurrency !== Number.POSITIVE_INFINITY &&
246
+ (!Number.isInteger(concurrency) || concurrency < 1)
247
+ ) {
248
+ throw new Error("render option `concurrency` must be a positive integer");
249
+ }
250
+
251
+ const clipResults = await pMap(
252
+ clipElements,
253
+ async (clipElement, i) => {
254
+ try {
255
+ return {
256
+ status: "fulfilled" as const,
257
+ value: await renderClip(clipElement, ctx),
258
+ index: i,
259
+ };
260
+ } catch (reason) {
261
+ return {
262
+ status: "rejected" as const,
263
+ reason: reason as Error,
264
+ index: i,
265
+ };
266
+ }
267
+ },
268
+ { concurrency },
241
269
  );
242
270
 
243
- const failures = clipResults
244
- .map((r, i) =>
245
- r.status === "rejected" ? { index: i, reason: r.reason } : null,
246
- )
247
- .filter(Boolean) as { index: number; reason: Error }[];
271
+ const failures = clipResults.filter(
272
+ (r): r is Extract<typeof r, { status: "rejected" }> =>
273
+ r.status === "rejected",
274
+ );
248
275
 
249
276
  if (failures.length > 0) {
250
277
  const successCount = clipResults.length - failures.length;
@@ -266,11 +293,10 @@ export async function renderRoot(
266
293
  );
267
294
  }
268
295
 
269
- const renderedClips = clipResults.map(
270
- (r) =>
271
- (r as PromiseFulfilledResult<Awaited<ReturnType<typeof renderClip>>>)
272
- .value,
273
- );
296
+ const renderedClips = clipResults.map((r) => {
297
+ if (r.status !== "fulfilled") throw new Error("unexpected");
298
+ return r.value;
299
+ });
274
300
 
275
301
  const clips: Clip[] = [];
276
302
  let currentTime = 0;
@@ -209,7 +209,16 @@ export interface SwipeProps extends BaseProps {
209
209
  }
210
210
 
211
211
  export interface PackshotProps extends BaseProps {
212
- background?: VargElement<"image"> | string;
212
+ /**
213
+ * Packshot background.
214
+ *
215
+ * - `string` — treated as a solid fill color (e.g. `"#000000"`).
216
+ * - `VargElement<"image">` — a generated or static image, rendered and
217
+ * used as a full-bleed cover background.
218
+ * - `VargElement<"video">` — a generated or static video, rendered and
219
+ * used as a looping full-bleed cover background.
220
+ */
221
+ background?: VargElement<"image"> | VargElement<"video"> | string;
213
222
  logo?: string;
214
223
  /**
215
224
  * Logo position on screen.
@@ -276,6 +285,8 @@ export interface RenderOptions {
276
285
  defaults?: DefaultModels;
277
286
  backend?: FFmpegBackend;
278
287
  storage?: StorageProvider;
288
+ /** Max concurrent clip renders. Defaults to unlimited. */
289
+ concurrency?: number;
279
290
  }
280
291
 
281
292
  // Re-export from file module for convenience