@ai-sdk/fal 2.0.16 → 2.0.18
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 +16 -0
- package/dist/index.d.mts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +252 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +259 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/fal-provider.ts +17 -0
- package/src/fal-video-model.ts +324 -0
- package/src/fal-video-settings.ts +8 -0
- package/src/index.ts +3 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AISDKError,
|
|
3
|
+
type Experimental_VideoModelV3,
|
|
4
|
+
type SharedV3Warning,
|
|
5
|
+
} from '@ai-sdk/provider';
|
|
6
|
+
import {
|
|
7
|
+
combineHeaders,
|
|
8
|
+
convertImageModelFileToDataUri,
|
|
9
|
+
createJsonErrorResponseHandler,
|
|
10
|
+
createJsonResponseHandler,
|
|
11
|
+
delay,
|
|
12
|
+
getFromApi,
|
|
13
|
+
lazySchema,
|
|
14
|
+
parseProviderOptions,
|
|
15
|
+
postJsonToApi,
|
|
16
|
+
zodSchema,
|
|
17
|
+
} from '@ai-sdk/provider-utils';
|
|
18
|
+
import { z } from 'zod/v4';
|
|
19
|
+
import type { FalConfig } from './fal-config';
|
|
20
|
+
import { falErrorDataSchema, falFailedResponseHandler } from './fal-error';
|
|
21
|
+
import type { FalVideoModelId } from './fal-video-settings';
|
|
22
|
+
|
|
23
|
+
export type FalVideoProviderOptions = {
|
|
24
|
+
loop?: boolean | null;
|
|
25
|
+
motionStrength?: number | null;
|
|
26
|
+
pollIntervalMs?: number | null;
|
|
27
|
+
pollTimeoutMs?: number | null;
|
|
28
|
+
resolution?: string | null;
|
|
29
|
+
negativePrompt?: string | null;
|
|
30
|
+
promptOptimizer?: boolean | null;
|
|
31
|
+
[key: string]: unknown; // For passthrough
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Provider options schema for FAL video generation
|
|
35
|
+
export const falVideoProviderOptionsSchema = lazySchema(() =>
|
|
36
|
+
zodSchema(
|
|
37
|
+
z
|
|
38
|
+
.object({
|
|
39
|
+
// Video loop - only for Luma models
|
|
40
|
+
loop: z.boolean().nullish(),
|
|
41
|
+
|
|
42
|
+
// Motion strength (provider-specific)
|
|
43
|
+
motionStrength: z.number().min(0).max(1).nullish(),
|
|
44
|
+
|
|
45
|
+
// Polling configuration
|
|
46
|
+
pollIntervalMs: z.number().positive().nullish(),
|
|
47
|
+
pollTimeoutMs: z.number().positive().nullish(),
|
|
48
|
+
|
|
49
|
+
// Resolution (model-specific, e.g., '480p', '720p', '1080p')
|
|
50
|
+
resolution: z.string().nullish(),
|
|
51
|
+
|
|
52
|
+
// Model-specific parameters
|
|
53
|
+
negativePrompt: z.string().nullish(),
|
|
54
|
+
promptOptimizer: z.boolean().nullish(),
|
|
55
|
+
})
|
|
56
|
+
.passthrough(),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
interface FalVideoModelConfig extends FalConfig {
|
|
61
|
+
_internal?: {
|
|
62
|
+
currentDate?: () => Date;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class FalVideoModel implements Experimental_VideoModelV3 {
|
|
67
|
+
readonly specificationVersion = 'v3';
|
|
68
|
+
readonly maxVideosPerCall = 1; // FAL video models support 1 video at a time
|
|
69
|
+
|
|
70
|
+
get provider(): string {
|
|
71
|
+
return this.config.provider;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private get normalizedModelId(): string {
|
|
75
|
+
return this.modelId.replace(/^fal-ai\//, '').replace(/^fal\//, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
readonly modelId: FalVideoModelId,
|
|
80
|
+
private readonly config: FalVideoModelConfig,
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
async doGenerate(
|
|
84
|
+
options: Parameters<Experimental_VideoModelV3['doGenerate']>[0],
|
|
85
|
+
): Promise<Awaited<ReturnType<Experimental_VideoModelV3['doGenerate']>>> {
|
|
86
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
87
|
+
const warnings: SharedV3Warning[] = [];
|
|
88
|
+
|
|
89
|
+
const falOptions = (await parseProviderOptions({
|
|
90
|
+
provider: 'fal',
|
|
91
|
+
providerOptions: options.providerOptions,
|
|
92
|
+
schema: falVideoProviderOptionsSchema,
|
|
93
|
+
})) as FalVideoProviderOptions | undefined;
|
|
94
|
+
|
|
95
|
+
const body: Record<string, unknown> = {};
|
|
96
|
+
|
|
97
|
+
if (options.prompt != null) {
|
|
98
|
+
body.prompt = options.prompt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.image != null) {
|
|
102
|
+
if (options.image.type === 'url') {
|
|
103
|
+
body.image_url = options.image.url;
|
|
104
|
+
} else {
|
|
105
|
+
body.image_url = convertImageModelFileToDataUri(options.image);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.aspectRatio) {
|
|
110
|
+
body.aspect_ratio = options.aspectRatio;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (options.duration) {
|
|
114
|
+
body.duration = `${options.duration}s`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.seed) {
|
|
118
|
+
body.seed = options.seed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (falOptions != null) {
|
|
122
|
+
const opts = falOptions;
|
|
123
|
+
if (opts.loop !== undefined && opts.loop !== null) {
|
|
124
|
+
body.loop = opts.loop;
|
|
125
|
+
}
|
|
126
|
+
if (opts.motionStrength !== undefined && opts.motionStrength !== null) {
|
|
127
|
+
body.motion_strength = opts.motionStrength;
|
|
128
|
+
}
|
|
129
|
+
if (opts.resolution !== undefined && opts.resolution !== null) {
|
|
130
|
+
body.resolution = opts.resolution;
|
|
131
|
+
}
|
|
132
|
+
if (opts.negativePrompt !== undefined && opts.negativePrompt !== null) {
|
|
133
|
+
body.negative_prompt = opts.negativePrompt;
|
|
134
|
+
}
|
|
135
|
+
if (opts.promptOptimizer !== undefined && opts.promptOptimizer !== null) {
|
|
136
|
+
body.prompt_optimizer = opts.promptOptimizer;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
140
|
+
if (
|
|
141
|
+
![
|
|
142
|
+
'loop',
|
|
143
|
+
'motionStrength',
|
|
144
|
+
'pollIntervalMs',
|
|
145
|
+
'pollTimeoutMs',
|
|
146
|
+
'resolution',
|
|
147
|
+
'negativePrompt',
|
|
148
|
+
'promptOptimizer',
|
|
149
|
+
].includes(key)
|
|
150
|
+
) {
|
|
151
|
+
body[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { value: queueResponse } = await postJsonToApi({
|
|
157
|
+
url: this.config.url({
|
|
158
|
+
path: `https://queue.fal.run/fal-ai/${this.normalizedModelId}`,
|
|
159
|
+
modelId: this.modelId,
|
|
160
|
+
}),
|
|
161
|
+
headers: combineHeaders(this.config.headers(), options.headers),
|
|
162
|
+
body,
|
|
163
|
+
failedResponseHandler: falFailedResponseHandler,
|
|
164
|
+
successfulResponseHandler:
|
|
165
|
+
createJsonResponseHandler(falJobResponseSchema),
|
|
166
|
+
abortSignal: options.abortSignal,
|
|
167
|
+
fetch: this.config.fetch,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const responseUrl = queueResponse.response_url;
|
|
171
|
+
if (!responseUrl) {
|
|
172
|
+
throw new AISDKError({
|
|
173
|
+
name: 'FAL_VIDEO_GENERATION_ERROR',
|
|
174
|
+
message: 'No response URL returned from queue endpoint',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const pollIntervalMs = falOptions?.pollIntervalMs ?? 2000; // 2 seconds
|
|
179
|
+
const pollTimeoutMs = falOptions?.pollTimeoutMs ?? 300000; // 5 minutes
|
|
180
|
+
const startTime = Date.now();
|
|
181
|
+
let response: FalVideoResponse;
|
|
182
|
+
let responseHeaders: Record<string, string> | undefined;
|
|
183
|
+
|
|
184
|
+
while (true) {
|
|
185
|
+
try {
|
|
186
|
+
const { value: statusResponse, responseHeaders: statusHeaders } =
|
|
187
|
+
await getFromApi({
|
|
188
|
+
url: this.config.url({
|
|
189
|
+
path: responseUrl,
|
|
190
|
+
modelId: this.modelId,
|
|
191
|
+
}),
|
|
192
|
+
headers: combineHeaders(this.config.headers(), options.headers),
|
|
193
|
+
failedResponseHandler: async ({
|
|
194
|
+
response,
|
|
195
|
+
url,
|
|
196
|
+
requestBodyValues,
|
|
197
|
+
}) => {
|
|
198
|
+
const body = await response.clone().json();
|
|
199
|
+
|
|
200
|
+
if (body.detail === 'Request is still in progress') {
|
|
201
|
+
return {
|
|
202
|
+
value: new Error('Request is still in progress'),
|
|
203
|
+
rawValue: body,
|
|
204
|
+
responseHeaders: {},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return createJsonErrorResponseHandler({
|
|
209
|
+
errorSchema: falErrorDataSchema,
|
|
210
|
+
errorToMessage: data => data.error.message,
|
|
211
|
+
})({ response, url, requestBodyValues });
|
|
212
|
+
},
|
|
213
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
214
|
+
falVideoResponseSchema,
|
|
215
|
+
),
|
|
216
|
+
abortSignal: options.abortSignal,
|
|
217
|
+
fetch: this.config.fetch,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
response = statusResponse;
|
|
221
|
+
responseHeaders = statusHeaders;
|
|
222
|
+
break;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (
|
|
225
|
+
error instanceof Error &&
|
|
226
|
+
error.message === 'Request is still in progress'
|
|
227
|
+
) {
|
|
228
|
+
// Continue polling
|
|
229
|
+
} else {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (Date.now() - startTime > pollTimeoutMs) {
|
|
235
|
+
throw new AISDKError({
|
|
236
|
+
name: 'FAL_VIDEO_GENERATION_TIMEOUT',
|
|
237
|
+
message: `Video generation request timed out after ${pollTimeoutMs}ms`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await delay(pollIntervalMs);
|
|
242
|
+
|
|
243
|
+
if (options.abortSignal?.aborted) {
|
|
244
|
+
throw new AISDKError({
|
|
245
|
+
name: 'FAL_VIDEO_GENERATION_ABORTED',
|
|
246
|
+
message: 'Video generation request was aborted',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const videoUrl = response.video?.url;
|
|
252
|
+
|
|
253
|
+
if (!videoUrl || !response.video) {
|
|
254
|
+
throw new AISDKError({
|
|
255
|
+
name: 'FAL_VIDEO_GENERATION_ERROR',
|
|
256
|
+
message: 'No video URL in response',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
videos: [
|
|
262
|
+
{
|
|
263
|
+
type: 'url',
|
|
264
|
+
url: videoUrl,
|
|
265
|
+
mediaType: response.video.content_type || 'video/mp4',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
warnings,
|
|
269
|
+
response: {
|
|
270
|
+
timestamp: currentDate,
|
|
271
|
+
modelId: this.modelId,
|
|
272
|
+
headers: responseHeaders,
|
|
273
|
+
},
|
|
274
|
+
providerMetadata: {
|
|
275
|
+
fal: {
|
|
276
|
+
videos: [
|
|
277
|
+
{
|
|
278
|
+
url: videoUrl,
|
|
279
|
+
width: response.video.width,
|
|
280
|
+
height: response.video.height,
|
|
281
|
+
duration: response.video.duration,
|
|
282
|
+
fps: response.video.fps,
|
|
283
|
+
contentType: response.video.content_type,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
...(response.seed !== undefined ? { seed: response.seed } : {}),
|
|
287
|
+
...(response.timings ? { timings: response.timings } : {}),
|
|
288
|
+
...(response.has_nsfw_concepts !== undefined
|
|
289
|
+
? { has_nsfw_concepts: response.has_nsfw_concepts }
|
|
290
|
+
: {}),
|
|
291
|
+
...(response.prompt ? { prompt: response.prompt } : {}),
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const falJobResponseSchema = z.object({
|
|
299
|
+
request_id: z.string().nullish(),
|
|
300
|
+
response_url: z.string().nullish(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const falVideoResponseSchema = z.object({
|
|
304
|
+
video: z
|
|
305
|
+
.object({
|
|
306
|
+
url: z.string(),
|
|
307
|
+
width: z.number().nullish(),
|
|
308
|
+
height: z.number().nullish(),
|
|
309
|
+
duration: z.number().nullish(),
|
|
310
|
+
fps: z.number().nullish(),
|
|
311
|
+
content_type: z.string().nullish(),
|
|
312
|
+
})
|
|
313
|
+
.nullish(),
|
|
314
|
+
seed: z.number().nullish(),
|
|
315
|
+
timings: z
|
|
316
|
+
.object({
|
|
317
|
+
inference: z.number().nullish(),
|
|
318
|
+
})
|
|
319
|
+
.nullish(),
|
|
320
|
+
has_nsfw_concepts: z.array(z.boolean()).nullish(),
|
|
321
|
+
prompt: z.string().nullish(),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
type FalVideoResponse = z.infer<typeof falVideoResponseSchema>;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { createFal, fal } from './fal-provider';
|
|
2
1
|
export type { FalProvider, FalProviderSettings } from './fal-provider';
|
|
2
|
+
export { createFal, fal } from './fal-provider';
|
|
3
3
|
export type { FalImageProviderOptions } from './fal-image-options';
|
|
4
|
+
export type { FalVideoProviderOptions } from './fal-video-model';
|
|
5
|
+
export type { FalVideoModelId } from './fal-video-settings';
|
|
4
6
|
export { VERSION } from './version';
|