@tanstack/ai-fal 0.9.5 → 0.9.6

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.
Files changed (39) hide show
  1. package/dist/esm/adapters/audio.d.ts +32 -0
  2. package/dist/esm/adapters/audio.js +83 -0
  3. package/dist/esm/adapters/audio.js.map +1 -0
  4. package/dist/esm/adapters/image.d.ts +36 -0
  5. package/dist/esm/adapters/image.js +115 -0
  6. package/dist/esm/adapters/image.js.map +1 -0
  7. package/dist/esm/adapters/speech.d.ts +32 -0
  8. package/dist/esm/adapters/speech.js +87 -0
  9. package/dist/esm/adapters/speech.js.map +1 -0
  10. package/dist/esm/adapters/transcription.d.ts +32 -0
  11. package/dist/esm/adapters/transcription.js +93 -0
  12. package/dist/esm/adapters/transcription.js.map +1 -0
  13. package/dist/esm/adapters/video.d.ts +34 -0
  14. package/dist/esm/adapters/video.js +165 -0
  15. package/dist/esm/adapters/video.js.map +1 -0
  16. package/dist/esm/image/generated/image-field-overrides.d.ts +1279 -0
  17. package/dist/esm/image/generated/image-field-overrides.js +368 -0
  18. package/dist/esm/image/generated/image-field-overrides.js.map +1 -0
  19. package/dist/esm/image/image-inputs.d.ts +50 -0
  20. package/dist/esm/image/image-inputs.js +119 -0
  21. package/dist/esm/image/image-inputs.js.map +1 -0
  22. package/dist/esm/image/image-provider-options.d.ts +2 -0
  23. package/dist/esm/image/image-provider-options.js +23 -0
  24. package/dist/esm/image/image-provider-options.js.map +1 -0
  25. package/dist/esm/index.d.ts +7 -0
  26. package/dist/esm/index.js +22 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/model-meta.d.ts +129 -0
  29. package/dist/esm/utils/billing.d.ts +39 -0
  30. package/dist/esm/utils/billing.js +55 -0
  31. package/dist/esm/utils/billing.js.map +1 -0
  32. package/dist/esm/utils/client.d.ts +45 -0
  33. package/dist/esm/utils/client.js +101 -0
  34. package/dist/esm/utils/client.js.map +1 -0
  35. package/dist/esm/utils/index.d.ts +2 -0
  36. package/dist/esm/video/video-provider-options.d.ts +2 -0
  37. package/dist/esm/video/video-provider-options.js +18 -0
  38. package/dist/esm/video/video-provider-options.js.map +1 -0
  39. package/package.json +4 -4
