@tanstack/ai-fal 0.9.11 → 0.9.13
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/dist/esm/adapters/audio.js +92 -76
- package/dist/esm/adapters/audio.js.map +1 -1
- package/dist/esm/adapters/image.js +99 -108
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/speech.js +86 -83
- package/dist/esm/adapters/speech.js.map +1 -1
- package/dist/esm/adapters/transcription.js +90 -89
- package/dist/esm/adapters/transcription.js.map +1 -1
- package/dist/esm/adapters/video.js +146 -150
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/image/generated/image-field-overrides.js +809 -367
- package/dist/esm/image/generated/image-field-overrides.js.map +1 -1
- package/dist/esm/image/image-inputs.js +143 -104
- package/dist/esm/image/image-inputs.js.map +1 -1
- package/dist/esm/image/image-provider-options.js +16 -21
- package/dist/esm/image/image-provider-options.js.map +1 -1
- package/dist/esm/index.js +2 -17
- package/dist/esm/utils/billing.js +89 -45
- package/dist/esm/utils/billing.js.map +1 -1
- package/dist/esm/utils/client.js +90 -87
- package/dist/esm/utils/client.js.map +1 -1
- package/dist/esm/video/video-provider-options.js +15 -16
- package/dist/esm/video/video-provider-options.js.map +1 -1
- package/package.json +7 -7
- package/dist/esm/index.js.map +0 -1
|
@@ -1,165 +1,161 @@
|
|
|
1
|
+
import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
|
|
2
|
+
import { configureFalClient, generateId } from "../utils/client.js";
|
|
3
|
+
import { mapImageInputsToFalVideoFields } from "../image/image-inputs.js";
|
|
4
|
+
import { mapVideoSizeToFalFormat } from "../video/video-provider-options.js";
|
|
1
5
|
import { fal } from "@fal-ai/client";
|
|
2
6
|
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
3
7
|
import { BaseVideoAdapter } from "@tanstack/ai/adapters";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
//#region src/adapters/video.ts
|
|
9
|
+
/**
|
|
10
|
+
* Map video conditioning inputs onto fal field names.
|
|
11
|
+
* Video-to-video endpoints on fal almost universally use `video_url`; the
|
|
12
|
+
* occasional model takes `video_urls` (rare). Mirror the image-input logic
|
|
13
|
+
* positionally with a `reference` role escape hatch via `reference_video_urls`.
|
|
14
|
+
*/
|
|
8
15
|
function mapVideoInputsToFalFields(videoInputs) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (sources.length === 1) {
|
|
23
|
-
out.video_url = sources[0];
|
|
24
|
-
} else if (sources.length > 1) {
|
|
25
|
-
out.video_urls = sources;
|
|
26
|
-
}
|
|
27
|
-
return out;
|
|
16
|
+
if (!videoInputs || videoInputs.length === 0) return {};
|
|
17
|
+
const references = [];
|
|
18
|
+
const sources = [];
|
|
19
|
+
for (const part of videoInputs) {
|
|
20
|
+
const url = videoPartToUrl(part);
|
|
21
|
+
if (part.metadata?.role === "reference" || part.metadata?.role === "character") references.push(url);
|
|
22
|
+
else sources.push(url);
|
|
23
|
+
}
|
|
24
|
+
const out = {};
|
|
25
|
+
if (references.length > 0) out.reference_video_urls = references;
|
|
26
|
+
if (sources.length === 1) out.video_url = sources[0];
|
|
27
|
+
else if (sources.length > 1) out.video_urls = sources;
|
|
28
|
+
return out;
|
|
28
29
|
}
|
|
29
30
|
function mapAudioInputsToFalFields(audioInputs) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
audio_url: part.source.type === "url" ? part.source.value : `data:${part.source.mimeType};base64,${part.source.value}`
|
|
39
|
-
};
|
|
31
|
+
if (!audioInputs || audioInputs.length === 0) return {};
|
|
32
|
+
const [part, ...rest] = audioInputs;
|
|
33
|
+
if (!part || rest.length > 0) throw new Error(`fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`);
|
|
34
|
+
return { audio_url: part.source.type === "url" ? part.source.value : `data:${part.source.mimeType};base64,${part.source.value}` };
|
|
40
35
|
}
|
|
41
36
|
function videoPartToUrl(part) {
|
|
42
|
-
|
|
37
|
+
return part.source.type === "url" ? part.source.value : `data:${part.source.mimeType};base64,${part.source.value}`;
|
|
43
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Maps fal.ai queue status to TanStack AI video status.
|
|
41
|
+
*
|
|
42
|
+
* Note: fal.ai does not return a FAILED queue status. Errors surface
|
|
43
|
+
* as exceptions when fetching results from a COMPLETED job (e.g. 422
|
|
44
|
+
* validation errors). Those are handled in getVideoUrl().
|
|
45
|
+
*/
|
|
44
46
|
function mapFalStatusToVideoStatus(falStatus) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return "completed";
|
|
52
|
-
default:
|
|
53
|
-
return "processing";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
class FalVideoAdapter extends BaseVideoAdapter {
|
|
57
|
-
kind = "video";
|
|
58
|
-
name = "fal";
|
|
59
|
-
constructor(model, config) {
|
|
60
|
-
super({}, model);
|
|
61
|
-
configureFalClient(config);
|
|
62
|
-
}
|
|
63
|
-
async createVideoJob(options) {
|
|
64
|
-
const { size, duration, modelOptions, logger } = options;
|
|
65
|
-
logger.request(`activity=generateVideo provider=fal model=${this.model}`, {
|
|
66
|
-
provider: "fal",
|
|
67
|
-
model: this.model
|
|
68
|
-
});
|
|
69
|
-
try {
|
|
70
|
-
const resolved = resolveMediaPrompt(options.prompt);
|
|
71
|
-
const sizeParams = mapVideoSizeToFalFormat(size);
|
|
72
|
-
const inputImageFields = mapImageInputsToFalVideoFields(
|
|
73
|
-
this.model,
|
|
74
|
-
resolved.images
|
|
75
|
-
);
|
|
76
|
-
const videoFields = mapVideoInputsToFalFields(resolved.videos);
|
|
77
|
-
const audioFields = mapAudioInputsToFalFields(resolved.audios);
|
|
78
|
-
const input = {
|
|
79
|
-
...sizeParams,
|
|
80
|
-
...inputImageFields,
|
|
81
|
-
...videoFields,
|
|
82
|
-
...audioFields,
|
|
83
|
-
// modelOptions applied after derived media fields so explicit user
|
|
84
|
-
// overrides (video_url, reference_video_urls, audio_url, ...) win.
|
|
85
|
-
...modelOptions,
|
|
86
|
-
// Media-only prompts omit the prompt field rather than sending an
|
|
87
|
-
// empty string (e.g. pure image-to-video endpoints).
|
|
88
|
-
...resolved.text ? { prompt: resolved.text } : {},
|
|
89
|
-
...duration ? { duration } : {}
|
|
90
|
-
};
|
|
91
|
-
const { request_id } = await fal.queue.submit(this.model, {
|
|
92
|
-
input
|
|
93
|
-
});
|
|
94
|
-
return {
|
|
95
|
-
jobId: request_id,
|
|
96
|
-
model: this.model
|
|
97
|
-
};
|
|
98
|
-
} catch (error) {
|
|
99
|
-
logger.errors("fal.createVideoJob fatal", {
|
|
100
|
-
error,
|
|
101
|
-
source: "fal.createVideoJob"
|
|
102
|
-
});
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
async getVideoStatus(jobId) {
|
|
107
|
-
const statusResponse = await fal.queue.status(this.model, {
|
|
108
|
-
requestId: jobId,
|
|
109
|
-
logs: true
|
|
110
|
-
});
|
|
111
|
-
return {
|
|
112
|
-
jobId,
|
|
113
|
-
status: mapFalStatusToVideoStatus(statusResponse.status),
|
|
114
|
-
...statusResponse.queue_position != null ? {
|
|
115
|
-
progress: Math.max(0, 100 - statusResponse.queue_position * 10)
|
|
116
|
-
} : {}
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
async getVideoUrl(jobId) {
|
|
120
|
-
let result;
|
|
121
|
-
try {
|
|
122
|
-
result = await fal.queue.result(this.model, {
|
|
123
|
-
requestId: jobId
|
|
124
|
-
});
|
|
125
|
-
} catch (error) {
|
|
126
|
-
const err = error;
|
|
127
|
-
const detail = err.body?.detail;
|
|
128
|
-
if (Array.isArray(detail)) {
|
|
129
|
-
const messages = detail.map(
|
|
130
|
-
(d) => d.loc ? `${d.loc.join(".")}: ${d.msg}` : d.msg
|
|
131
|
-
);
|
|
132
|
-
throw new Error(`Video generation failed: ${messages.join("; ")}`);
|
|
133
|
-
}
|
|
134
|
-
throw new Error(
|
|
135
|
-
`Failed to retrieve video result: ${err.message || String(error)}`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
const data = result.data;
|
|
139
|
-
const url = data.video?.url || data.video_url;
|
|
140
|
-
if (!url) {
|
|
141
|
-
throw new Error("Video URL not found in response");
|
|
142
|
-
}
|
|
143
|
-
const usage = buildFalUsage(takeBillableUnits(result.requestId));
|
|
144
|
-
return {
|
|
145
|
-
jobId,
|
|
146
|
-
url,
|
|
147
|
-
...usage ? { usage } : {}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
generateId() {
|
|
151
|
-
return generateId(this.name);
|
|
152
|
-
}
|
|
47
|
+
switch (falStatus) {
|
|
48
|
+
case "IN_QUEUE": return "pending";
|
|
49
|
+
case "IN_PROGRESS": return "processing";
|
|
50
|
+
case "COMPLETED": return "completed";
|
|
51
|
+
default: return "processing";
|
|
52
|
+
}
|
|
153
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* fal.ai video generation adapter.
|
|
56
|
+
* Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.
|
|
57
|
+
*
|
|
58
|
+
* Uses fal.ai's comprehensive type system to provide autocomplete
|
|
59
|
+
* and type safety for all supported video models.
|
|
60
|
+
*
|
|
61
|
+
* @experimental Video generation is an experimental feature and may change.
|
|
62
|
+
*/
|
|
63
|
+
var FalVideoAdapter = class extends BaseVideoAdapter {
|
|
64
|
+
kind = "video";
|
|
65
|
+
name = "fal";
|
|
66
|
+
constructor(model, config) {
|
|
67
|
+
super({}, model);
|
|
68
|
+
configureFalClient(config);
|
|
69
|
+
}
|
|
70
|
+
async createVideoJob(options) {
|
|
71
|
+
const { size, duration, modelOptions, logger } = options;
|
|
72
|
+
logger.request(`activity=generateVideo provider=fal model=${this.model}`, {
|
|
73
|
+
provider: "fal",
|
|
74
|
+
model: this.model
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
const resolved = resolveMediaPrompt(options.prompt);
|
|
78
|
+
const sizeParams = mapVideoSizeToFalFormat(size);
|
|
79
|
+
const inputImageFields = mapImageInputsToFalVideoFields(this.model, resolved.images);
|
|
80
|
+
const videoFields = mapVideoInputsToFalFields(resolved.videos);
|
|
81
|
+
const audioFields = mapAudioInputsToFalFields(resolved.audios);
|
|
82
|
+
const input = {
|
|
83
|
+
...sizeParams,
|
|
84
|
+
...inputImageFields,
|
|
85
|
+
...videoFields,
|
|
86
|
+
...audioFields,
|
|
87
|
+
...modelOptions,
|
|
88
|
+
...resolved.text ? { prompt: resolved.text } : {},
|
|
89
|
+
...duration ? { duration } : {}
|
|
90
|
+
};
|
|
91
|
+
const { request_id } = await fal.queue.submit(this.model, { input });
|
|
92
|
+
return {
|
|
93
|
+
jobId: request_id,
|
|
94
|
+
model: this.model
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.errors("fal.createVideoJob fatal", {
|
|
98
|
+
error,
|
|
99
|
+
source: "fal.createVideoJob"
|
|
100
|
+
});
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async getVideoStatus(jobId) {
|
|
105
|
+
const statusResponse = await fal.queue.status(this.model, {
|
|
106
|
+
requestId: jobId,
|
|
107
|
+
logs: true
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
jobId,
|
|
111
|
+
status: mapFalStatusToVideoStatus(statusResponse.status),
|
|
112
|
+
...statusResponse.queue_position != null ? { progress: Math.max(0, 100 - statusResponse.queue_position * 10) } : {}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async getVideoUrl(jobId) {
|
|
116
|
+
let result;
|
|
117
|
+
try {
|
|
118
|
+
result = await fal.queue.result(this.model, { requestId: jobId });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const err = error;
|
|
121
|
+
const detail = err.body?.detail;
|
|
122
|
+
if (Array.isArray(detail)) {
|
|
123
|
+
const messages = detail.map((d) => d.loc ? `${d.loc.join(".")}: ${d.msg}` : d.msg);
|
|
124
|
+
throw new Error(`Video generation failed: ${messages.join("; ")}`);
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Failed to retrieve video result: ${err.message || String(error)}`);
|
|
127
|
+
}
|
|
128
|
+
const data = result.data;
|
|
129
|
+
const url = data.video?.url || data.video_url;
|
|
130
|
+
if (!url) throw new Error("Video URL not found in response");
|
|
131
|
+
const usage = buildFalUsage(takeBillableUnits(result.requestId));
|
|
132
|
+
return {
|
|
133
|
+
jobId,
|
|
134
|
+
url,
|
|
135
|
+
...usage ? { usage } : {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
generateId() {
|
|
139
|
+
return generateId(this.name);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Create a fal.ai video adapter with an explicit API key.
|
|
144
|
+
*
|
|
145
|
+
* @experimental Video generation is an experimental feature and may change.
|
|
146
|
+
*/
|
|
154
147
|
function createFalVideo(model, config) {
|
|
155
|
-
|
|
148
|
+
return new FalVideoAdapter(model, config);
|
|
156
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.
|
|
152
|
+
*
|
|
153
|
+
* @experimental Video generation is an experimental feature and may change.
|
|
154
|
+
*/
|
|
157
155
|
function falVideo(model, config) {
|
|
158
|
-
|
|
156
|
+
return createFalVideo(model, config);
|
|
159
157
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
};
|
|
165
|
-
//# sourceMappingURL=video.js.map
|
|
158
|
+
//#endregion
|
|
159
|
+
export { FalVideoAdapter, createFalVideo, falVideo };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=video.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.js","sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapVideoSizeToFalFormat } from '../video/video-provider-options'\nimport { mapImageInputsToFalVideoFields } from '../image/image-inputs'\nimport type {\n AudioPart,\n MediaInputMetadata,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoSize,\n FalVideoPromptModalitiesFor,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils/client'\n\n/**\n * Map video conditioning inputs onto fal field names.\n * Video-to-video endpoints on fal almost universally use `video_url`; the\n * occasional model takes `video_urls` (rare). Mirror the image-input logic\n * positionally with a `reference` role escape hatch via `reference_video_urls`.\n */\nfunction mapVideoInputsToFalFields(\n videoInputs?: ReadonlyArray<VideoPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!videoInputs || videoInputs.length === 0) return {}\n const references: Array<string> = []\n const sources: Array<string> = []\n for (const part of videoInputs) {\n const url = videoPartToUrl(part)\n if (\n part.metadata?.role === 'reference' ||\n part.metadata?.role === 'character'\n ) {\n references.push(url)\n } else {\n sources.push(url)\n }\n }\n const out: Record<string, unknown> = {}\n if (references.length > 0) out.reference_video_urls = references\n if (sources.length === 1) {\n out.video_url = sources[0]\n } else if (sources.length > 1) {\n out.video_urls = sources\n }\n return out\n}\n\nfunction mapAudioInputsToFalFields(\n audioInputs?: ReadonlyArray<AudioPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!audioInputs || audioInputs.length === 0) return {}\n const [part, ...rest] = audioInputs\n if (!part || rest.length > 0) {\n throw new Error(\n `fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`,\n )\n }\n return {\n audio_url:\n part.source.type === 'url'\n ? part.source.value\n : `data:${part.source.mimeType};base64,${part.source.value}`,\n }\n}\n\nfunction videoPartToUrl(part: VideoPart<MediaInputMetadata>): string {\n return part.source.type === 'url'\n ? part.source.value\n : `data:${part.source.mimeType};base64,${part.source.value}`\n}\n\ntype FalQueueStatus = 'IN_QUEUE' | 'IN_PROGRESS' | 'COMPLETED'\n\ninterface FalStatusResponse {\n status: FalQueueStatus\n queue_position?: number\n logs?: Array<{ message: string }>\n}\n\ninterface FalVideoResultData {\n video?: { url: string }\n video_url?: string\n}\n\n/**\n * Maps fal.ai queue status to TanStack AI video status.\n *\n * Note: fal.ai does not return a FAILED queue status. Errors surface\n * as exceptions when fetching results from a COMPLETED job (e.g. 422\n * validation errors). Those are handled in getVideoUrl().\n */\nfunction mapFalStatusToVideoStatus(\n falStatus: FalQueueStatus,\n): VideoStatusResult['status'] {\n switch (falStatus) {\n case 'IN_QUEUE':\n return 'pending'\n case 'IN_PROGRESS':\n return 'processing'\n case 'COMPLETED':\n return 'completed'\n default:\n return 'processing'\n }\n}\n\n/**\n * fal.ai video generation adapter.\n * Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.\n *\n * Uses fal.ai's comprehensive type system to provide autocomplete\n * and type safety for all supported video models.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<\n TModel,\n FalVideoProviderOptions<TModel>,\n Record<TModel, FalVideoProviderOptions<TModel>>,\n Record<TModel, FalModelVideoSize<TModel>>,\n Record<TModel, FalVideoPromptModalitiesFor<TModel>>\n> {\n override readonly kind = 'video' as const\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super({}, model)\n configureFalClient(config)\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n FalVideoProviderOptions<TModel>,\n FalModelVideoSize<TModel>\n >,\n ): Promise<VideoJobResult> {\n const { size, duration, modelOptions, logger } = options\n\n logger.request(`activity=generateVideo provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n try {\n const resolved = resolveMediaPrompt(options.prompt)\n const sizeParams = mapVideoSizeToFalFormat(size)\n const inputImageFields = mapImageInputsToFalVideoFields(\n this.model,\n resolved.images,\n )\n const videoFields = mapVideoInputsToFalFields(resolved.videos)\n const audioFields = mapAudioInputsToFalFields(resolved.audios)\n\n const input = {\n ...sizeParams,\n ...inputImageFields,\n ...videoFields,\n ...audioFields,\n // modelOptions applied after derived media fields so explicit user\n // overrides (video_url, reference_video_urls, audio_url, ...) win.\n ...modelOptions,\n // Media-only prompts omit the prompt field rather than sending an\n // empty string (e.g. pure image-to-video endpoints).\n ...(resolved.text ? { prompt: resolved.text } : {}),\n ...(duration ? { duration } : {}),\n } as FalModelInput<TModel>\n\n // Submit to queue and get request ID\n const { request_id } = await fal.queue.submit(this.model, {\n input,\n })\n\n return {\n jobId: request_id,\n model: this.model,\n }\n } catch (error) {\n logger.errors('fal.createVideoJob fatal', {\n error,\n source: 'fal.createVideoJob',\n })\n throw error\n }\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n const statusResponse = (await fal.queue.status(this.model, {\n requestId: jobId,\n logs: true,\n })) as FalStatusResponse\n\n return {\n jobId,\n status: mapFalStatusToVideoStatus(statusResponse.status),\n ...(statusResponse.queue_position != null\n ? {\n progress: Math.max(0, 100 - statusResponse.queue_position * 10),\n }\n : {}),\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n let result\n try {\n result = await fal.queue.result(this.model, {\n requestId: jobId,\n })\n } catch (error: unknown) {\n // fal.ai may report COMPLETED status but throw on result fetch\n // (e.g. 422 validation errors). Extract the detailed error info.\n const err = error as { body?: { detail?: unknown }; message?: string }\n const detail = err.body?.detail\n if (Array.isArray(detail)) {\n const messages = detail.map(\n (d: { msg?: string; loc?: Array<string> }) =>\n d.loc ? `${d.loc.join('.')}: ${d.msg}` : d.msg,\n )\n throw new Error(`Video generation failed: ${messages.join('; ')}`)\n }\n throw new Error(\n `Failed to retrieve video result: ${err.message || String(error)}`,\n )\n }\n\n const data = result.data as FalVideoResultData\n\n // Different models return video URL in different formats\n const url = data.video?.url || data.video_url\n if (!url) {\n throw new Error('Video URL not found in response')\n }\n\n const usage = buildFalUsage(takeBillableUnits(result.requestId))\n\n return {\n jobId,\n url,\n ...(usage ? { usage } : {}),\n }\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n}\n\n/**\n * Create a fal.ai video adapter with an explicit API key.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function createFalVideo<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalVideoAdapter<TModel> {\n return new FalVideoAdapter(model, config)\n}\n\n/**\n * Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function falVideo<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalVideoAdapter<TModel> {\n return createFalVideo(model, config)\n}\n"],"names":["utilGenerateId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapVideoSizeToFalFormat } from '../video/video-provider-options'\nimport { mapImageInputsToFalVideoFields } from '../image/image-inputs'\nimport type {\n AudioPart,\n MediaInputMetadata,\n VideoGenerationOptions,\n VideoJobResult,\n VideoPart,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoSize,\n FalVideoPromptModalitiesFor,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils/client'\n\n/**\n * Map video conditioning inputs onto fal field names.\n * Video-to-video endpoints on fal almost universally use `video_url`; the\n * occasional model takes `video_urls` (rare). Mirror the image-input logic\n * positionally with a `reference` role escape hatch via `reference_video_urls`.\n */\nfunction mapVideoInputsToFalFields(\n videoInputs?: ReadonlyArray<VideoPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!videoInputs || videoInputs.length === 0) return {}\n const references: Array<string> = []\n const sources: Array<string> = []\n for (const part of videoInputs) {\n const url = videoPartToUrl(part)\n if (\n part.metadata?.role === 'reference' ||\n part.metadata?.role === 'character'\n ) {\n references.push(url)\n } else {\n sources.push(url)\n }\n }\n const out: Record<string, unknown> = {}\n if (references.length > 0) out.reference_video_urls = references\n if (sources.length === 1) {\n out.video_url = sources[0]\n } else if (sources.length > 1) {\n out.video_urls = sources\n }\n return out\n}\n\nfunction mapAudioInputsToFalFields(\n audioInputs?: ReadonlyArray<AudioPart<MediaInputMetadata>>,\n): Record<string, unknown> {\n if (!audioInputs || audioInputs.length === 0) return {}\n const [part, ...rest] = audioInputs\n if (!part || rest.length > 0) {\n throw new Error(\n `fal: exactly one audio prompt part is supported (received ${audioInputs.length}).`,\n )\n }\n return {\n audio_url:\n part.source.type === 'url'\n ? part.source.value\n : `data:${part.source.mimeType};base64,${part.source.value}`,\n }\n}\n\nfunction videoPartToUrl(part: VideoPart<MediaInputMetadata>): string {\n return part.source.type === 'url'\n ? part.source.value\n : `data:${part.source.mimeType};base64,${part.source.value}`\n}\n\ntype FalQueueStatus = 'IN_QUEUE' | 'IN_PROGRESS' | 'COMPLETED'\n\ninterface FalStatusResponse {\n status: FalQueueStatus\n queue_position?: number\n logs?: Array<{ message: string }>\n}\n\ninterface FalVideoResultData {\n video?: { url: string }\n video_url?: string\n}\n\n/**\n * Maps fal.ai queue status to TanStack AI video status.\n *\n * Note: fal.ai does not return a FAILED queue status. Errors surface\n * as exceptions when fetching results from a COMPLETED job (e.g. 422\n * validation errors). Those are handled in getVideoUrl().\n */\nfunction mapFalStatusToVideoStatus(\n falStatus: FalQueueStatus,\n): VideoStatusResult['status'] {\n switch (falStatus) {\n case 'IN_QUEUE':\n return 'pending'\n case 'IN_PROGRESS':\n return 'processing'\n case 'COMPLETED':\n return 'completed'\n default:\n return 'processing'\n }\n}\n\n/**\n * fal.ai video generation adapter.\n * Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.\n *\n * Uses fal.ai's comprehensive type system to provide autocomplete\n * and type safety for all supported video models.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<\n TModel,\n FalVideoProviderOptions<TModel>,\n Record<TModel, FalVideoProviderOptions<TModel>>,\n Record<TModel, FalModelVideoSize<TModel>>,\n Record<TModel, FalVideoPromptModalitiesFor<TModel>>\n> {\n override readonly kind = 'video' as const\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super({}, model)\n configureFalClient(config)\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n FalVideoProviderOptions<TModel>,\n FalModelVideoSize<TModel>\n >,\n ): Promise<VideoJobResult> {\n const { size, duration, modelOptions, logger } = options\n\n logger.request(`activity=generateVideo provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n try {\n const resolved = resolveMediaPrompt(options.prompt)\n const sizeParams = mapVideoSizeToFalFormat(size)\n const inputImageFields = mapImageInputsToFalVideoFields(\n this.model,\n resolved.images,\n )\n const videoFields = mapVideoInputsToFalFields(resolved.videos)\n const audioFields = mapAudioInputsToFalFields(resolved.audios)\n\n const input = {\n ...sizeParams,\n ...inputImageFields,\n ...videoFields,\n ...audioFields,\n // modelOptions applied after derived media fields so explicit user\n // overrides (video_url, reference_video_urls, audio_url, ...) win.\n ...modelOptions,\n // Media-only prompts omit the prompt field rather than sending an\n // empty string (e.g. pure image-to-video endpoints).\n ...(resolved.text ? { prompt: resolved.text } : {}),\n ...(duration ? { duration } : {}),\n } as FalModelInput<TModel>\n\n // Submit to queue and get request ID\n const { request_id } = await fal.queue.submit(this.model, {\n input,\n })\n\n return {\n jobId: request_id,\n model: this.model,\n }\n } catch (error) {\n logger.errors('fal.createVideoJob fatal', {\n error,\n source: 'fal.createVideoJob',\n })\n throw error\n }\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n const statusResponse = (await fal.queue.status(this.model, {\n requestId: jobId,\n logs: true,\n })) as FalStatusResponse\n\n return {\n jobId,\n status: mapFalStatusToVideoStatus(statusResponse.status),\n ...(statusResponse.queue_position != null\n ? {\n progress: Math.max(0, 100 - statusResponse.queue_position * 10),\n }\n : {}),\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n let result\n try {\n result = await fal.queue.result(this.model, {\n requestId: jobId,\n })\n } catch (error: unknown) {\n // fal.ai may report COMPLETED status but throw on result fetch\n // (e.g. 422 validation errors). Extract the detailed error info.\n const err = error as { body?: { detail?: unknown }; message?: string }\n const detail = err.body?.detail\n if (Array.isArray(detail)) {\n const messages = detail.map(\n (d: { msg?: string; loc?: Array<string> }) =>\n d.loc ? `${d.loc.join('.')}: ${d.msg}` : d.msg,\n )\n throw new Error(`Video generation failed: ${messages.join('; ')}`)\n }\n throw new Error(\n `Failed to retrieve video result: ${err.message || String(error)}`,\n )\n }\n\n const data = result.data as FalVideoResultData\n\n // Different models return video URL in different formats\n const url = data.video?.url || data.video_url\n if (!url) {\n throw new Error('Video URL not found in response')\n }\n\n const usage = buildFalUsage(takeBillableUnits(result.requestId))\n\n return {\n jobId,\n url,\n ...(usage ? { usage } : {}),\n }\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n}\n\n/**\n * Create a fal.ai video adapter with an explicit API key.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function createFalVideo<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalVideoAdapter<TModel> {\n return new FalVideoAdapter(model, config)\n}\n\n/**\n * Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function falVideo<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalVideoAdapter<TModel> {\n return createFalVideo(model, config)\n}\n"],"mappings":"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"}
|