@tanstack/ai-fal 0.9.12 → 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,83 +1,99 @@
|
|
|
1
|
+
import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
|
|
2
|
+
import { configureFalClient, deriveAudioContentType, generateId } from "../utils/client.js";
|
|
1
3
|
import { fal } from "@fal-ai/client";
|
|
2
4
|
import { BaseAudioAdapter } from "@tanstack/ai/adapters";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
//#region src/adapters/audio.ts
|
|
6
|
+
/**
|
|
7
|
+
* Fal audio models don't agree on the name for "how long should this be."
|
|
8
|
+
* Most accept seconds via `duration`, but a few use model-specific fields —
|
|
9
|
+
* e.g. ElevenLabs wants milliseconds via `music_length_ms`, Stable Audio wants
|
|
10
|
+
* `seconds_total`. Explicit per-model entries override the default; user
|
|
11
|
+
* `modelOptions` still win over either.
|
|
12
|
+
*/
|
|
13
|
+
var DURATION_FRAGMENT_BUILDERS = {
|
|
14
|
+
"fal-ai/elevenlabs/music": (seconds) => ({ music_length_ms: seconds * 1e3 }),
|
|
15
|
+
"fal-ai/stable-audio-25/text-to-audio": (seconds) => ({ seconds_total: seconds })
|
|
10
16
|
};
|
|
11
17
|
function buildDurationFragment(model, duration) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
class FalAudioAdapter extends BaseAudioAdapter {
|
|
17
|
-
name = "fal";
|
|
18
|
-
constructor(model, config) {
|
|
19
|
-
super(model, {});
|
|
20
|
-
configureFalClient(config);
|
|
21
|
-
}
|
|
22
|
-
async generateAudio(options) {
|
|
23
|
-
const { logger } = options;
|
|
24
|
-
logger.request(`activity=generateAudio provider=fal model=${this.model}`, {
|
|
25
|
-
provider: "fal",
|
|
26
|
-
model: this.model
|
|
27
|
-
});
|
|
28
|
-
try {
|
|
29
|
-
const input = this.buildInput(options);
|
|
30
|
-
const result = await fal.subscribe(this.model, { input });
|
|
31
|
-
return this.transformResponse(result);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
logger.errors("fal.generateAudio fatal", {
|
|
34
|
-
error,
|
|
35
|
-
source: "fal.generateAudio"
|
|
36
|
-
});
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
buildInput(options) {
|
|
41
|
-
return {
|
|
42
|
-
...buildDurationFragment(this.model, options.duration),
|
|
43
|
-
...options.modelOptions,
|
|
44
|
-
prompt: options.prompt
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
generateId() {
|
|
48
|
-
return generateId(this.name);
|
|
49
|
-
}
|
|
50
|
-
transformResponse(response) {
|
|
51
|
-
const data = response.data;
|
|
52
|
-
let audioUrl;
|
|
53
|
-
let contentType;
|
|
54
|
-
if (data["audio"] && typeof data["audio"] === "object" && "url" in data["audio"]) {
|
|
55
|
-
const audioObj = data["audio"];
|
|
56
|
-
audioUrl = audioObj.url;
|
|
57
|
-
contentType = audioObj.content_type;
|
|
58
|
-
} else if (typeof data.audio_url === "string") {
|
|
59
|
-
audioUrl = data.audio_url;
|
|
60
|
-
}
|
|
61
|
-
if (!audioUrl) {
|
|
62
|
-
throw new Error("Audio URL not found in fal audio generation response");
|
|
63
|
-
}
|
|
64
|
-
const usage = buildFalUsage(takeBillableUnits(response.requestId));
|
|
65
|
-
return {
|
|
66
|
-
id: response.requestId || this.generateId(),
|
|
67
|
-
model: this.model,
|
|
68
|
-
audio: {
|
|
69
|
-
url: audioUrl,
|
|
70
|
-
contentType: deriveAudioContentType(contentType, audioUrl)
|
|
71
|
-
},
|
|
72
|
-
...usage ? { usage } : {}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
18
|
+
if (duration == null) return {};
|
|
19
|
+
const builder = DURATION_FRAGMENT_BUILDERS[model];
|
|
20
|
+
return builder ? builder(duration) : { duration };
|
|
75
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* fal.ai audio generation adapter.
|
|
24
|
+
*
|
|
25
|
+
* Supports fal.ai audio models like diffrhythm (music), sound effects, etc.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const adapter = falAudio('fal-ai/diffrhythm')
|
|
30
|
+
* const result = await generateAudio({
|
|
31
|
+
* adapter,
|
|
32
|
+
* prompt: 'An upbeat electronic track with synths',
|
|
33
|
+
* duration: 10,
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
var FalAudioAdapter = class extends BaseAudioAdapter {
|
|
38
|
+
name = "fal";
|
|
39
|
+
constructor(model, config) {
|
|
40
|
+
super(model, {});
|
|
41
|
+
configureFalClient(config);
|
|
42
|
+
}
|
|
43
|
+
async generateAudio(options) {
|
|
44
|
+
const { logger } = options;
|
|
45
|
+
logger.request(`activity=generateAudio provider=fal model=${this.model}`, {
|
|
46
|
+
provider: "fal",
|
|
47
|
+
model: this.model
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
const input = this.buildInput(options);
|
|
51
|
+
const result = await fal.subscribe(this.model, { input });
|
|
52
|
+
return this.transformResponse(result);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.errors("fal.generateAudio fatal", {
|
|
55
|
+
error,
|
|
56
|
+
source: "fal.generateAudio"
|
|
57
|
+
});
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
buildInput(options) {
|
|
62
|
+
return {
|
|
63
|
+
...buildDurationFragment(this.model, options.duration),
|
|
64
|
+
...options.modelOptions,
|
|
65
|
+
prompt: options.prompt
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
generateId() {
|
|
69
|
+
return generateId(this.name);
|
|
70
|
+
}
|
|
71
|
+
transformResponse(response) {
|
|
72
|
+
const data = response.data;
|
|
73
|
+
let audioUrl;
|
|
74
|
+
let contentType;
|
|
75
|
+
if (data["audio"] && typeof data["audio"] === "object" && "url" in data["audio"]) {
|
|
76
|
+
const audioObj = data["audio"];
|
|
77
|
+
audioUrl = audioObj.url;
|
|
78
|
+
contentType = audioObj.content_type;
|
|
79
|
+
} else if (typeof data.audio_url === "string") audioUrl = data.audio_url;
|
|
80
|
+
if (!audioUrl) throw new Error("Audio URL not found in fal audio generation response");
|
|
81
|
+
const usage = buildFalUsage(takeBillableUnits(response.requestId));
|
|
82
|
+
return {
|
|
83
|
+
id: response.requestId || this.generateId(),
|
|
84
|
+
model: this.model,
|
|
85
|
+
audio: {
|
|
86
|
+
url: audioUrl,
|
|
87
|
+
contentType: deriveAudioContentType(contentType, audioUrl)
|
|
88
|
+
},
|
|
89
|
+
...usage ? { usage } : {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
76
93
|
function falAudio(model, config) {
|
|
77
|
-
|
|
94
|
+
return new FalAudioAdapter(model, config);
|
|
78
95
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//# sourceMappingURL=audio.js.map
|
|
96
|
+
//#endregion
|
|
97
|
+
export { FalAudioAdapter, falAudio };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.js","sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n deriveAudioContentType,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils/client'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Provider options for audio generation, excluding fields TanStack AI handles.\n */\nexport type FalAudioProviderOptions<TModel extends string> = Omit<\n FalModelInput<TModel>,\n 'prompt'\n>\n\n/**\n * Fal audio models don't agree on the name for \"how long should this be.\"\n * Most accept seconds via `duration`, but a few use model-specific fields —\n * e.g. ElevenLabs wants milliseconds via `music_length_ms`, Stable Audio wants\n * `seconds_total`. Explicit per-model entries override the default; user\n * `modelOptions` still win over either.\n */\nconst DURATION_FRAGMENT_BUILDERS: Record<\n string,\n (seconds: number) => Record<string, unknown>\n> = {\n 'fal-ai/elevenlabs/music': (seconds) => ({ music_length_ms: seconds * 1000 }),\n 'fal-ai/stable-audio-25/text-to-audio': (seconds) => ({\n seconds_total: seconds,\n }),\n}\n\nfunction buildDurationFragment(\n model: string,\n duration: number | undefined,\n): Record<string, unknown> {\n if (duration == null) return {}\n const builder = DURATION_FRAGMENT_BUILDERS[model]\n return builder ? builder(duration) : { duration }\n}\n\n/**\n * fal.ai audio generation adapter.\n *\n * Supports fal.ai audio models like diffrhythm (music), sound effects, etc.\n *\n * @example\n * ```typescript\n * const adapter = falAudio('fal-ai/diffrhythm')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An upbeat electronic track with synths',\n * duration: 10,\n * })\n * ```\n */\nexport class FalAudioAdapter<TModel extends FalModel> extends BaseAudioAdapter<\n TModel,\n FalAudioProviderOptions<TModel>\n> {\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super(model, {})\n configureFalClient(config)\n }\n\n async generateAudio(\n options: AudioGenerationOptions<FalAudioProviderOptions<TModel>>,\n ): Promise<AudioGenerationResult> {\n const { logger } = options\n logger.request(`activity=generateAudio provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n try {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateAudio fatal', {\n error,\n source: 'fal.generateAudio',\n })\n throw error\n }\n }\n\n private buildInput(\n options: AudioGenerationOptions<FalAudioProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n return {\n ...buildDurationFragment(this.model, options.duration),\n ...options.modelOptions,\n prompt: options.prompt,\n } as FalModelInput<TModel>\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): AudioGenerationResult {\n const data = response.data as Record<string, unknown>\n\n // fal returns { audio: { url, content_type } } or { audio_url: string }\n let audioUrl: string | undefined\n let contentType: string | undefined\n\n if (\n data['audio'] &&\n typeof data['audio'] === 'object' &&\n 'url' in data['audio']\n ) {\n const audioObj = data['audio'] as { url: string; content_type?: string }\n audioUrl = audioObj.url\n contentType = audioObj.content_type\n } else if (typeof data.audio_url === 'string') {\n audioUrl = data.audio_url\n }\n\n if (!audioUrl) {\n throw new Error('Audio URL not found in fal audio generation response')\n }\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: {\n url: audioUrl,\n contentType: deriveAudioContentType(contentType, audioUrl),\n },\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falAudio<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalAudioAdapter<TModel> {\n return new FalAudioAdapter(model, config)\n}\n"],"
|
|
1
|
+
{"version":3,"file":"audio.js","names":[],"sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n deriveAudioContentType,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils/client'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Provider options for audio generation, excluding fields TanStack AI handles.\n */\nexport type FalAudioProviderOptions<TModel extends string> = Omit<\n FalModelInput<TModel>,\n 'prompt'\n>\n\n/**\n * Fal audio models don't agree on the name for \"how long should this be.\"\n * Most accept seconds via `duration`, but a few use model-specific fields —\n * e.g. ElevenLabs wants milliseconds via `music_length_ms`, Stable Audio wants\n * `seconds_total`. Explicit per-model entries override the default; user\n * `modelOptions` still win over either.\n */\nconst DURATION_FRAGMENT_BUILDERS: Record<\n string,\n (seconds: number) => Record<string, unknown>\n> = {\n 'fal-ai/elevenlabs/music': (seconds) => ({ music_length_ms: seconds * 1000 }),\n 'fal-ai/stable-audio-25/text-to-audio': (seconds) => ({\n seconds_total: seconds,\n }),\n}\n\nfunction buildDurationFragment(\n model: string,\n duration: number | undefined,\n): Record<string, unknown> {\n if (duration == null) return {}\n const builder = DURATION_FRAGMENT_BUILDERS[model]\n return builder ? builder(duration) : { duration }\n}\n\n/**\n * fal.ai audio generation adapter.\n *\n * Supports fal.ai audio models like diffrhythm (music), sound effects, etc.\n *\n * @example\n * ```typescript\n * const adapter = falAudio('fal-ai/diffrhythm')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An upbeat electronic track with synths',\n * duration: 10,\n * })\n * ```\n */\nexport class FalAudioAdapter<TModel extends FalModel> extends BaseAudioAdapter<\n TModel,\n FalAudioProviderOptions<TModel>\n> {\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super(model, {})\n configureFalClient(config)\n }\n\n async generateAudio(\n options: AudioGenerationOptions<FalAudioProviderOptions<TModel>>,\n ): Promise<AudioGenerationResult> {\n const { logger } = options\n logger.request(`activity=generateAudio provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n try {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateAudio fatal', {\n error,\n source: 'fal.generateAudio',\n })\n throw error\n }\n }\n\n private buildInput(\n options: AudioGenerationOptions<FalAudioProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n return {\n ...buildDurationFragment(this.model, options.duration),\n ...options.modelOptions,\n prompt: options.prompt,\n } as FalModelInput<TModel>\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): AudioGenerationResult {\n const data = response.data as Record<string, unknown>\n\n // fal returns { audio: { url, content_type } } or { audio_url: string }\n let audioUrl: string | undefined\n let contentType: string | undefined\n\n if (\n data['audio'] &&\n typeof data['audio'] === 'object' &&\n 'url' in data['audio']\n ) {\n const audioObj = data['audio'] as { url: string; content_type?: string }\n audioUrl = audioObj.url\n contentType = audioObj.content_type\n } else if (typeof data.audio_url === 'string') {\n audioUrl = data.audio_url\n }\n\n if (!audioUrl) {\n throw new Error('Audio URL not found in fal audio generation response')\n }\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: {\n url: audioUrl,\n contentType: deriveAudioContentType(contentType, audioUrl),\n },\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falAudio<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalAudioAdapter<TModel> {\n return new FalAudioAdapter(model, config)\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,IAAM,6BAGF;CACF,4BAA4B,aAAa,EAAE,iBAAiB,UAAU,IAAK;CAC3E,yCAAyC,aAAa,EACpD,eAAe,QACjB;AACF;AAEA,SAAS,sBACP,OACA,UACyB;CACzB,IAAI,YAAY,MAAM,OAAO,CAAC;CAC9B,MAAM,UAAU,2BAA2B;CAC3C,OAAO,UAAU,QAAQ,QAAQ,IAAI,EAAE,SAAS;AAClD;;;;;;;;;;;;;;;;AAiBA,IAAa,kBAAb,cAA8D,iBAG5D;CACA,OAAgB;CAEhB,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,cACJ,SACgC;EAChC,MAAM,EAAE,WAAW;EACnB,OAAO,QAAQ,6CAA6C,KAAK,SAAS;GACxE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EACD,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,OAAO;GACrC,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,MAAM,CAAC;GACxD,OAAO,KAAK,kBAAkB,MAAM;EACtC,SAAS,OAAO;GACd,OAAO,OAAO,2BAA2B;IACvC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SACuB;EACvB,OAAO;GACL,GAAG,sBAAsB,KAAK,OAAO,QAAQ,QAAQ;GACrD,GAAG,QAAQ;GACX,QAAQ,QAAQ;EAClB;CACF;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,kBACE,UACuB;EACvB,MAAM,OAAO,SAAS;EAGtB,IAAI;EACJ,IAAI;EAEJ,IACE,KAAK,YACL,OAAO,KAAK,aAAa,YACzB,SAAS,KAAK,UACd;GACA,MAAM,WAAW,KAAK;GACtB,WAAW,SAAS;GACpB,cAAc,SAAS;EACzB,OAAO,IAAI,OAAO,KAAK,cAAc,UACnC,WAAW,KAAK;EAGlB,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,sDAAsD;EAGxE,MAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;EAEjE,OAAO;GACL,IAAI,SAAS,aAAa,KAAK,WAAW;GAC1C,OAAO,KAAK;GACZ,OAAO;IACL,KAAK;IACL,aAAa,uBAAuB,aAAa,QAAQ;GAC3D;GACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;AACF;AAEA,SAAgB,SACd,OACA,QACyB;CACzB,OAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C"}
|
|
@@ -1,115 +1,106 @@
|
|
|
1
|
-
import { fal } from "@fal-ai/client";
|
|
2
|
-
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
3
|
-
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
4
|
-
import { configureFalClient, generateId } from "../utils/client.js";
|
|
5
1
|
import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
|
|
2
|
+
import { configureFalClient, generateId } from "../utils/client.js";
|
|
6
3
|
import { mapSizeToFalFormat } from "../image/image-provider-options.js";
|
|
7
4
|
import { mapImageInputsToFalFields } from "../image/image-inputs.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
return { url };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
5
|
+
import { fal } from "@fal-ai/client";
|
|
6
|
+
import { resolveMediaPrompt } from "@tanstack/ai";
|
|
7
|
+
import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
8
|
+
//#region src/adapters/image.ts
|
|
9
|
+
/**
|
|
10
|
+
* fal.ai image generation adapter with full type inference.
|
|
11
|
+
*
|
|
12
|
+
* Uses fal.ai's comprehensive type system to provide autocomplete
|
|
13
|
+
*
|
|
14
|
+
* and type safety for all 600+ supported models.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const adapter = falImage('fal-ai/flux/dev')
|
|
19
|
+
* const result = await adapter.generateImages({
|
|
20
|
+
* model: 'fal-ai/flux/dev',
|
|
21
|
+
* prompt: 'a cat',
|
|
22
|
+
* modelOptions: {
|
|
23
|
+
* num_inference_steps: 28, // Type-safe! Autocomplete works
|
|
24
|
+
* guidance_scale: 3.5,
|
|
25
|
+
* },
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
var FalImageAdapter = class extends BaseImageAdapter {
|
|
30
|
+
kind = "image";
|
|
31
|
+
name = "fal";
|
|
32
|
+
constructor(model, config) {
|
|
33
|
+
super(model, {});
|
|
34
|
+
configureFalClient(config);
|
|
35
|
+
}
|
|
36
|
+
async generateImages(options) {
|
|
37
|
+
const { logger } = options;
|
|
38
|
+
logger.request(`activity=generateImage provider=fal model=${this.model}`, {
|
|
39
|
+
provider: "fal",
|
|
40
|
+
model: this.model
|
|
41
|
+
});
|
|
42
|
+
const resolved = resolveMediaPrompt(options.prompt);
|
|
43
|
+
if (resolved.videos.length > 0) throw new Error(`fal.generateImages does not support video prompt parts on model ${this.model}.`);
|
|
44
|
+
if (resolved.audios.length > 0) throw new Error(`fal.generateImages does not support audio prompt parts on model ${this.model}.`);
|
|
45
|
+
try {
|
|
46
|
+
const input = this.buildInput(options, resolved);
|
|
47
|
+
const result = await fal.subscribe(this.model, { input });
|
|
48
|
+
return this.transformResponse(result);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.errors("fal.generateImage fatal", {
|
|
51
|
+
error,
|
|
52
|
+
source: "fal.generateImage"
|
|
53
|
+
});
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
buildInput(options, resolved) {
|
|
58
|
+
const sizeParams = mapSizeToFalFormat(options.size);
|
|
59
|
+
const inputFields = mapImageInputsToFalFields(this.model, resolved.images);
|
|
60
|
+
return {
|
|
61
|
+
...sizeParams,
|
|
62
|
+
...inputFields,
|
|
63
|
+
...options.modelOptions,
|
|
64
|
+
...resolved.text ? { prompt: resolved.text } : {},
|
|
65
|
+
num_images: options.numberOfImages
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
generateId() {
|
|
69
|
+
return generateId(this.name);
|
|
70
|
+
}
|
|
71
|
+
transformResponse(response) {
|
|
72
|
+
const images = [];
|
|
73
|
+
const data = response.data;
|
|
74
|
+
if ("images" in data && Array.isArray(data.images)) for (const img of data.images) images.push(this.parseImage(img));
|
|
75
|
+
else if ("image" in data && data.image && typeof data.image === "object") images.push(this.parseImage(data.image));
|
|
76
|
+
if (images.length === 0) throw new Error("Unexpected fal image response shape. Expected images[] or image{}. Got keys: " + Object.keys(data).join(","));
|
|
77
|
+
const usage = buildFalUsage(takeBillableUnits(response.requestId));
|
|
78
|
+
return {
|
|
79
|
+
id: response.requestId || this.generateId(),
|
|
80
|
+
model: this.model,
|
|
81
|
+
images,
|
|
82
|
+
...usage ? { usage } : {}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
parseImage(img) {
|
|
86
|
+
let url;
|
|
87
|
+
if (typeof img === "string") url = img;
|
|
88
|
+
else if (img && typeof img === "object" && "url" in img && typeof img.url === "string") url = img.url;
|
|
89
|
+
else throw new Error(`Invalid image payload from fal response: expected string or { url: string }, received ${img === null ? "null" : typeof img}`);
|
|
90
|
+
if (url.startsWith("data:")) {
|
|
91
|
+
const base64Match = url.match(/^data:image\/[^;]+;base64,(.+)$/);
|
|
92
|
+
if (base64Match && base64Match[1]) return { b64Json: base64Match[1] };
|
|
93
|
+
}
|
|
94
|
+
return { url };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
104
97
|
function createFalImage(model, config) {
|
|
105
|
-
|
|
98
|
+
return new FalImageAdapter(model, config);
|
|
106
99
|
}
|
|
107
100
|
function falImage(model, config) {
|
|
108
|
-
|
|
101
|
+
return createFalImage(model, config);
|
|
109
102
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
};
|
|
115
|
-
//# sourceMappingURL=image.js.map
|
|
103
|
+
//#endregion
|
|
104
|
+
export { FalImageAdapter, createFalImage, falImage };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport { mapImageInputsToFalFields } from '../image/image-inputs'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils/client'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type {\n FalImagePromptModalitiesFor,\n FalImageProviderOptions,\n FalModel,\n FalModelImageSize,\n FalModelInput,\n} from '../model-meta'\n\n/**\n * fal.ai image generation adapter with full type inference.\n *\n * Uses fal.ai's comprehensive type system to provide autocomplete\n *\n * and type safety for all 600+ supported models.\n *\n * @example\n * ```typescript\n * const adapter = falImage('fal-ai/flux/dev')\n * const result = await adapter.generateImages({\n * model: 'fal-ai/flux/dev',\n * prompt: 'a cat',\n * modelOptions: {\n * num_inference_steps: 28, // Type-safe! Autocomplete works\n * guidance_scale: 3.5,\n * },\n * })\n * ```\n */\nexport class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<\n TModel,\n FalImageProviderOptions<TModel>,\n Record<TModel, FalImageProviderOptions<TModel>>,\n Record<TModel, FalModelImageSize<TModel>>,\n Record<TModel, FalImagePromptModalitiesFor<TModel>>\n> {\n override readonly kind = 'image' 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 generateImages(\n options: ImageGenerationOptions<\n FalImageProviderOptions<TModel>,\n FalModelImageSize<TModel>\n >,\n ): Promise<ImageGenerationResult> {\n const { logger } = options\n\n logger.request(`activity=generateImage provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `fal.generateImages does not support video prompt parts on model ${this.model}.`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `fal.generateImages does not support audio prompt parts on model ${this.model}.`,\n )\n }\n\n try {\n const input = this.buildInput(options, resolved)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateImage fatal', {\n error,\n source: 'fal.generateImage',\n })\n throw error\n }\n }\n\n private buildInput(\n options: ImageGenerationOptions<\n FalImageProviderOptions<TModel>,\n FalModelImageSize<TModel>\n >,\n resolved: ResolvedMediaPrompt,\n ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n // Order matters: size and derived image-input fields first, then\n // modelOptions (so explicit user overrides win for mask_url /\n // control_image_url / reference_image_urls), then the call-controlled\n // prompt / num_images, which always take precedence.\n const inputFields = mapImageInputsToFalFields(this.model, resolved.images)\n const input = {\n ...sizeParams,\n ...inputFields,\n ...options.modelOptions,\n // Media-only prompts (e.g. upscalers, background removal) omit the\n // prompt field entirely rather than sending an empty string.\n ...(resolved.text ? { prompt: resolved.text } : {}),\n num_images: options.numberOfImages,\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const data = response.data\n\n // Handle array of images (most models return { images: [...] })\n if ('images' in data && Array.isArray(data.images)) {\n for (const img of data.images) {\n images.push(this.parseImage(img))\n }\n }\n // Handle single image response (some models return { image: {...} })\n else if ('image' in data && data.image && typeof data.image === 'object') {\n images.push(this.parseImage(data.image))\n }\n\n if (images.length === 0) {\n throw new Error(\n 'Unexpected fal image response shape. Expected images[] or image{}. Got keys: ' +\n Object.keys(data).join(','),\n )\n }\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n ...(usage ? { usage } : {}),\n }\n }\n\n private parseImage(img: unknown): GeneratedImage {\n let url: string\n if (typeof img === 'string') {\n url = img\n } else if (\n img &&\n typeof img === 'object' &&\n 'url' in img &&\n typeof img.url === 'string'\n ) {\n url = (img as { url: string }).url\n } else {\n throw new Error(\n `Invalid image payload from fal response: expected string or { url: string }, received ${\n img === null ? 'null' : typeof img\n }`,\n )\n }\n\n if (url.startsWith('data:')) {\n const base64Match = url.match(/^data:image\\/[^;]+;base64,(.+)$/)\n if (base64Match && base64Match[1]) {\n return { b64Json: base64Match[1] }\n }\n }\n return { url }\n }\n}\n\nexport function createFalImage<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalImageAdapter<TModel> {\n return new FalImageAdapter(model, config)\n}\n\nexport function falImage<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalImageAdapter<TModel> {\n return createFalImage(model, config)\n}\n"],"
|
|
1
|
+
{"version":3,"file":"image.js","names":[],"sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n configureFalClient,\n generateId as utilGenerateId,\n} from '../utils/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport { mapImageInputsToFalFields } from '../image/image-inputs'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils/client'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n ResolvedMediaPrompt,\n} from '@tanstack/ai'\nimport type {\n FalImagePromptModalitiesFor,\n FalImageProviderOptions,\n FalModel,\n FalModelImageSize,\n FalModelInput,\n} from '../model-meta'\n\n/**\n * fal.ai image generation adapter with full type inference.\n *\n * Uses fal.ai's comprehensive type system to provide autocomplete\n *\n * and type safety for all 600+ supported models.\n *\n * @example\n * ```typescript\n * const adapter = falImage('fal-ai/flux/dev')\n * const result = await adapter.generateImages({\n * model: 'fal-ai/flux/dev',\n * prompt: 'a cat',\n * modelOptions: {\n * num_inference_steps: 28, // Type-safe! Autocomplete works\n * guidance_scale: 3.5,\n * },\n * })\n * ```\n */\nexport class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<\n TModel,\n FalImageProviderOptions<TModel>,\n Record<TModel, FalImageProviderOptions<TModel>>,\n Record<TModel, FalModelImageSize<TModel>>,\n Record<TModel, FalImagePromptModalitiesFor<TModel>>\n> {\n override readonly kind = 'image' 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 generateImages(\n options: ImageGenerationOptions<\n FalImageProviderOptions<TModel>,\n FalModelImageSize<TModel>\n >,\n ): Promise<ImageGenerationResult> {\n const { logger } = options\n\n logger.request(`activity=generateImage provider=fal model=${this.model}`, {\n provider: 'fal',\n model: this.model,\n })\n\n const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `fal.generateImages does not support video prompt parts on model ${this.model}.`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `fal.generateImages does not support audio prompt parts on model ${this.model}.`,\n )\n }\n\n try {\n const input = this.buildInput(options, resolved)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateImage fatal', {\n error,\n source: 'fal.generateImage',\n })\n throw error\n }\n }\n\n private buildInput(\n options: ImageGenerationOptions<\n FalImageProviderOptions<TModel>,\n FalModelImageSize<TModel>\n >,\n resolved: ResolvedMediaPrompt,\n ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n // Order matters: size and derived image-input fields first, then\n // modelOptions (so explicit user overrides win for mask_url /\n // control_image_url / reference_image_urls), then the call-controlled\n // prompt / num_images, which always take precedence.\n const inputFields = mapImageInputsToFalFields(this.model, resolved.images)\n const input = {\n ...sizeParams,\n ...inputFields,\n ...options.modelOptions,\n // Media-only prompts (e.g. upscalers, background removal) omit the\n // prompt field entirely rather than sending an empty string.\n ...(resolved.text ? { prompt: resolved.text } : {}),\n num_images: options.numberOfImages,\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const data = response.data\n\n // Handle array of images (most models return { images: [...] })\n if ('images' in data && Array.isArray(data.images)) {\n for (const img of data.images) {\n images.push(this.parseImage(img))\n }\n }\n // Handle single image response (some models return { image: {...} })\n else if ('image' in data && data.image && typeof data.image === 'object') {\n images.push(this.parseImage(data.image))\n }\n\n if (images.length === 0) {\n throw new Error(\n 'Unexpected fal image response shape. Expected images[] or image{}. Got keys: ' +\n Object.keys(data).join(','),\n )\n }\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\n ...(usage ? { usage } : {}),\n }\n }\n\n private parseImage(img: unknown): GeneratedImage {\n let url: string\n if (typeof img === 'string') {\n url = img\n } else if (\n img &&\n typeof img === 'object' &&\n 'url' in img &&\n typeof img.url === 'string'\n ) {\n url = (img as { url: string }).url\n } else {\n throw new Error(\n `Invalid image payload from fal response: expected string or { url: string }, received ${\n img === null ? 'null' : typeof img\n }`,\n )\n }\n\n if (url.startsWith('data:')) {\n const base64Match = url.match(/^data:image\\/[^;]+;base64,(.+)$/)\n if (base64Match && base64Match[1]) {\n return { b64Json: base64Match[1] }\n }\n }\n return { url }\n }\n}\n\nexport function createFalImage<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalImageAdapter<TModel> {\n return new FalImageAdapter(model, config)\n}\n\nexport function falImage<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalImageAdapter<TModel> {\n return createFalImage(model, config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,kBAAb,cAA8D,iBAM5D;CACA,OAAyB;CACzB,OAAgB;CAEhB,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,eACJ,SAIgC;EAChC,MAAM,EAAE,WAAW;EAEnB,OAAO,QAAQ,6CAA6C,KAAK,SAAS;GACxE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EAED,MAAM,WAAW,mBAAmB,QAAQ,MAAM;EAElD,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,mEAAmE,KAAK,MAAM,EAChF;EAEF,IAAI,SAAS,OAAO,SAAS,GAC3B,MAAM,IAAI,MACR,mEAAmE,KAAK,MAAM,EAChF;EAGF,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,SAAS,QAAQ;GAC/C,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,MAAM,CAAC;GACxD,OAAO,KAAK,kBAAkB,MAAM;EACtC,SAAS,OAAO;GACd,OAAO,OAAO,2BAA2B;IACvC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SAIA,UACuB;EACvB,MAAM,aAAa,mBAAmB,QAAQ,IAAI;EAKlD,MAAM,cAAc,0BAA0B,KAAK,OAAO,SAAS,MAAM;EAUzE,OAAO;GARL,GAAG;GACH,GAAG;GACH,GAAG,QAAQ;GAGX,GAAI,SAAS,OAAO,EAAE,QAAQ,SAAS,KAAK,IAAI,CAAC;GACjD,YAAY,QAAQ;EAEf;CACT;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,kBACE,UACuB;EACvB,MAAM,SAAgC,CAAC;EACvC,MAAM,OAAO,SAAS;EAGtB,IAAI,YAAY,QAAQ,MAAM,QAAQ,KAAK,MAAM,GAC/C,KAAK,MAAM,OAAO,KAAK,QACrB,OAAO,KAAK,KAAK,WAAW,GAAG,CAAC;OAI/B,IAAI,WAAW,QAAQ,KAAK,SAAS,OAAO,KAAK,UAAU,UAC9D,OAAO,KAAK,KAAK,WAAW,KAAK,KAAK,CAAC;EAGzC,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MACR,kFACE,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,CAC9B;EAGF,MAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;EAEjE,OAAO;GACL,IAAI,SAAS,aAAa,KAAK,WAAW;GAC1C,OAAO,KAAK;GACZ;GACA,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;CAEA,WAAmB,KAA8B;EAC/C,IAAI;EACJ,IAAI,OAAO,QAAQ,UACjB,MAAM;OACD,IACL,OACA,OAAO,QAAQ,YACf,SAAS,OACT,OAAO,IAAI,QAAQ,UAEnB,MAAO,IAAwB;OAE/B,MAAM,IAAI,MACR,yFACE,QAAQ,OAAO,SAAS,OAAO,KAEnC;EAGF,IAAI,IAAI,WAAW,OAAO,GAAG;GAC3B,MAAM,cAAc,IAAI,MAAM,iCAAiC;GAC/D,IAAI,eAAe,YAAY,IAC7B,OAAO,EAAE,SAAS,YAAY,GAAG;EAErC;EACA,OAAO,EAAE,IAAI;CACf;AACF;AAEA,SAAgB,eACd,OACA,QACyB;CACzB,OAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAEA,SAAgB,SACd,OACA,QACyB;CACzB,OAAO,eAAe,OAAO,MAAM;AACrC"}
|