@@ -0,0 +1,32 @@
1
+ import { BaseAudioAdapter } from '@tanstack/ai/adapters';
2
+ import { AudioGenerationOptions, AudioGenerationResult } from '@tanstack/ai';
3
+ import { FalClientConfig } from '../utils.js';
4
+ import { FalModel, FalModelInput } from '../model-meta.js';
5
+ /**
6
+ * Provider options for audio generation, excluding fields TanStack AI handles.
7
+ */
8
+ export type FalAudioProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'prompt'>;
9
+ /**
10
+ * fal.ai audio generation adapter.
11
+ *
12
+ * Supports fal.ai audio models like diffrhythm (music), sound effects, etc.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const adapter = falAudio('fal-ai/diffrhythm')
17
+ * const result = await generateAudio({
18
+ * adapter,
19
+ * prompt: 'An upbeat electronic track with synths',
20
+ * duration: 10,
21
+ * })
22
+ * ```
23
+ */
24
+ export declare class FalAudioAdapter<TModel extends FalModel> extends BaseAudioAdapter<TModel, FalAudioProviderOptions<TModel>> {
25
+ readonly name: "fal";
26
+ constructor(model: TModel, config?: FalClientConfig);
27
+ generateAudio(options: AudioGenerationOptions<FalAudioProviderOptions<TModel>>): Promise<AudioGenerationResult>;
28
+ private buildInput;
29
+ protected generateId(): string;
30
+ private transformResponse;
31
+ }
32
+ export declare function falAudio<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalAudioAdapter<TModel>;
@@ -0,0 +1,83 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import { BaseAudioAdapter } from "@tanstack/ai/adapters";
3
+ import { configureFalClient, generateId, deriveAudioContentType } from "../utils/client.js";
4
+ import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
5
+ const DURATION_FRAGMENT_BUILDERS = {
6
+ "fal-ai/elevenlabs/music": (seconds) => ({ music_length_ms: seconds * 1e3 }),
7
+ "fal-ai/stable-audio-25/text-to-audio": (seconds) => ({
8
+ seconds_total: seconds
9
+ })
10
+ };
11
+ function buildDurationFragment(model, duration) {
12
+ if (duration == null) return {};
13
+ const builder = DURATION_FRAGMENT_BUILDERS[model];
14
+ return builder ? builder(duration) : { duration };
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
+ }
75
+ }
76
+ function falAudio(model, config) {
77
+ return new FalAudioAdapter(model, config);
78
+ }
79
+ export {
80
+ FalAudioAdapter,
81
+ falAudio
82
+ };
83
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +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 buildFalUsage,\n configureFalClient,\n deriveAudioContentType,\n takeBillableUnits,\n generateId as utilGenerateId,\n} from '../utils'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils'\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"],"names":["utilGenerateId"],"mappings":";;;;AAgCA,MAAM,6BAGF;AAAA,EACF,2BAA2B,CAAC,aAAa,EAAE,iBAAiB,UAAU,IAAA;AAAA,EACtE,wCAAwC,CAAC,aAAa;AAAA,IACpD,eAAe;AAAA,EAAA;AAEnB;AAEA,SAAS,sBACP,OACA,UACyB;AACzB,MAAI,YAAY,KAAM,QAAO,CAAA;AAC7B,QAAM,UAAU,2BAA2B,KAAK;AAChD,SAAO,UAAU,QAAQ,QAAQ,IAAI,EAAE,SAAA;AACzC;AAiBO,MAAM,wBAAiD,iBAG5D;AAAA,EACS,OAAO;AAAA,EAEhB,YAAY,OAAe,QAA0B;AACnD,UAAM,OAAO,EAAE;AACf,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,cACJ,SACgC;AAChC,UAAM,EAAE,WAAW;AACnB,WAAO,QAAQ,6CAA6C,KAAK,KAAK,IAAI;AAAA,MACxE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AACD,QAAI;AACF,YAAM,QAAQ,KAAK,WAAW,OAAO;AACrC,YAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,OAAO;AACxD,aAAO,KAAK,kBAAkB,MAAM;AAAA,IACtC,SAAS,OAAO;AACd,aAAO,OAAO,2BAA2B;AAAA,QACvC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,WACN,SACuB;AACvB,WAAO;AAAA,MACL,GAAG,sBAAsB,KAAK,OAAO,QAAQ,QAAQ;AAAA,MACrD,GAAG,QAAQ;AAAA,MACX,QAAQ,QAAQ;AAAA,IAAA;AAAA,EAEpB;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AAAA,EAEQ,kBACN,UACuB;AACvB,UAAM,OAAO,SAAS;AAGtB,QAAI;AACJ,QAAI;AAEJ,QACE,KAAK,OAAO,KACZ,OAAO,KAAK,OAAO,MAAM,YACzB,SAAS,KAAK,OAAO,GACrB;AACA,YAAM,WAAW,KAAK,OAAO;AAC7B,iBAAW,SAAS;AACpB,oBAAc,SAAS;AAAA,IACzB,WAAW,OAAO,KAAK,cAAc,UAAU;AAC7C,iBAAW,KAAK;AAAA,IAClB;AAEA,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,sDAAsD;AAAA,IACxE;AAEA,UAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;AAEjE,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ,OAAO;AAAA,QACL,KAAK;AAAA,QACL,aAAa,uBAAuB,aAAa,QAAQ;AAAA,MAAA;AAAA,MAE3D,GAAI,QAAQ,EAAE,UAAU,CAAA;AAAA,IAAC;AAAA,EAE7B;AACF;AAEO,SAAS,SACd,OACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;"}
@@ -0,0 +1,36 @@
1
+ import { BaseImageAdapter } from '@tanstack/ai/adapters';
2
+ import { FalClientConfig } from '../utils.js';
3
+ import { ImageGenerationOptions, ImageGenerationResult } from '@tanstack/ai';
4
+ import { FalImagePromptModalitiesFor, FalImageProviderOptions, FalModel, FalModelImageSize } from '../model-meta.js';
5
+ /**
6
+ * fal.ai image generation adapter with full type inference.
7
+ *
8
+ * Uses fal.ai's comprehensive type system to provide autocomplete
9
+ *
10
+ * and type safety for all 600+ supported models.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const adapter = falImage('fal-ai/flux/dev')
15
+ * const result = await adapter.generateImages({
16
+ * model: 'fal-ai/flux/dev',
17
+ * prompt: 'a cat',
18
+ * modelOptions: {
19
+ * num_inference_steps: 28, // Type-safe! Autocomplete works
20
+ * guidance_scale: 3.5,
21
+ * },
22
+ * })
23
+ * ```
24
+ */
25
+ export declare class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<TModel, FalImageProviderOptions<TModel>, Record<TModel, FalImageProviderOptions<TModel>>, Record<TModel, FalModelImageSize<TModel>>, Record<TModel, FalImagePromptModalitiesFor<TModel>>> {
26
+ readonly kind: "image";
27
+ readonly name: "fal";
28
+ constructor(model: TModel, config?: FalClientConfig);
29
+ generateImages(options: ImageGenerationOptions<FalImageProviderOptions<TModel>, FalModelImageSize<TModel>>): Promise<ImageGenerationResult>;
30
+ private buildInput;
31
+ protected generateId(): string;
32
+ private transformResponse;
33
+ private parseImage;
34
+ }
35
+ export declare function createFalImage<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalImageAdapter<TModel>;
36
+ export declare function falImage<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalImageAdapter<TModel>;
@@ -0,0 +1,115 @@
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
+ import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
6
+ import { mapSizeToFalFormat } from "../image/image-provider-options.js";
7
+ import { mapImageInputsToFalFields } from "../image/image-inputs.js";
8
+ class FalImageAdapter extends BaseImageAdapter {
9
+ kind = "image";
10
+ name = "fal";
11
+ constructor(model, config) {
12
+ super(model, {});
13
+ configureFalClient(config);
14
+ }
15
+ async generateImages(options) {
16
+ const { logger } = options;
17
+ logger.request(`activity=generateImage provider=fal model=${this.model}`, {
18
+ provider: "fal",
19
+ model: this.model
20
+ });
21
+ const resolved = resolveMediaPrompt(options.prompt);
22
+ if (resolved.videos.length > 0) {
23
+ throw new Error(
24
+ `fal.generateImages does not support video prompt parts on model ${this.model}.`
25
+ );
26
+ }
27
+ if (resolved.audios.length > 0) {
28
+ throw new Error(
29
+ `fal.generateImages does not support audio prompt parts on model ${this.model}.`
30
+ );
31
+ }
32
+ try {
33
+ const input = this.buildInput(options, resolved);
34
+ const result = await fal.subscribe(this.model, { input });
35
+ return this.transformResponse(result);
36
+ } catch (error) {
37
+ logger.errors("fal.generateImage fatal", {
38
+ error,
39
+ source: "fal.generateImage"
40
+ });
41
+ throw error;
42
+ }
43
+ }
44
+ buildInput(options, resolved) {
45
+ const sizeParams = mapSizeToFalFormat(options.size);
46
+ const inputFields = mapImageInputsToFalFields(this.model, resolved.images);
47
+ const input = {
48
+ ...sizeParams,
49
+ ...inputFields,
50
+ ...options.modelOptions,
51
+ // Media-only prompts (e.g. upscalers, background removal) omit the
52
+ // prompt field entirely rather than sending an empty string.
53
+ ...resolved.text ? { prompt: resolved.text } : {},
54
+ num_images: options.numberOfImages
55
+ };
56
+ return input;
57
+ }
58
+ generateId() {
59
+ return generateId(this.name);
60
+ }
61
+ transformResponse(response) {
62
+ const images = [];
63
+ const data = response.data;
64
+ if ("images" in data && Array.isArray(data.images)) {
65
+ for (const img of data.images) {
66
+ images.push(this.parseImage(img));
67
+ }
68
+ } else if ("image" in data && data.image && typeof data.image === "object") {
69
+ images.push(this.parseImage(data.image));
70
+ }
71
+ if (images.length === 0) {
72
+ throw new Error(
73
+ "Unexpected fal image response shape. Expected images[] or image{}. Got keys: " + Object.keys(data).join(",")
74
+ );
75
+ }
76
+ const usage = buildFalUsage(takeBillableUnits(response.requestId));
77
+ return {
78
+ id: response.requestId || this.generateId(),
79
+ model: this.model,
80
+ images,
81
+ ...usage ? { usage } : {}
82
+ };
83
+ }
84
+ parseImage(img) {
85
+ let url;
86
+ if (typeof img === "string") {
87
+ url = img;
88
+ } else if (img && typeof img === "object" && "url" in img && typeof img.url === "string") {
89
+ url = img.url;
90
+ } else {
91
+ throw new Error(
92
+ `Invalid image payload from fal response: expected string or { url: string }, received ${img === null ? "null" : typeof img}`
93
+ );
94
+ }
95
+ if (url.startsWith("data:")) {
96
+ const base64Match = url.match(/^data:image\/[^;]+;base64,(.+)$/);
97
+ if (base64Match && base64Match[1]) {
98
+ return { b64Json: base64Match[1] };
99
+ }
100
+ }
101
+ return { url };
102
+ }
103
+ }
104
+ function createFalImage(model, config) {
105
+ return new FalImageAdapter(model, config);
106
+ }
107
+ function falImage(model, config) {
108
+ return createFalImage(model, config);
109
+ }
110
+ export {
111
+ FalImageAdapter,
112
+ createFalImage,
113
+ falImage
114
+ };
115
+ //# sourceMappingURL=image.js.map
@@ -0,0 +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 buildFalUsage,\n configureFalClient,\n takeBillableUnits,\n generateId as utilGenerateId,\n} from '../utils'\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'\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"],"names":["utilGenerateId"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { BaseTTSAdapter } from '@tanstack/ai/adapters';
2
+ import { TTSOptions, TTSResult } from '@tanstack/ai';
3
+ import { FalClientConfig } from '../utils.js';
4
+ import { FalModel, FalModelInput } from '../model-meta.js';
5
+ /**
6
+ * Provider options for TTS, excluding fields TanStack AI handles.
7
+ */
8
+ export type FalSpeechProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'prompt' | 'text'>;
9
+ /**
10
+ * fal.ai text-to-speech adapter.
11
+ *
12
+ * Supports fal.ai TTS models like kokoro, elevenlabs, etc.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const adapter = falSpeech('fal-ai/kokoro/american-english')
17
+ * const result = await generateSpeech({
18
+ * adapter,
19
+ * text: 'Hello, world!',
20
+ * voice: 'af_heart',
21
+ * })
22
+ * ```
23
+ */
24
+ export declare class FalSpeechAdapter<TModel extends FalModel> extends BaseTTSAdapter<TModel, FalSpeechProviderOptions<TModel>> {
25
+ readonly name: "fal";
26
+ constructor(model: TModel, config?: FalClientConfig);
27
+ generateSpeech(options: TTSOptions<FalSpeechProviderOptions<TModel>>): Promise<TTSResult>;
28
+ private buildInput;
29
+ protected generateId(): string;
30
+ private transformResponse;
31
+ }
32
+ export declare function falSpeech<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalSpeechAdapter<TModel>;
@@ -0,0 +1,87 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import { BaseTTSAdapter } from "@tanstack/ai/adapters";
3
+ import { configureFalClient, generateId, arrayBufferToBase64, extractUrlExtension } from "../utils/client.js";
4
+ import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
5
+ class FalSpeechAdapter extends BaseTTSAdapter {
6
+ name = "fal";
7
+ constructor(model, config) {
8
+ super(model, {});
9
+ configureFalClient(config);
10
+ }
11
+ async generateSpeech(options) {
12
+ const { logger } = options;
13
+ logger.request(`activity=generateSpeech provider=fal model=${this.model}`, {
14
+ provider: "fal",
15
+ model: this.model
16
+ });
17
+ try {
18
+ const input = this.buildInput(options);
19
+ const result = await fal.subscribe(this.model, { input });
20
+ return await this.transformResponse(result);
21
+ } catch (error) {
22
+ logger.errors("fal.generateSpeech fatal", {
23
+ error,
24
+ source: "fal.generateSpeech"
25
+ });
26
+ throw error;
27
+ }
28
+ }
29
+ buildInput(options) {
30
+ const input = {
31
+ ...options.modelOptions,
32
+ // Map text to both prompt and text fields (different models use different fields)
33
+ prompt: options.text,
34
+ text: options.text,
35
+ ...options.voice ? { voice: options.voice } : {},
36
+ ...options.speed ? { speed: options.speed } : {}
37
+ };
38
+ return input;
39
+ }
40
+ generateId() {
41
+ return generateId(this.name);
42
+ }
43
+ async transformResponse(response) {
44
+ const data = response.data;
45
+ let audioUrl;
46
+ let contentType;
47
+ if (data["audio"] && typeof data["audio"] === "object" && "url" in data["audio"]) {
48
+ const audioObj = data["audio"];
49
+ audioUrl = audioObj.url;
50
+ contentType = audioObj.content_type;
51
+ } else if (typeof data.audio_url === "string") {
52
+ audioUrl = data.audio_url;
53
+ }
54
+ if (!audioUrl) {
55
+ throw new Error("Audio URL not found in fal TTS response");
56
+ }
57
+ const audioResponse = await fetch(audioUrl);
58
+ if (!audioResponse.ok) {
59
+ throw new Error(
60
+ `Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`
61
+ );
62
+ }
63
+ const arrayBuffer = await audioResponse.arrayBuffer();
64
+ const base64 = arrayBufferToBase64(arrayBuffer);
65
+ const contentTypeMime = contentType?.split(";")[0]?.trim();
66
+ const safeUrlExtension = extractUrlExtension(audioUrl);
67
+ const rawFormat = safeUrlExtension || contentTypeMime?.split("/")[1] || "wav";
68
+ const format = rawFormat === "mpeg" ? "mp3" : rawFormat;
69
+ const usage = buildFalUsage(takeBillableUnits(response.requestId));
70
+ return {
71
+ id: response.requestId || this.generateId(),
72
+ model: this.model,
73
+ audio: base64,
74
+ format,
75
+ contentType: contentTypeMime || `audio/${format}`,
76
+ ...usage ? { usage } : {}
77
+ };
78
+ }
79
+ }
80
+ function falSpeech(model, config) {
81
+ return new FalSpeechAdapter(model, config);
82
+ }
83
+ export {
84
+ FalSpeechAdapter,
85
+ falSpeech
86
+ };
87
+ //# sourceMappingURL=speech.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.js","sources":["../../../src/adapters/speech.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n arrayBufferToBase64,\n buildFalUsage,\n configureFalClient,\n extractUrlExtension,\n takeBillableUnits,\n generateId as utilGenerateId,\n} from '../utils'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Provider options for TTS, excluding fields TanStack AI handles.\n */\nexport type FalSpeechProviderOptions<TModel extends string> = Omit<\n FalModelInput<TModel>,\n 'prompt' | 'text'\n>\n\n/**\n * fal.ai text-to-speech adapter.\n *\n * Supports fal.ai TTS models like kokoro, elevenlabs, etc.\n *\n * @example\n * ```typescript\n * const adapter = falSpeech('fal-ai/kokoro/american-english')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!',\n * voice: 'af_heart',\n * })\n * ```\n */\nexport class FalSpeechAdapter<TModel extends FalModel> extends BaseTTSAdapter<\n TModel,\n FalSpeechProviderOptions<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 generateSpeech(\n options: TTSOptions<FalSpeechProviderOptions<TModel>>,\n ): Promise<TTSResult> {\n const { logger } = options\n logger.request(`activity=generateSpeech 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 await this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateSpeech fatal', {\n error,\n source: 'fal.generateSpeech',\n })\n throw error\n }\n }\n\n private buildInput(\n options: TTSOptions<FalSpeechProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n const input = {\n ...options.modelOptions,\n // Map text to both prompt and text fields (different models use different fields)\n prompt: options.text,\n text: options.text,\n ...(options.voice ? { voice: options.voice } : {}),\n ...(options.speed ? { speed: options.speed } : {}),\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private async transformResponse(\n response: Result<OutputType<TModel>>,\n ): Promise<TTSResult> {\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 TTS response')\n }\n\n // Fetch the audio and convert to base64 to match TTSResult contract.\n // Using a chunked helper here — spreading Uint8Array into btoa exceeds\n // V8's argument limit (~65k) for any realistic TTS clip.\n const audioResponse = await fetch(audioUrl)\n if (!audioResponse.ok) {\n throw new Error(\n `Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`,\n )\n }\n const arrayBuffer = await audioResponse.arrayBuffer()\n const base64 = arrayBufferToBase64(arrayBuffer)\n\n // Strip parameters like `; charset=...` from contentType, and only use\n // the URL extension as a fallback when it looks like a real extension.\n const contentTypeMime = contentType?.split(';')[0]?.trim()\n const safeUrlExtension = extractUrlExtension(audioUrl)\n // Prefer URL-derived extension when available (more canonical for file\n // consumers), otherwise derive from the content-type mime subtype, then\n // fall back to `wav`. Normalize `mpeg` → `mp3` so the format field is a\n // usable file extension rather than the IANA subtype.\n const rawFormat =\n safeUrlExtension || contentTypeMime?.split('/')[1] || 'wav'\n const format = rawFormat === 'mpeg' ? 'mp3' : rawFormat\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: base64,\n format,\n contentType: contentTypeMime || `audio/${format}`,\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falSpeech<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalSpeechAdapter<TModel> {\n return new FalSpeechAdapter(model, config)\n}\n"],"names":["utilGenerateId"],"mappings":";;;;AAsCO,MAAM,yBAAkD,eAG7D;AAAA,EACS,OAAO;AAAA,EAEhB,YAAY,OAAe,QAA0B;AACnD,UAAM,OAAO,EAAE;AACf,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SACoB;AACpB,UAAM,EAAE,WAAW;AACnB,WAAO,QAAQ,8CAA8C,KAAK,KAAK,IAAI;AAAA,MACzE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AACD,QAAI;AACF,YAAM,QAAQ,KAAK,WAAW,OAAO;AACrC,YAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,OAAO;AACxD,aAAO,MAAM,KAAK,kBAAkB,MAAM;AAAA,IAC5C,SAAS,OAAO;AACd,aAAO,OAAO,4BAA4B;AAAA,QACxC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,WACN,SACuB;AACvB,UAAM,QAAQ;AAAA,MACZ,GAAG,QAAQ;AAAA;AAAA,MAEX,QAAQ,QAAQ;AAAA,MAChB,MAAM,QAAQ;AAAA,MACd,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAA,IAAU,CAAA;AAAA,MAC/C,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAA,IAAU,CAAA;AAAA,IAAC;AAElD,WAAO;AAAA,EACT;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AAAA,EAEA,MAAc,kBACZ,UACoB;AACpB,UAAM,OAAO,SAAS;AAGtB,QAAI;AACJ,QAAI;AAEJ,QACE,KAAK,OAAO,KACZ,OAAO,KAAK,OAAO,MAAM,YACzB,SAAS,KAAK,OAAO,GACrB;AACA,YAAM,WAAW,KAAK,OAAO;AAC7B,iBAAW,SAAS;AACpB,oBAAc,SAAS;AAAA,IACzB,WAAW,OAAO,KAAK,cAAc,UAAU;AAC7C,iBAAW,KAAK;AAAA,IAClB;AAEA,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAKA,UAAM,gBAAgB,MAAM,MAAM,QAAQ;AAC1C,QAAI,CAAC,cAAc,IAAI;AACrB,YAAM,IAAI;AAAA,QACR,8BAA8B,QAAQ,KAAK,cAAc,MAAM,IAAI,cAAc,UAAU;AAAA,MAAA;AAAA,IAE/F;AACA,UAAM,cAAc,MAAM,cAAc,YAAA;AACxC,UAAM,SAAS,oBAAoB,WAAW;AAI9C,UAAM,kBAAkB,aAAa,MAAM,GAAG,EAAE,CAAC,GAAG,KAAA;AACpD,UAAM,mBAAmB,oBAAoB,QAAQ;AAKrD,UAAM,YACJ,oBAAoB,iBAAiB,MAAM,GAAG,EAAE,CAAC,KAAK;AACxD,UAAM,SAAS,cAAc,SAAS,QAAQ;AAE9C,UAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;AAEjE,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ,OAAO;AAAA,MACP;AAAA,MACA,aAAa,mBAAmB,SAAS,MAAM;AAAA,MAC/C,GAAI,QAAQ,EAAE,UAAU,CAAA;AAAA,IAAC;AAAA,EAE7B;AACF;AAEO,SAAS,UACd,OACA,QAC0B;AAC1B,SAAO,IAAI,iBAAiB,OAAO,MAAM;AAC3C;"}
@@ -0,0 +1,32 @@
1
+ import { BaseTranscriptionAdapter } from '@tanstack/ai/adapters';
2
+ import { TranscriptionOptions, TranscriptionResult } from '@tanstack/ai';
3
+ import { FalClientConfig } from '../utils.js';
4
+ import { FalModel, FalModelInput } from '../model-meta.js';
5
+ /**
6
+ * Provider options for transcription, excluding fields TanStack AI handles.
7
+ */
8
+ export type FalTranscriptionProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'audio_url'>;
9
+ /**
10
+ * fal.ai transcription (speech-to-text) adapter.
11
+ *
12
+ * Supports fal.ai STT models like whisper, wizper, etc.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const adapter = falTranscription('fal-ai/whisper')
17
+ * const result = await generateTranscription({
18
+ * adapter,
19
+ * audio: 'https://example.com/audio.mp3',
20
+ * language: 'en',
21
+ * })
22
+ * ```
23
+ */
24
+ export declare class FalTranscriptionAdapter<TModel extends FalModel> extends BaseTranscriptionAdapter<TModel, FalTranscriptionProviderOptions<TModel>> {
25
+ readonly name: "fal";
26
+ constructor(model: TModel, config?: FalClientConfig);
27
+ transcribe(options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>): Promise<TranscriptionResult>;
28
+ private buildInput;
29
+ protected generateId(): string;
30
+ private transformResponse;
31
+ }
32
+ export declare function falTranscription<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalTranscriptionAdapter<TModel>;
@@ -0,0 +1,93 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import { BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
3
+ import { configureFalClient, dataUrlToBlob, generateId } from "../utils/client.js";
4
+ import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
5
+ class FalTranscriptionAdapter extends BaseTranscriptionAdapter {
6
+ name = "fal";
7
+ constructor(model, config) {
8
+ super(model, {});
9
+ configureFalClient(config);
10
+ }
11
+ async transcribe(options) {
12
+ const { logger } = options;
13
+ logger.request(
14
+ `activity=generateTranscription provider=fal model=${this.model}`,
15
+ {
16
+ provider: "fal",
17
+ model: this.model
18
+ }
19
+ );
20
+ try {
21
+ const input = this.buildInput(options);
22
+ const result = await fal.subscribe(this.model, { input });
23
+ return this.transformResponse(result);
24
+ } catch (error) {
25
+ logger.errors("fal.generateTranscription fatal", {
26
+ error,
27
+ source: "fal.generateTranscription"
28
+ });
29
+ throw error;
30
+ }
31
+ }
32
+ buildInput(options) {
33
+ let audioInput;
34
+ if (options.audio instanceof ArrayBuffer) {
35
+ audioInput = new Blob([options.audio]);
36
+ } else if (typeof options.audio === "string") {
37
+ audioInput = dataUrlToBlob(options.audio) ?? options.audio;
38
+ } else {
39
+ audioInput = options.audio;
40
+ }
41
+ const input = {
42
+ ...options.modelOptions,
43
+ audio_url: audioInput,
44
+ ...options.language ? { language: options.language } : {},
45
+ ...options.prompt ? { prompt: options.prompt } : {}
46
+ };
47
+ return input;
48
+ }
49
+ generateId() {
50
+ return generateId(this.name);
51
+ }
52
+ transformResponse(response) {
53
+ const data = response.data;
54
+ const text = data.text || "";
55
+ let segments;
56
+ const chunks = data.chunks;
57
+ if (chunks && Array.isArray(chunks)) {
58
+ segments = chunks.flatMap((chunk, index) => {
59
+ const ts = chunk.timestamp;
60
+ if (!Array.isArray(ts) || ts.length < 2 || typeof ts[0] !== "number" || typeof ts[1] !== "number") {
61
+ return [];
62
+ }
63
+ return [
64
+ {
65
+ id: index,
66
+ start: ts[0],
67
+ end: ts[1],
68
+ text: chunk.text,
69
+ ...chunk.speaker ? { speaker: chunk.speaker } : {}
70
+ }
71
+ ];
72
+ });
73
+ }
74
+ const language = data.language || data.inferred_languages?.[0] || data.languages?.[0];
75
+ const usage = buildFalUsage(takeBillableUnits(response.requestId));
76
+ return {
77
+ id: response.requestId || this.generateId(),
78
+ model: this.model,
79
+ text,
80
+ ...language !== void 0 ? { language } : {},
81
+ ...segments !== void 0 ? { segments } : {},
82
+ ...usage ? { usage } : {}
83
+ };
84
+ }
85
+ }
86
+ function falTranscription(model, config) {
87
+ return new FalTranscriptionAdapter(model, config);
88
+ }
89
+ export {
90
+ FalTranscriptionAdapter,
91
+ falTranscription
92
+ };
93
+ //# sourceMappingURL=transcription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcription.js","sources":["../../../src/adapters/transcription.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseTranscriptionAdapter } from '@tanstack/ai/adapters'\nimport {\n buildFalUsage,\n configureFalClient,\n dataUrlToBlob,\n takeBillableUnits,\n generateId as utilGenerateId,\n} from '../utils'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type {\n TranscriptionOptions,\n TranscriptionResult,\n TranscriptionSegment,\n} from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Provider options for transcription, excluding fields TanStack AI handles.\n */\nexport type FalTranscriptionProviderOptions<TModel extends string> = Omit<\n FalModelInput<TModel>,\n 'audio_url'\n>\n\ninterface FalChunk {\n text: string\n timestamp?: [number, number] | null\n speaker?: string\n}\n\n/**\n * fal.ai transcription (speech-to-text) adapter.\n *\n * Supports fal.ai STT models like whisper, wizper, etc.\n *\n * @example\n * ```typescript\n * const adapter = falTranscription('fal-ai/whisper')\n * const result = await generateTranscription({\n * adapter,\n * audio: 'https://example.com/audio.mp3',\n * language: 'en',\n * })\n * ```\n */\nexport class FalTranscriptionAdapter<\n TModel extends FalModel,\n> extends BaseTranscriptionAdapter<\n TModel,\n FalTranscriptionProviderOptions<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 transcribe(\n options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n logger.request(\n `activity=generateTranscription provider=fal model=${this.model}`,\n {\n provider: 'fal',\n model: this.model,\n },\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.generateTranscription fatal', {\n error,\n source: 'fal.generateTranscription',\n })\n throw error\n }\n }\n\n private buildInput(\n options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n // fal-client auto-uploads Blob/File inputs via fal.storage.upload, but\n // passes strings through unchanged — so a `data:` URL would reach fal's\n // API and get rejected with a 422 \"Unsupported data URL\". Decode data\n // URLs to a Blob up front so the auto-upload path handles them.\n let audioInput: string | Blob | File\n if (options.audio instanceof ArrayBuffer) {\n audioInput = new Blob([options.audio])\n } else if (typeof options.audio === 'string') {\n audioInput = dataUrlToBlob(options.audio) ?? options.audio\n } else {\n audioInput = options.audio\n }\n\n const input = {\n ...options.modelOptions,\n audio_url: audioInput,\n ...(options.language ? { language: options.language } : {}),\n ...(options.prompt ? { prompt: options.prompt } : {}),\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 ): TranscriptionResult {\n const data = response.data as Record<string, unknown>\n\n const text = (data.text as string) || ''\n\n // Map fal chunks to TanStack segments. fal whisper can return\n // `timestamp: null` on some chunks (e.g. when word-level timing is\n // disabled), and the runtime payload is not actually constrained to a\n // 2-tuple — treat it as unknown and validate before indexing.\n let segments: Array<TranscriptionSegment> | undefined\n const chunks = data.chunks as Array<FalChunk> | undefined\n if (chunks && Array.isArray(chunks)) {\n segments = chunks.flatMap((chunk, index) => {\n const ts = chunk.timestamp as unknown\n if (\n !Array.isArray(ts) ||\n ts.length < 2 ||\n typeof ts[0] !== 'number' ||\n typeof ts[1] !== 'number'\n ) {\n return []\n }\n return [\n {\n id: index,\n start: ts[0],\n end: ts[1],\n text: chunk.text,\n ...(chunk.speaker ? { speaker: chunk.speaker } : {}),\n },\n ]\n })\n }\n\n // Extract language from response\n const language =\n (data.language as string) ||\n (data.inferred_languages as Array<string> | undefined)?.[0] ||\n (data.languages as Array<string> | undefined)?.[0]\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n text,\n ...(language !== undefined ? { language } : {}),\n ...(segments !== undefined ? { segments } : {}),\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falTranscription<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalTranscriptionAdapter<TModel> {\n return new FalTranscriptionAdapter(model, config)\n}\n"],"names":["utilGenerateId"],"mappings":";;;;AA+CO,MAAM,gCAEH,yBAGR;AAAA,EACS,OAAO;AAAA,EAEhB,YAAY,OAAe,QAA0B;AACnD,UAAM,OAAO,EAAE;AACf,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,WACJ,SAC8B;AAC9B,UAAM,EAAE,WAAW;AACnB,WAAO;AAAA,MACL,qDAAqD,KAAK,KAAK;AAAA,MAC/D;AAAA,QACE,UAAU;AAAA,QACV,OAAO,KAAK;AAAA,MAAA;AAAA,IACd;AAEF,QAAI;AACF,YAAM,QAAQ,KAAK,WAAW,OAAO;AACrC,YAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,OAAO;AACxD,aAAO,KAAK,kBAAkB,MAAM;AAAA,IACtC,SAAS,OAAO;AACd,aAAO,OAAO,mCAAmC;AAAA,QAC/C;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,WACN,SACuB;AAKvB,QAAI;AACJ,QAAI,QAAQ,iBAAiB,aAAa;AACxC,mBAAa,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC;AAAA,IACvC,WAAW,OAAO,QAAQ,UAAU,UAAU;AAC5C,mBAAa,cAAc,QAAQ,KAAK,KAAK,QAAQ;AAAA,IACvD,OAAO;AACL,mBAAa,QAAQ;AAAA,IACvB;AAEA,UAAM,QAAQ;AAAA,MACZ,GAAG,QAAQ;AAAA,MACX,WAAW;AAAA,MACX,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAA,IAAa,CAAA;AAAA,MACxD,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAA,IAAW,CAAA;AAAA,IAAC;AAErD,WAAO;AAAA,EACT;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AAAA,EAEQ,kBACN,UACqB;AACrB,UAAM,OAAO,SAAS;AAEtB,UAAM,OAAQ,KAAK,QAAmB;AAMtC,QAAI;AACJ,UAAM,SAAS,KAAK;AACpB,QAAI,UAAU,MAAM,QAAQ,MAAM,GAAG;AACnC,iBAAW,OAAO,QAAQ,CAAC,OAAO,UAAU;AAC1C,cAAM,KAAK,MAAM;AACjB,YACE,CAAC,MAAM,QAAQ,EAAE,KACjB,GAAG,SAAS,KACZ,OAAO,GAAG,CAAC,MAAM,YACjB,OAAO,GAAG,CAAC,MAAM,UACjB;AACA,iBAAO,CAAA;AAAA,QACT;AACA,eAAO;AAAA,UACL;AAAA,YACE,IAAI;AAAA,YACJ,OAAO,GAAG,CAAC;AAAA,YACX,KAAK,GAAG,CAAC;AAAA,YACT,MAAM,MAAM;AAAA,YACZ,GAAI,MAAM,UAAU,EAAE,SAAS,MAAM,QAAA,IAAY,CAAA;AAAA,UAAC;AAAA,QACpD;AAAA,MAEJ,CAAC;AAAA,IACH;AAGA,UAAM,WACH,KAAK,YACL,KAAK,qBAAmD,CAAC,KACzD,KAAK,YAA0C,CAAC;AAEnD,UAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;AAEjE,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ;AAAA,MACA,GAAI,aAAa,SAAY,EAAE,SAAA,IAAa,CAAA;AAAA,MAC5C,GAAI,aAAa,SAAY,EAAE,SAAA,IAAa,CAAA;AAAA,MAC5C,GAAI,QAAQ,EAAE,UAAU,CAAA;AAAA,IAAC;AAAA,EAE7B;AACF;AAEO,SAAS,iBACd,OACA,QACiC;AACjC,SAAO,IAAI,wBAAwB,OAAO,MAAM;AAClD;"}
@@ -0,0 +1,34 @@
1
+ import { BaseVideoAdapter } from '@tanstack/ai/adapters';
2
+ import { VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '@tanstack/ai';
3
+ import { FalModel, FalModelVideoSize, FalVideoPromptModalitiesFor, FalVideoProviderOptions } from '../model-meta.js';
4
+ import { FalClientConfig } from '../utils.js';
5
+ /**
6
+ * fal.ai video generation adapter.
7
+ * Supports MiniMax, Luma, Kling, Hunyuan, and other fal.ai video models.
8
+ *
9
+ * Uses fal.ai's comprehensive type system to provide autocomplete
10
+ * and type safety for all supported video models.
11
+ *
12
+ * @experimental Video generation is an experimental feature and may change.
13
+ */
14
+ export declare class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<TModel, FalVideoProviderOptions<TModel>, Record<TModel, FalVideoProviderOptions<TModel>>, Record<TModel, FalModelVideoSize<TModel>>, Record<TModel, FalVideoPromptModalitiesFor<TModel>>> {
15
+ readonly kind: "video";
16
+ readonly name: "fal";
17
+ constructor(model: TModel, config?: FalClientConfig);
18
+ createVideoJob(options: VideoGenerationOptions<FalVideoProviderOptions<TModel>, FalModelVideoSize<TModel>>): Promise<VideoJobResult>;
19
+ getVideoStatus(jobId: string): Promise<VideoStatusResult>;
20
+ getVideoUrl(jobId: string): Promise<VideoUrlResult>;
21
+ protected generateId(): string;
22
+ }
23
+ /**
24
+ * Create a fal.ai video adapter with an explicit API key.
25
+ *
26
+ * @experimental Video generation is an experimental feature and may change.
27
+ */
28
+ export declare function createFalVideo<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalVideoAdapter<TModel>;
29
+ /**
30
+ * Create a fal.ai video adapter using config.apiKey or the FAL_KEY environment variable.
31
+ *
32
+ * @experimental Video generation is an experimental feature and may change.
33
+ */
34
+ export declare function falVideo<TModel extends FalModel>(model: TModel, config?: FalClientConfig): FalVideoAdapter<TModel>;