@ai-sdk/fal 2.0.9 → 2.0.11
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/CHANGELOG.md +12 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/docs/10-fal.mdx +320 -0
- package/package.json +8 -3
- package/src/fal-api-types.ts +189 -0
- package/src/fal-config.ts +9 -0
- package/src/fal-error.test.ts +34 -0
- package/src/fal-error.ts +16 -0
- package/src/fal-image-model.test.ts +930 -0
- package/src/fal-image-model.ts +367 -0
- package/src/fal-image-options.ts +129 -0
- package/src/fal-image-settings.ts +71 -0
- package/src/fal-provider.test.ts +57 -0
- package/src/fal-provider.ts +183 -0
- package/src/fal-speech-model.test.ts +128 -0
- package/src/fal-speech-model.ts +156 -0
- package/src/fal-speech-settings.ts +10 -0
- package/src/fal-transcription-model.test.ts +181 -0
- package/src/fal-transcription-model.ts +270 -0
- package/src/fal-transcription-options.ts +1 -0
- package/src/index.ts +4 -0
- package/src/transcript-test.mp3 +0 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { ImageModelV3, SharedV3Warning } from '@ai-sdk/provider';
|
|
2
|
+
import type { Resolvable } from '@ai-sdk/provider-utils';
|
|
3
|
+
import {
|
|
4
|
+
combineHeaders,
|
|
5
|
+
convertImageModelFileToDataUri,
|
|
6
|
+
createBinaryResponseHandler,
|
|
7
|
+
createJsonErrorResponseHandler,
|
|
8
|
+
createJsonResponseHandler,
|
|
9
|
+
createStatusCodeErrorResponseHandler,
|
|
10
|
+
FetchFunction,
|
|
11
|
+
getFromApi,
|
|
12
|
+
parseProviderOptions,
|
|
13
|
+
postJsonToApi,
|
|
14
|
+
resolve,
|
|
15
|
+
} from '@ai-sdk/provider-utils';
|
|
16
|
+
import { z } from 'zod/v4';
|
|
17
|
+
import { FalImageModelId, FalImageSize } from './fal-image-settings';
|
|
18
|
+
import { falImageProviderOptionsSchema } from './fal-image-options';
|
|
19
|
+
|
|
20
|
+
interface FalImageModelConfig {
|
|
21
|
+
provider: string;
|
|
22
|
+
baseURL: string;
|
|
23
|
+
headers?: Resolvable<Record<string, string | undefined>>;
|
|
24
|
+
fetch?: FetchFunction;
|
|
25
|
+
_internal?: {
|
|
26
|
+
currentDate?: () => Date;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class FalImageModel implements ImageModelV3 {
|
|
31
|
+
readonly specificationVersion = 'v3';
|
|
32
|
+
readonly maxImagesPerCall = 1;
|
|
33
|
+
|
|
34
|
+
get provider(): string {
|
|
35
|
+
return this.config.provider;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
readonly modelId: FalImageModelId,
|
|
40
|
+
private readonly config: FalImageModelConfig,
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
private async getArgs({
|
|
44
|
+
prompt,
|
|
45
|
+
n,
|
|
46
|
+
size,
|
|
47
|
+
aspectRatio,
|
|
48
|
+
seed,
|
|
49
|
+
providerOptions,
|
|
50
|
+
files,
|
|
51
|
+
mask,
|
|
52
|
+
}: Parameters<ImageModelV3['doGenerate']>[0]) {
|
|
53
|
+
const warnings: Array<SharedV3Warning> = [];
|
|
54
|
+
|
|
55
|
+
let imageSize: FalImageSize | undefined;
|
|
56
|
+
if (size) {
|
|
57
|
+
const [width, height] = size.split('x').map(Number);
|
|
58
|
+
imageSize = { width, height };
|
|
59
|
+
} else if (aspectRatio) {
|
|
60
|
+
imageSize = convertAspectRatioToSize(aspectRatio);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const falOptions = await parseProviderOptions({
|
|
64
|
+
provider: 'fal',
|
|
65
|
+
providerOptions,
|
|
66
|
+
schema: falImageProviderOptionsSchema,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const requestBody: Record<string, unknown> = {
|
|
70
|
+
prompt,
|
|
71
|
+
seed,
|
|
72
|
+
image_size: imageSize,
|
|
73
|
+
num_images: n,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Handle image editing: convert files to image_url or image_urls
|
|
77
|
+
if (files != null && files.length > 0) {
|
|
78
|
+
const useMultipleImages = falOptions?.useMultipleImages === true;
|
|
79
|
+
|
|
80
|
+
if (useMultipleImages) {
|
|
81
|
+
// Use image_urls array for models that support multiple images (e.g., flux-2/edit)
|
|
82
|
+
requestBody.image_urls = files.map(file =>
|
|
83
|
+
convertImageModelFileToDataUri(file),
|
|
84
|
+
);
|
|
85
|
+
} else {
|
|
86
|
+
// Use single image_url for standard image editing models
|
|
87
|
+
requestBody.image_url = convertImageModelFileToDataUri(files[0]);
|
|
88
|
+
|
|
89
|
+
if (files.length > 1) {
|
|
90
|
+
warnings.push({
|
|
91
|
+
type: 'other',
|
|
92
|
+
message:
|
|
93
|
+
'Multiple input images provided but useMultipleImages is not enabled. ' +
|
|
94
|
+
'Only the first image will be used. Set providerOptions.fal.useMultipleImages ' +
|
|
95
|
+
'to true for models that support multiple images (e.g., fal-ai/flux-2/edit).',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle mask for inpainting
|
|
102
|
+
if (mask != null) {
|
|
103
|
+
requestBody.mask_url = convertImageModelFileToDataUri(mask);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (falOptions) {
|
|
107
|
+
const deprecatedKeys =
|
|
108
|
+
'__deprecatedKeys' in falOptions
|
|
109
|
+
? (falOptions.__deprecatedKeys as string[])
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
if (deprecatedKeys && deprecatedKeys.length > 0) {
|
|
113
|
+
warnings.push({
|
|
114
|
+
type: 'other',
|
|
115
|
+
message: `The following provider options use deprecated snake_case and will be removed in @ai-sdk/fal v2.0. Please use camelCase instead: ${deprecatedKeys
|
|
116
|
+
.map(key => {
|
|
117
|
+
const camelCase = key.replace(/_([a-z])/g, (_, letter) =>
|
|
118
|
+
letter.toUpperCase(),
|
|
119
|
+
);
|
|
120
|
+
return `'${key}' (use '${camelCase}')`;
|
|
121
|
+
})
|
|
122
|
+
.join(', ')}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fieldMapping: Record<string, string> = {
|
|
127
|
+
imageUrl: 'image_url',
|
|
128
|
+
maskUrl: 'mask_url',
|
|
129
|
+
guidanceScale: 'guidance_scale',
|
|
130
|
+
numInferenceSteps: 'num_inference_steps',
|
|
131
|
+
enableSafetyChecker: 'enable_safety_checker',
|
|
132
|
+
outputFormat: 'output_format',
|
|
133
|
+
syncMode: 'sync_mode',
|
|
134
|
+
safetyTolerance: 'safety_tolerance',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
for (const [key, value] of Object.entries(falOptions)) {
|
|
138
|
+
if (key === '__deprecatedKeys') continue;
|
|
139
|
+
if (key === 'useMultipleImages') continue; // Don't send to API
|
|
140
|
+
const apiKey = fieldMapping[key] ?? key;
|
|
141
|
+
|
|
142
|
+
if (value !== undefined) {
|
|
143
|
+
requestBody[apiKey] = value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { requestBody, warnings } as const;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async doGenerate(
|
|
152
|
+
options: Parameters<ImageModelV3['doGenerate']>[0],
|
|
153
|
+
): Promise<Awaited<ReturnType<ImageModelV3['doGenerate']>>> {
|
|
154
|
+
const { requestBody, warnings } = await this.getArgs(options);
|
|
155
|
+
|
|
156
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
157
|
+
const { value, responseHeaders } = await postJsonToApi({
|
|
158
|
+
url: `${this.config.baseURL}/${this.modelId}`,
|
|
159
|
+
headers: combineHeaders(
|
|
160
|
+
await resolve(this.config.headers),
|
|
161
|
+
options.headers,
|
|
162
|
+
),
|
|
163
|
+
body: requestBody,
|
|
164
|
+
failedResponseHandler: falFailedResponseHandler,
|
|
165
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
166
|
+
falImageResponseSchema,
|
|
167
|
+
),
|
|
168
|
+
abortSignal: options.abortSignal,
|
|
169
|
+
fetch: this.config.fetch,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const {
|
|
173
|
+
images: targetImages,
|
|
174
|
+
// prompt is just passed through and not a revised prompt per image
|
|
175
|
+
prompt: _prompt,
|
|
176
|
+
// NSFW information is normalized merged into `providerMetadata.fal.images`
|
|
177
|
+
has_nsfw_concepts,
|
|
178
|
+
nsfw_content_detected,
|
|
179
|
+
// pass through other properties to providerMetadata
|
|
180
|
+
...responseMetaData
|
|
181
|
+
} = value;
|
|
182
|
+
|
|
183
|
+
// download the images:
|
|
184
|
+
const downloadedImages = await Promise.all(
|
|
185
|
+
targetImages.map(image =>
|
|
186
|
+
this.downloadImage(image.url, options.abortSignal),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
images: downloadedImages,
|
|
192
|
+
warnings,
|
|
193
|
+
response: {
|
|
194
|
+
modelId: this.modelId,
|
|
195
|
+
timestamp: currentDate,
|
|
196
|
+
headers: responseHeaders,
|
|
197
|
+
},
|
|
198
|
+
providerMetadata: {
|
|
199
|
+
fal: {
|
|
200
|
+
images: targetImages.map((image, index) => {
|
|
201
|
+
const {
|
|
202
|
+
url,
|
|
203
|
+
content_type: contentType,
|
|
204
|
+
file_name: fileName,
|
|
205
|
+
file_data: fileData,
|
|
206
|
+
file_size: fileSize,
|
|
207
|
+
...imageMetaData
|
|
208
|
+
} = image;
|
|
209
|
+
|
|
210
|
+
const nsfw =
|
|
211
|
+
has_nsfw_concepts?.[index] ?? nsfw_content_detected?.[index];
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
...imageMetaData,
|
|
215
|
+
...removeOnlyUndefined({
|
|
216
|
+
contentType,
|
|
217
|
+
fileName,
|
|
218
|
+
fileData,
|
|
219
|
+
fileSize,
|
|
220
|
+
nsfw,
|
|
221
|
+
}),
|
|
222
|
+
};
|
|
223
|
+
}),
|
|
224
|
+
...responseMetaData,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async downloadImage(
|
|
231
|
+
url: string,
|
|
232
|
+
abortSignal: AbortSignal | undefined,
|
|
233
|
+
): Promise<Uint8Array> {
|
|
234
|
+
const { value: response } = await getFromApi({
|
|
235
|
+
url,
|
|
236
|
+
// No specific headers should be needed for this request as it's a
|
|
237
|
+
// generated image provided by fal.ai.
|
|
238
|
+
abortSignal,
|
|
239
|
+
failedResponseHandler: createStatusCodeErrorResponseHandler(),
|
|
240
|
+
successfulResponseHandler: createBinaryResponseHandler(),
|
|
241
|
+
fetch: this.config.fetch,
|
|
242
|
+
});
|
|
243
|
+
return response;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function removeOnlyUndefined<T extends Record<string, unknown>>(obj: T) {
|
|
248
|
+
return Object.fromEntries(
|
|
249
|
+
Object.entries(obj).filter(([, v]) => v !== undefined),
|
|
250
|
+
) as Partial<T>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
Converts an aspect ratio to an image size compatible with fal.ai APIs.
|
|
255
|
+
@param aspectRatio - The aspect ratio to convert.
|
|
256
|
+
@returns The image size.
|
|
257
|
+
*/
|
|
258
|
+
function convertAspectRatioToSize(
|
|
259
|
+
aspectRatio: `${number}:${number}`,
|
|
260
|
+
): FalImageSize | undefined {
|
|
261
|
+
switch (aspectRatio) {
|
|
262
|
+
case '1:1':
|
|
263
|
+
return 'square_hd';
|
|
264
|
+
case '16:9':
|
|
265
|
+
return 'landscape_16_9';
|
|
266
|
+
case '9:16':
|
|
267
|
+
return 'portrait_16_9';
|
|
268
|
+
case '4:3':
|
|
269
|
+
return 'landscape_4_3';
|
|
270
|
+
case '3:4':
|
|
271
|
+
return 'portrait_4_3';
|
|
272
|
+
case '16:10':
|
|
273
|
+
return { width: 1280, height: 800 };
|
|
274
|
+
case '10:16':
|
|
275
|
+
return { width: 800, height: 1280 };
|
|
276
|
+
case '21:9':
|
|
277
|
+
return { width: 2560, height: 1080 };
|
|
278
|
+
case '9:21':
|
|
279
|
+
return { width: 1080, height: 2560 };
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Validation error has a particular payload to inform the exact property that is invalid
|
|
285
|
+
const falValidationErrorSchema = z.object({
|
|
286
|
+
detail: z.array(
|
|
287
|
+
z.object({
|
|
288
|
+
loc: z.array(z.string()),
|
|
289
|
+
msg: z.string(),
|
|
290
|
+
type: z.string(),
|
|
291
|
+
}),
|
|
292
|
+
),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
type ValidationError = z.infer<typeof falValidationErrorSchema>;
|
|
296
|
+
|
|
297
|
+
// Other errors have a message property
|
|
298
|
+
const falHttpErrorSchema = z.object({
|
|
299
|
+
message: z.string(),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const falErrorSchema = z.union([falValidationErrorSchema, falHttpErrorSchema]);
|
|
303
|
+
|
|
304
|
+
const falImageSchema = z.object({
|
|
305
|
+
url: z.string(),
|
|
306
|
+
width: z.number().nullish(),
|
|
307
|
+
height: z.number().nullish(),
|
|
308
|
+
// e.g. https://fal.ai/models/fal-ai/fashn/tryon/v1.6/api#schema-output
|
|
309
|
+
content_type: z.string().nullish(),
|
|
310
|
+
// e.g. https://fal.ai/models/fal-ai/flowedit/api#schema-output
|
|
311
|
+
file_name: z.string().nullish(),
|
|
312
|
+
file_data: z.string().optional(),
|
|
313
|
+
file_size: z.number().nullish(),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// https://fal.ai/models/fal-ai/lora/api#type-File
|
|
317
|
+
const loraFileSchema = z.object({
|
|
318
|
+
url: z.string(),
|
|
319
|
+
content_type: z.string().optional(),
|
|
320
|
+
file_name: z.string().nullable().optional(),
|
|
321
|
+
file_data: z.string().optional(),
|
|
322
|
+
file_size: z.number().nullable().optional(),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const commonResponseSchema = z.object({
|
|
326
|
+
timings: z
|
|
327
|
+
.object({
|
|
328
|
+
inference: z.number().optional(),
|
|
329
|
+
})
|
|
330
|
+
.optional(),
|
|
331
|
+
seed: z.number().optional(),
|
|
332
|
+
has_nsfw_concepts: z.array(z.boolean()).optional(),
|
|
333
|
+
prompt: z.string().optional(),
|
|
334
|
+
// https://fal.ai/models/fal-ai/lcm/api#schema-output
|
|
335
|
+
nsfw_content_detected: z.array(z.boolean()).optional(),
|
|
336
|
+
num_inference_steps: z.number().optional(),
|
|
337
|
+
// https://fal.ai/models/fal-ai/lora/api#schema-output
|
|
338
|
+
debug_latents: loraFileSchema.optional(),
|
|
339
|
+
debug_per_pass_latents: loraFileSchema.optional(),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Most FAL image models respond with an array of images, but some have a response
|
|
343
|
+
// with a single image, e.g. https://fal.ai/models/easel-ai/easel-avatar/api#schema-output
|
|
344
|
+
const base = z.looseObject(commonResponseSchema.shape);
|
|
345
|
+
const falImageResponseSchema = z
|
|
346
|
+
.union([
|
|
347
|
+
base.extend({ images: z.array(falImageSchema) }),
|
|
348
|
+
base.extend({ image: falImageSchema }),
|
|
349
|
+
])
|
|
350
|
+
.transform(v => ('images' in v ? v : { ...v, images: [v.image] }))
|
|
351
|
+
.pipe(base.extend({ images: z.array(falImageSchema) }));
|
|
352
|
+
|
|
353
|
+
function isValidationError(error: unknown): error is ValidationError {
|
|
354
|
+
return falValidationErrorSchema.safeParse(error).success;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const falFailedResponseHandler = createJsonErrorResponseHandler({
|
|
358
|
+
errorSchema: falErrorSchema,
|
|
359
|
+
errorToMessage: error => {
|
|
360
|
+
if (isValidationError(error)) {
|
|
361
|
+
return error.detail
|
|
362
|
+
.map(detail => `${detail.loc.join('.')}: ${detail.msg}`)
|
|
363
|
+
.join('\n');
|
|
364
|
+
}
|
|
365
|
+
return error.message ?? 'Unknown fal error';
|
|
366
|
+
},
|
|
367
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { InferSchema, lazySchema, zodSchema } from '@ai-sdk/provider-utils';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
|
|
4
|
+
export const falImageProviderOptionsSchema = lazySchema(() =>
|
|
5
|
+
zodSchema(
|
|
6
|
+
z
|
|
7
|
+
.object({
|
|
8
|
+
/** @deprecated use prompt.images instead */
|
|
9
|
+
imageUrl: z.string().nullish().meta({
|
|
10
|
+
deprecated: true,
|
|
11
|
+
description: 'Use `prompt.images` instead',
|
|
12
|
+
}),
|
|
13
|
+
maskUrl: z
|
|
14
|
+
.string()
|
|
15
|
+
.nullish()
|
|
16
|
+
.meta({ deprecated: true, description: 'Use `prompt.mask` instead' }),
|
|
17
|
+
guidanceScale: z.number().min(1).max(20).nullish(),
|
|
18
|
+
numInferenceSteps: z.number().min(1).max(50).nullish(),
|
|
19
|
+
enableSafetyChecker: z.boolean().nullish(),
|
|
20
|
+
outputFormat: z.enum(['jpeg', 'png']).nullish(),
|
|
21
|
+
syncMode: z.boolean().nullish(),
|
|
22
|
+
strength: z.number().nullish(),
|
|
23
|
+
acceleration: z.enum(['none', 'regular', 'high']).nullish(),
|
|
24
|
+
safetyTolerance: z
|
|
25
|
+
.enum(['1', '2', '3', '4', '5', '6'])
|
|
26
|
+
.or(z.number().min(1).max(6))
|
|
27
|
+
.nullish(),
|
|
28
|
+
/**
|
|
29
|
+
* When true, converts multiple input images to `image_urls` array instead of `image_url` string.
|
|
30
|
+
*/
|
|
31
|
+
useMultipleImages: z.boolean().nullish(),
|
|
32
|
+
|
|
33
|
+
// Deprecated snake_case versions
|
|
34
|
+
image_url: z.string().nullish(),
|
|
35
|
+
mask_url: z.string().nullish(),
|
|
36
|
+
guidance_scale: z.number().min(1).max(20).nullish(),
|
|
37
|
+
num_inference_steps: z.number().min(1).max(50).nullish(),
|
|
38
|
+
enable_safety_checker: z.boolean().nullish(),
|
|
39
|
+
output_format: z.enum(['jpeg', 'png']).nullish(),
|
|
40
|
+
sync_mode: z.boolean().nullish(),
|
|
41
|
+
safety_tolerance: z
|
|
42
|
+
.enum(['1', '2', '3', '4', '5', '6'])
|
|
43
|
+
.or(z.number().min(1).max(6))
|
|
44
|
+
.nullish(),
|
|
45
|
+
})
|
|
46
|
+
.passthrough()
|
|
47
|
+
.transform(data => {
|
|
48
|
+
const result: Record<string, unknown> = {};
|
|
49
|
+
const deprecatedKeys: string[] = [];
|
|
50
|
+
|
|
51
|
+
const mapKey = (snakeKey: string, camelKey: string) => {
|
|
52
|
+
const snakeValue = data[snakeKey as keyof typeof data];
|
|
53
|
+
const camelValue = data[camelKey as keyof typeof data];
|
|
54
|
+
|
|
55
|
+
// If snake_case is used, mark it as deprecated
|
|
56
|
+
if (snakeValue !== undefined && snakeValue !== null) {
|
|
57
|
+
deprecatedKeys.push(snakeKey);
|
|
58
|
+
result[camelKey] = snakeValue;
|
|
59
|
+
} else if (camelValue !== undefined && camelValue !== null) {
|
|
60
|
+
result[camelKey] = camelValue;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Map all known parameters
|
|
65
|
+
mapKey('image_url', 'imageUrl');
|
|
66
|
+
mapKey('mask_url', 'maskUrl');
|
|
67
|
+
mapKey('guidance_scale', 'guidanceScale');
|
|
68
|
+
mapKey('num_inference_steps', 'numInferenceSteps');
|
|
69
|
+
mapKey('enable_safety_checker', 'enableSafetyChecker');
|
|
70
|
+
mapKey('output_format', 'outputFormat');
|
|
71
|
+
mapKey('sync_mode', 'syncMode');
|
|
72
|
+
mapKey('safety_tolerance', 'safetyTolerance');
|
|
73
|
+
|
|
74
|
+
// These don't have snake_case equivalents
|
|
75
|
+
if (data.strength !== undefined && data.strength !== null) {
|
|
76
|
+
result.strength = data.strength;
|
|
77
|
+
}
|
|
78
|
+
if (data.acceleration !== undefined && data.acceleration !== null) {
|
|
79
|
+
result.acceleration = data.acceleration;
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
data.useMultipleImages !== undefined &&
|
|
83
|
+
data.useMultipleImages !== null
|
|
84
|
+
) {
|
|
85
|
+
result.useMultipleImages = data.useMultipleImages;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const [key, value] of Object.entries(data)) {
|
|
89
|
+
if (
|
|
90
|
+
![
|
|
91
|
+
// camelCase known keys
|
|
92
|
+
'imageUrl',
|
|
93
|
+
'maskUrl',
|
|
94
|
+
'guidanceScale',
|
|
95
|
+
'numInferenceSteps',
|
|
96
|
+
'enableSafetyChecker',
|
|
97
|
+
'outputFormat',
|
|
98
|
+
'syncMode',
|
|
99
|
+
'strength',
|
|
100
|
+
'acceleration',
|
|
101
|
+
'safetyTolerance',
|
|
102
|
+
'useMultipleImages',
|
|
103
|
+
// snake_case known keys
|
|
104
|
+
'image_url',
|
|
105
|
+
'mask_url',
|
|
106
|
+
'guidance_scale',
|
|
107
|
+
'num_inference_steps',
|
|
108
|
+
'enable_safety_checker',
|
|
109
|
+
'output_format',
|
|
110
|
+
'sync_mode',
|
|
111
|
+
'safety_tolerance',
|
|
112
|
+
].includes(key)
|
|
113
|
+
) {
|
|
114
|
+
result[key] = value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (deprecatedKeys.length > 0) {
|
|
119
|
+
(result as any).__deprecatedKeys = deprecatedKeys;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
export type FalImageProviderOptions = InferSchema<
|
|
128
|
+
typeof falImageProviderOptionsSchema
|
|
129
|
+
>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// https://fal.ai/explore/search?categories=image-to-image,text-to-image&q=newest
|
|
2
|
+
export type FalImageModelId =
|
|
3
|
+
| 'fal-ai/aura-sr'
|
|
4
|
+
| 'fal-ai/bria/background/remove'
|
|
5
|
+
| 'fal-ai/bria/eraser'
|
|
6
|
+
| 'fal-ai/bria/product-shot'
|
|
7
|
+
| 'fal-ai/bria/reimagine'
|
|
8
|
+
| 'bria/text-to-image/3.2'
|
|
9
|
+
| 'fal-ai/bria/text-to-image/base'
|
|
10
|
+
| 'fal-ai/bria/text-to-image/fast'
|
|
11
|
+
| 'fal-ai/bria/text-to-image/hd'
|
|
12
|
+
| 'fal-ai/bytedance/dreamina/v3.1/text-to-image'
|
|
13
|
+
| 'fal-ai/ccsr'
|
|
14
|
+
| 'fal-ai/clarity-upscaler'
|
|
15
|
+
| 'fal-ai/creative-upscaler'
|
|
16
|
+
| 'fal-ai/esrgan'
|
|
17
|
+
| 'fal-ai/flux-general'
|
|
18
|
+
| 'fal-ai/flux-general/differential-diffusion'
|
|
19
|
+
| 'fal-ai/flux-general/image-to-image'
|
|
20
|
+
| 'fal-ai/flux-general/inpainting'
|
|
21
|
+
| 'fal-ai/flux-general/rf-inversion'
|
|
22
|
+
| 'fal-ai/flux-kontext-lora/text-to-image'
|
|
23
|
+
| 'fal-ai/flux-lora'
|
|
24
|
+
| 'fal-ai/flux-lora/image-to-image'
|
|
25
|
+
| 'fal-ai/flux-lora/inpainting'
|
|
26
|
+
| 'fal-ai/flux-pro/kontext'
|
|
27
|
+
| 'fal-ai/flux-pro/kontext/max'
|
|
28
|
+
| 'fal-ai/flux-pro/v1.1'
|
|
29
|
+
| 'fal-ai/flux-pro/v1.1-ultra'
|
|
30
|
+
| 'fal-ai/flux-pro/v1.1-ultra-finetuned'
|
|
31
|
+
| 'fal-ai/flux-pro/v1.1-ultra/redux'
|
|
32
|
+
| 'fal-ai/flux-pro/v1.1/redux'
|
|
33
|
+
| 'fal-ai/flux/dev'
|
|
34
|
+
| 'fal-ai/flux/dev/image-to-image'
|
|
35
|
+
| 'fal-ai/flux/dev/redux'
|
|
36
|
+
| 'fal-ai/flux/krea'
|
|
37
|
+
| 'fal-ai/flux/krea/image-to-image'
|
|
38
|
+
| 'fal-ai/flux/krea/redux'
|
|
39
|
+
| 'fal-ai/flux/schnell'
|
|
40
|
+
| 'fal-ai/flux/schnell/redux'
|
|
41
|
+
| 'fal-ai/ideogram/character'
|
|
42
|
+
| 'fal-ai/ideogram/character/edit'
|
|
43
|
+
| 'fal-ai/ideogram/character/remix'
|
|
44
|
+
| 'fal-ai/imagen4/preview'
|
|
45
|
+
| 'fal-ai/luma-photon'
|
|
46
|
+
| 'fal-ai/luma-photon/flash'
|
|
47
|
+
| 'fal-ai/object-removal'
|
|
48
|
+
| 'fal-ai/omnigen-v2'
|
|
49
|
+
| 'fal-ai/qwen-image'
|
|
50
|
+
| 'fal-ai/recraft/v3/text-to-image'
|
|
51
|
+
| 'fal-ai/recraft/v3/image-to-image'
|
|
52
|
+
| 'fal-ai/sana/sprint'
|
|
53
|
+
| 'fal-ai/sana/v1.5/4.8b'
|
|
54
|
+
| 'fal-ai/sana/v1.5/1.6b'
|
|
55
|
+
| 'fal-ai/sky-raccoon'
|
|
56
|
+
| 'fal-ai/wan/v2.2-5b/text-to-image'
|
|
57
|
+
| 'fal-ai/wan/v2.2-a14b/text-to-image'
|
|
58
|
+
| 'fal-ai/fashn/tryon/v1.6'
|
|
59
|
+
| (string & {});
|
|
60
|
+
|
|
61
|
+
export type FalImageSize =
|
|
62
|
+
| 'square'
|
|
63
|
+
| 'square_hd'
|
|
64
|
+
| 'landscape_16_9'
|
|
65
|
+
| 'landscape_4_3'
|
|
66
|
+
| 'portrait_16_9'
|
|
67
|
+
| 'portrait_4_3'
|
|
68
|
+
| {
|
|
69
|
+
width: number;
|
|
70
|
+
height: number;
|
|
71
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { createFal } from './fal-provider';
|
|
3
|
+
import { FalImageModel } from './fal-image-model';
|
|
4
|
+
|
|
5
|
+
vi.mock('./fal-image-model', () => ({
|
|
6
|
+
FalImageModel: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('createFal', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('image', () => {
|
|
15
|
+
it('should construct an image model with default configuration', () => {
|
|
16
|
+
const provider = createFal();
|
|
17
|
+
const modelId = 'fal-ai/flux/dev';
|
|
18
|
+
|
|
19
|
+
const model = provider.image(modelId);
|
|
20
|
+
|
|
21
|
+
expect(model).toBeInstanceOf(FalImageModel);
|
|
22
|
+
expect(FalImageModel).toHaveBeenCalledWith(
|
|
23
|
+
modelId,
|
|
24
|
+
expect.objectContaining({
|
|
25
|
+
provider: 'fal.image',
|
|
26
|
+
baseURL: 'https://fal.run',
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should respect custom configuration options', () => {
|
|
32
|
+
const customBaseURL = 'https://custom.fal.run';
|
|
33
|
+
const customHeaders = { 'X-Custom-Header': 'value' };
|
|
34
|
+
const mockFetch = vi.fn();
|
|
35
|
+
|
|
36
|
+
const provider = createFal({
|
|
37
|
+
apiKey: 'custom-api-key',
|
|
38
|
+
baseURL: customBaseURL,
|
|
39
|
+
headers: customHeaders,
|
|
40
|
+
fetch: mockFetch,
|
|
41
|
+
});
|
|
42
|
+
const modelId = 'fal-ai/flux/dev';
|
|
43
|
+
|
|
44
|
+
provider.image(modelId);
|
|
45
|
+
|
|
46
|
+
expect(FalImageModel).toHaveBeenCalledWith(
|
|
47
|
+
modelId,
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
baseURL: customBaseURL,
|
|
50
|
+
headers: expect.any(Function),
|
|
51
|
+
fetch: mockFetch,
|
|
52
|
+
provider: 'fal.image',
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|