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 +1 -1
- package/src/ai-sdk/providers/fal.ts +26 -1
- package/src/definitions/models/index.ts +12 -0
- package/src/definitions/models/nano-banana-2.ts +115 -0
- package/src/definitions/models/qwen-image-2.ts +113 -0
- package/src/definitions/models/recraft-v4.ts +94 -0
- package/src/definitions/models/reve.ts +66 -0
- package/src/providers/fal.ts +17 -0
- package/src/react/renderers/packshot.ts +18 -1
- package/src/react/renderers/render.ts +39 -13
- package/src/react/types.ts +12 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/providers/fal.ts
CHANGED
|
@@ -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(
|
|
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
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
r.status === "rejected"
|
|
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
|
-
|
|
272
|
-
|
|
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;
|
package/src/react/types.ts
CHANGED
|
@@ -209,7 +209,16 @@ export interface SwipeProps extends BaseProps {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export interface PackshotProps extends BaseProps {
|
|
212
|
-
|
|
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
|