@tanstack/ai-fal 0.7.8 → 0.7.9
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 +3 -3
- package/dist/esm/adapters/audio.js.map +1 -1
- package/dist/esm/adapters/image.js +2 -2
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/speech.js +3 -3
- package/dist/esm/adapters/speech.js.map +1 -1
- package/dist/esm/adapters/transcription.js +3 -3
- package/dist/esm/adapters/transcription.js.map +1 -1
- package/dist/esm/adapters/video.js +8 -5
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/model-meta.d.ts +2 -2
- package/dist/esm/utils/client.js +4 -3
- package/dist/esm/utils/client.js.map +1 -1
- package/dist/esm/video/video-provider-options.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/audio.ts +6 -2
- package/src/adapters/image.ts +1 -1
- package/src/adapters/speech.ts +6 -2
- package/src/adapters/transcription.ts +2 -2
- package/src/adapters/video.ts +10 -8
- package/src/model-meta.ts +2 -2
- package/src/utils/client.ts +4 -3
- package/src/video/video-provider-options.ts +3 -3
|
@@ -13,9 +13,9 @@ function buildDurationFragment(model, duration) {
|
|
|
13
13
|
return builder ? builder(duration) : { duration };
|
|
14
14
|
}
|
|
15
15
|
class FalAudioAdapter extends BaseAudioAdapter {
|
|
16
|
+
name = "fal";
|
|
16
17
|
constructor(model, config) {
|
|
17
18
|
super(model, {});
|
|
18
|
-
this.name = "fal";
|
|
19
19
|
configureFalClient(config);
|
|
20
20
|
}
|
|
21
21
|
async generateAudio(options) {
|
|
@@ -50,8 +50,8 @@ class FalAudioAdapter extends BaseAudioAdapter {
|
|
|
50
50
|
const data = response.data;
|
|
51
51
|
let audioUrl;
|
|
52
52
|
let contentType;
|
|
53
|
-
if (data
|
|
54
|
-
const audioObj = data
|
|
53
|
+
if (data["audio"] && typeof data["audio"] === "object" && "url" in data["audio"]) {
|
|
54
|
+
const audioObj = data["audio"];
|
|
55
55
|
audioUrl = audioObj.url;
|
|
56
56
|
contentType = audioObj.content_type;
|
|
57
57
|
} else if (typeof data.audio_url === "string") {
|
|
@@ -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'\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 (data
|
|
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'\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 return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: {\n url: audioUrl,\n contentType: deriveAudioContentType(contentType, audioUrl),\n },\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":";;;AA8BA,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,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,IAC3D;AAAA,EAEJ;AACF;AAEO,SAAS,SACd,OACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;"}
|
|
@@ -3,10 +3,10 @@ import { BaseImageAdapter } from "@tanstack/ai/adapters";
|
|
|
3
3
|
import { configureFalClient, generateId } from "../utils/client.js";
|
|
4
4
|
import { mapSizeToFalFormat } from "../image/image-provider-options.js";
|
|
5
5
|
class FalImageAdapter extends BaseImageAdapter {
|
|
6
|
+
kind = "image";
|
|
7
|
+
name = "fal";
|
|
6
8
|
constructor(model, config) {
|
|
7
9
|
super(model, {});
|
|
8
|
-
this.kind = "image";
|
|
9
|
-
this.name = "fal";
|
|
10
10
|
configureFalClient(config);
|
|
11
11
|
}
|
|
12
12
|
async generateImages(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { configureFalClient, generateId as utilGenerateId } from '../utils'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\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> {\n 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 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.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 ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n const input = {\n ...options.modelOptions,\n ...sizeParams,\n prompt: options.prompt,\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 return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\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 as { url: unknown }).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":";;;;AAsCO,MAAM,wBAAiD,iBAK5D;AAAA,
|
|
1
|
+
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport { configureFalClient, generateId as utilGenerateId } from '../utils'\nimport { mapSizeToFalFormat } from '../image/image-provider-options'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { FalClientConfig } from '../utils'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\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> {\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 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.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 ): FalModelInput<TModel> {\n const sizeParams = mapSizeToFalFormat(options.size)\n const input = {\n ...options.modelOptions,\n ...sizeParams,\n prompt: options.prompt,\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 return {\n id: response.requestId || this.generateId(),\n model: this.model,\n images,\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 as { url: unknown }).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":";;;;AAsCO,MAAM,wBAAiD,iBAK5D;AAAA,EACkB,OAAO;AAAA,EAChB,OAAO;AAAA,EAEhB,YAAY,OAAe,QAA0B;AACnD,UAAM,OAAO,EAAE;AACf,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SAIgC;AAChC,UAAM,EAAE,WAAW;AAEnB,WAAO,QAAQ,6CAA6C,KAAK,KAAK,IAAI;AAAA,MACxE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AAED,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,SAIuB;AACvB,UAAM,aAAa,mBAAmB,QAAQ,IAAI;AAClD,UAAM,QAAQ;AAAA,MACZ,GAAG,QAAQ;AAAA,MACX,GAAG;AAAA,MACH,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,IAAA;AAEtB,WAAO;AAAA,EACT;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AAAA,EAEQ,kBACN,UACuB;AACvB,UAAM,SAAgC,CAAA;AACtC,UAAM,OAAO,SAAS;AAGtB,QAAI,YAAY,QAAQ,MAAM,QAAQ,KAAK,MAAM,GAAG;AAClD,iBAAW,OAAO,KAAK,QAAQ;AAC7B,eAAO,KAAK,KAAK,WAAW,GAAG,CAAC;AAAA,MAClC;AAAA,IACF,WAES,WAAW,QAAQ,KAAK,SAAS,OAAO,KAAK,UAAU,UAAU;AACxE,aAAO,KAAK,KAAK,WAAW,KAAK,KAAK,CAAC;AAAA,IACzC;AAEA,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,kFACE,OAAO,KAAK,IAAI,EAAE,KAAK,GAAG;AAAA,MAAA;AAAA,IAEhC;AAEA,WAAO;AAAA,MACL,IAAI,SAAS,aAAa,KAAK,WAAA;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEQ,WAAW,KAA8B;AAC/C,QAAI;AACJ,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM;AAAA,IACR,WACE,OACA,OAAO,QAAQ,YACf,SAAS,OACT,OAAQ,IAAyB,QAAQ,UACzC;AACA,YAAO,IAAwB;AAAA,IACjC,OAAO;AACL,YAAM,IAAI;AAAA,QACR,yFACE,QAAQ,OAAO,SAAS,OAAO,GACjC;AAAA,MAAA;AAAA,IAEJ;AAEA,QAAI,IAAI,WAAW,OAAO,GAAG;AAC3B,YAAM,cAAc,IAAI,MAAM,iCAAiC;AAC/D,UAAI,eAAe,YAAY,CAAC,GAAG;AACjC,eAAO,EAAE,SAAS,YAAY,CAAC,EAAA;AAAA,MACjC;AAAA,IACF;AACA,WAAO,EAAE,IAAA;AAAA,EACX;AACF;AAEO,SAAS,eACd,OACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAEO,SAAS,SACd,OACA,QACyB;AACzB,SAAO,eAAe,OAAO,MAAM;AACrC;"}
|
|
@@ -2,9 +2,9 @@ import { fal } from "@fal-ai/client";
|
|
|
2
2
|
import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
3
3
|
import { configureFalClient, generateId, arrayBufferToBase64, extractUrlExtension } from "../utils/client.js";
|
|
4
4
|
class FalSpeechAdapter extends BaseTTSAdapter {
|
|
5
|
+
name = "fal";
|
|
5
6
|
constructor(model, config) {
|
|
6
7
|
super(model, {});
|
|
7
|
-
this.name = "fal";
|
|
8
8
|
configureFalClient(config);
|
|
9
9
|
}
|
|
10
10
|
async generateSpeech(options) {
|
|
@@ -43,8 +43,8 @@ class FalSpeechAdapter extends BaseTTSAdapter {
|
|
|
43
43
|
const data = response.data;
|
|
44
44
|
let audioUrl;
|
|
45
45
|
let contentType;
|
|
46
|
-
if (data
|
|
47
|
-
const audioObj = data
|
|
46
|
+
if (data["audio"] && typeof data["audio"] === "object" && "url" in data["audio"]) {
|
|
47
|
+
const audioObj = data["audio"];
|
|
48
48
|
audioUrl = audioObj.url;
|
|
49
49
|
contentType = audioObj.content_type;
|
|
50
50
|
} else if (typeof data.audio_url === "string") {
|
|
@@ -1 +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 configureFalClient,\n extractUrlExtension,\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 (data
|
|
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 configureFalClient,\n extractUrlExtension,\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 return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: base64,\n format,\n contentType: contentTypeMime || `audio/${format}`,\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":";;;AAoCO,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,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,IAAA;AAAA,EAEnD;AACF;AAEO,SAAS,UACd,OACA,QAC0B;AAC1B,SAAO,IAAI,iBAAiB,OAAO,MAAM;AAC3C;"}
|
|
@@ -2,9 +2,9 @@ import { fal } from "@fal-ai/client";
|
|
|
2
2
|
import { BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
|
|
3
3
|
import { configureFalClient, dataUrlToBlob, generateId } from "../utils/client.js";
|
|
4
4
|
class FalTranscriptionAdapter extends BaseTranscriptionAdapter {
|
|
5
|
+
name = "fal";
|
|
5
6
|
constructor(model, config) {
|
|
6
7
|
super(model, {});
|
|
7
|
-
this.name = "fal";
|
|
8
8
|
configureFalClient(config);
|
|
9
9
|
}
|
|
10
10
|
async transcribe(options) {
|
|
@@ -75,8 +75,8 @@ class FalTranscriptionAdapter extends BaseTranscriptionAdapter {
|
|
|
75
75
|
id: response.requestId || this.generateId(),
|
|
76
76
|
model: this.model,
|
|
77
77
|
text,
|
|
78
|
-
language,
|
|
79
|
-
segments
|
|
78
|
+
...language !== void 0 ? { language } : {},
|
|
79
|
+
...segments !== void 0 ? { segments } : {}
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -1 +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 configureFalClient,\n dataUrlToBlob,\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 return {\n id: response.requestId || this.generateId(),\n model: this.model,\n text,\n language,\n segments,\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":";;;AA6CO,MAAM,gCAEH,yBAGR;AAAA,
|
|
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 configureFalClient,\n dataUrlToBlob,\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 return {\n id: response.requestId || this.generateId(),\n model: this.model,\n text,\n ...(language !== undefined ? { language } : {}),\n ...(segments !== undefined ? { segments } : {}),\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":";;;AA6CO,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,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,aAAa,CAAA;AAAA,IAAC;AAAA,EAEjD;AACF;AAEO,SAAS,iBACd,OACA,QACiC;AACjC,SAAO,IAAI,wBAAwB,OAAO,MAAM;AAClD;"}
|
|
@@ -15,10 +15,10 @@ function mapFalStatusToVideoStatus(falStatus) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
class FalVideoAdapter extends BaseVideoAdapter {
|
|
18
|
+
kind = "video";
|
|
19
|
+
name = "fal";
|
|
18
20
|
constructor(model, config) {
|
|
19
21
|
super({}, model);
|
|
20
|
-
this.kind = "video";
|
|
21
|
-
this.name = "fal";
|
|
22
22
|
configureFalClient(config);
|
|
23
23
|
}
|
|
24
24
|
async createVideoJob(options) {
|
|
@@ -58,7 +58,9 @@ class FalVideoAdapter extends BaseVideoAdapter {
|
|
|
58
58
|
return {
|
|
59
59
|
jobId,
|
|
60
60
|
status: mapFalStatusToVideoStatus(statusResponse.status),
|
|
61
|
-
|
|
61
|
+
...statusResponse.queue_position != null ? {
|
|
62
|
+
progress: Math.max(0, 100 - statusResponse.queue_position * 10)
|
|
63
|
+
} : {}
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
async getVideoUrl(jobId) {
|
|
@@ -68,7 +70,8 @@ class FalVideoAdapter extends BaseVideoAdapter {
|
|
|
68
70
|
requestId: jobId
|
|
69
71
|
});
|
|
70
72
|
} catch (error) {
|
|
71
|
-
const
|
|
73
|
+
const err = error;
|
|
74
|
+
const detail = err.body?.detail;
|
|
72
75
|
if (Array.isArray(detail)) {
|
|
73
76
|
const messages = detail.map(
|
|
74
77
|
(d) => d.loc ? `${d.loc.join(".")}: ${d.msg}` : d.msg
|
|
@@ -76,7 +79,7 @@ class FalVideoAdapter extends BaseVideoAdapter {
|
|
|
76
79
|
throw new Error(`Video generation failed: ${messages.join("; ")}`);
|
|
77
80
|
}
|
|
78
81
|
throw new Error(
|
|
79
|
-
`Failed to retrieve video result: ${
|
|
82
|
+
`Failed to retrieve video result: ${err.message || String(error)}`
|
|
80
83
|
);
|
|
81
84
|
}
|
|
82
85
|
const data = result.data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.js","sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseVideoAdapter } from '@tanstack/ai/adapters'\nimport { configureFalClient, generateId as utilGenerateId } from '../utils'\nimport { mapVideoSizeToFalFormat } from '../video/video-provider-options'\nimport type {\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoSize,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils'\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> {\n 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 { prompt, 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 sizeParams = mapVideoSizeToFalFormat(size)\n\n const input = {\n ...modelOptions,\n ...sizeParams,\n prompt,\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
|
|
1
|
+
{"version":3,"file":"video.js","sources":["../../../src/adapters/video.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { BaseVideoAdapter } from '@tanstack/ai/adapters'\nimport { configureFalClient, generateId as utilGenerateId } from '../utils'\nimport { mapVideoSizeToFalFormat } from '../video/video-provider-options'\nimport type {\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type {\n FalModel,\n FalModelInput,\n FalModelVideoSize,\n FalVideoProviderOptions,\n} from '../model-meta'\nimport type { FalClientConfig } from '../utils'\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> {\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 { prompt, 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 sizeParams = mapVideoSizeToFalFormat(size)\n\n const input = {\n ...modelOptions,\n ...sizeParams,\n prompt,\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 return {\n jobId,\n url,\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":";;;;AAsCA,SAAS,0BACP,WAC6B;AAC7B,UAAQ,WAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EAAA;AAEb;AAWO,MAAM,wBAAiD,iBAK5D;AAAA,EACkB,OAAO;AAAA,EAChB,OAAO;AAAA,EAEhB,YAAY,OAAe,QAA0B;AACnD,UAAM,CAAA,GAAI,KAAK;AACf,uBAAmB,MAAM;AAAA,EAC3B;AAAA,EAEA,MAAM,eACJ,SAIyB;AACzB,UAAM,EAAE,QAAQ,MAAM,UAAU,cAAc,WAAW;AAEzD,WAAO,QAAQ,6CAA6C,KAAK,KAAK,IAAI;AAAA,MACxE,UAAU;AAAA,MACV,OAAO,KAAK;AAAA,IAAA,CACb;AAED,QAAI;AACF,YAAM,aAAa,wBAAwB,IAAI;AAE/C,YAAM,QAAQ;AAAA,QACZ,GAAG;AAAA,QACH,GAAG;AAAA,QACH;AAAA,QACA,GAAI,WAAW,EAAE,aAAa,CAAA;AAAA,MAAC;AAIjC,YAAM,EAAE,eAAe,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,QACxD;AAAA,MAAA,CACD;AAED,aAAO;AAAA,QACL,OAAO;AAAA,QACP,OAAO,KAAK;AAAA,MAAA;AAAA,IAEhB,SAAS,OAAO;AACd,aAAO,OAAO,4BAA4B;AAAA,QACxC;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,OAA2C;AAC9D,UAAM,iBAAkB,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,MACzD,WAAW;AAAA,MACX,MAAM;AAAA,IAAA,CACP;AAED,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,0BAA0B,eAAe,MAAM;AAAA,MACvD,GAAI,eAAe,kBAAkB,OACjC;AAAA,QACE,UAAU,KAAK,IAAI,GAAG,MAAM,eAAe,iBAAiB,EAAE;AAAA,MAAA,IAEhE,CAAA;AAAA,IAAC;AAAA,EAET;AAAA,EAEA,MAAM,YAAY,OAAwC;AACxD,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,IAAI,MAAM,OAAO,KAAK,OAAO;AAAA,QAC1C,WAAW;AAAA,MAAA,CACZ;AAAA,IACH,SAAS,OAAgB;AAGvB,YAAM,MAAM;AACZ,YAAM,SAAS,IAAI,MAAM;AACzB,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,cAAM,WAAW,OAAO;AAAA,UACtB,CAAC,MACC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE;AAAA,QAAA;AAE/C,cAAM,IAAI,MAAM,4BAA4B,SAAS,KAAK,IAAI,CAAC,EAAE;AAAA,MACnE;AACA,YAAM,IAAI;AAAA,QACR,oCAAoC,IAAI,WAAW,OAAO,KAAK,CAAC;AAAA,MAAA;AAAA,IAEpE;AAEA,UAAM,OAAO,OAAO;AAGpB,UAAM,MAAM,KAAK,OAAO,OAAO,KAAK;AACpC,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAAA,EAEmB,aAAqB;AACtC,WAAOA,WAAe,KAAK,IAAI;AAAA,EACjC;AACF;AAOO,SAAS,eACd,OACA,QACyB;AACzB,SAAO,IAAI,gBAAgB,OAAO,MAAM;AAC1C;AAOO,SAAS,SACd,OACA,QACyB;AACzB,SAAO,eAAe,OAAO,MAAM;AACrC;"}
|
package/dist/esm/model-meta.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type FalModel = keyof EndpointTypeMap | (string & {});
|
|
|
12
12
|
* type FluxInput = FalModelInput<'fal-ai/flux/dev'>
|
|
13
13
|
* // { prompt: string; num_inference_steps?: number; ... }
|
|
14
14
|
*/
|
|
15
|
-
export type FalModelInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? EndpointTypeMap[TModel]['input'] : Record<string,
|
|
15
|
+
export type FalModelInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? EndpointTypeMap[TModel]['input'] : Record<string, unknown>;
|
|
16
16
|
/**
|
|
17
17
|
* Utility type to extract the output type for a specific fal model.
|
|
18
18
|
*
|
|
@@ -75,7 +75,7 @@ export type FalModelVideoSizeInput<TModel extends string> = TModel extends keyof
|
|
|
75
75
|
* Provider options for video generation, excluding fields TanStack AI handles.
|
|
76
76
|
* Use this for the `modelOptions` parameter in video generation.
|
|
77
77
|
*/
|
|
78
|
-
export type FalVideoProviderOptions<TModel extends string> = TModel extends keyof EndpointTypeMap ? Omit<FalModelInput<TModel>, 'prompt'> : Record<string,
|
|
78
|
+
export type FalVideoProviderOptions<TModel extends string> = TModel extends keyof EndpointTypeMap ? Omit<FalModelInput<TModel>, 'prompt'> : Record<string, unknown>;
|
|
79
79
|
/**
|
|
80
80
|
* Provider options for TTS, excluding fields TanStack AI handles.
|
|
81
81
|
* Use this for the `modelOptions` parameter in speech generation.
|
package/dist/esm/utils/client.js
CHANGED
|
@@ -19,7 +19,7 @@ function extractUrlExtension(url) {
|
|
|
19
19
|
const parsed = new URL(url);
|
|
20
20
|
pathname = parsed.pathname;
|
|
21
21
|
} catch {
|
|
22
|
-
pathname = url.split("?")[0]
|
|
22
|
+
pathname = url.split("?")[0]?.split("#")[0] ?? url;
|
|
23
23
|
}
|
|
24
24
|
const normalized = pathname.replace(/\/+$/, "");
|
|
25
25
|
if (!normalized.includes("/")) return void 0;
|
|
@@ -50,6 +50,7 @@ function deriveAudioContentType(explicitContentType, url) {
|
|
|
50
50
|
return "audio/mp4";
|
|
51
51
|
case "webm":
|
|
52
52
|
return "audio/webm";
|
|
53
|
+
case void 0:
|
|
53
54
|
default:
|
|
54
55
|
return "audio/mpeg";
|
|
55
56
|
}
|
|
@@ -78,8 +79,8 @@ function arrayBufferToBase64(bytes) {
|
|
|
78
79
|
}
|
|
79
80
|
const view = new Uint8Array(bytes);
|
|
80
81
|
let binary = "";
|
|
81
|
-
for (
|
|
82
|
-
binary += String.fromCharCode(
|
|
82
|
+
for (const byte of view) {
|
|
83
|
+
binary += String.fromCharCode(byte);
|
|
83
84
|
}
|
|
84
85
|
return btoa(binary);
|
|
85
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'\n\nexport interface FalClientConfig {\n apiKey: string\n proxyUrl?: string\n}\n\nexport function getFalApiKeyFromEnv(): string {\n return getApiKeyFromEnv('FAL_KEY')\n}\n\nexport function configureFalClient(config?: FalClientConfig): void {\n const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()\n fal.config({\n credentials: apiKey,\n ...(config?.proxyUrl ? { proxyUrl: config.proxyUrl } : {}),\n })\n}\n\nexport function generateId(prefix: string): string {\n return _generateId(prefix)\n}\n\n/**\n * Extract a safe file extension from a URL. Strips query strings, URL\n * fragments, and any trailing slashes, and only returns the extension when\n * it looks like a real one (2-5 alphanumeric chars). Returns undefined\n * otherwise so callers can fall back to a default.\n */\nexport function extractUrlExtension(url: string): string | undefined {\n // Parse via URL when possible so we only look at the pathname and never\n // mistake a TLD (e.g. the `.com` in `https://x.com/`) for a file extension.\n let pathname: string\n try {\n const parsed = new URL(url)\n pathname = parsed.pathname\n } catch {\n // Fall back to treating the input as a raw path when URL parsing fails\n // (e.g. the caller passed a bare path). Still strip ?query and #fragment.\n pathname = url.split('?')[0]
|
|
1
|
+
{"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'\n\nexport interface FalClientConfig {\n apiKey: string\n proxyUrl?: string\n}\n\nexport function getFalApiKeyFromEnv(): string {\n return getApiKeyFromEnv('FAL_KEY')\n}\n\nexport function configureFalClient(config?: FalClientConfig): void {\n const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()\n fal.config({\n credentials: apiKey,\n ...(config?.proxyUrl ? { proxyUrl: config.proxyUrl } : {}),\n })\n}\n\nexport function generateId(prefix: string): string {\n return _generateId(prefix)\n}\n\n/**\n * Extract a safe file extension from a URL. Strips query strings, URL\n * fragments, and any trailing slashes, and only returns the extension when\n * it looks like a real one (2-5 alphanumeric chars). Returns undefined\n * otherwise so callers can fall back to a default.\n */\nexport function extractUrlExtension(url: string): string | undefined {\n // Parse via URL when possible so we only look at the pathname and never\n // mistake a TLD (e.g. the `.com` in `https://x.com/`) for a file extension.\n let pathname: string\n try {\n const parsed = new URL(url)\n pathname = parsed.pathname\n } catch {\n // Fall back to treating the input as a raw path when URL parsing fails\n // (e.g. the caller passed a bare path). Still strip ?query and #fragment.\n pathname = url.split('?')[0]?.split('#')[0] ?? url\n }\n // Drop trailing slashes so `/path/audio.mp3/` still yields `mp3`.\n const normalized = pathname.replace(/\\/+$/, '')\n // Require at least one `/` — otherwise we're looking at an empty pathname\n // (bare-host URLs like `https://x.com/` land here after stripping the\n // trailing slash).\n if (!normalized.includes('/')) return undefined\n const lastSegment = normalized.split('/').pop()\n if (!lastSegment) return undefined\n const extension = lastSegment.split('.').pop()\n if (!extension || extension === lastSegment) return undefined\n return /^[a-z0-9]{2,5}$/i.test(extension) ? extension : undefined\n}\n\n/**\n * Derive a reasonable audio content-type. Prefers the explicit MIME (stripped\n * of parameters), then an extension-based lookup for common audio formats,\n * otherwise falls back to audio/mpeg — fal URLs virtually always serve mp3.\n */\nexport function deriveAudioContentType(\n explicitContentType: string | undefined,\n url: string,\n): string {\n const stripped = explicitContentType?.split(';')[0]?.trim()\n if (stripped) return stripped\n\n const ext = extractUrlExtension(url)?.toLowerCase()\n switch (ext) {\n case 'mp3':\n return 'audio/mpeg'\n case 'wav':\n return 'audio/wav'\n case 'ogg':\n case 'oga':\n return 'audio/ogg'\n case 'flac':\n return 'audio/flac'\n case 'aac':\n return 'audio/aac'\n case 'm4a':\n case 'mp4':\n return 'audio/mp4'\n case 'webm':\n return 'audio/webm'\n case undefined:\n default:\n return 'audio/mpeg'\n }\n}\n\n/**\n * Decode a `data:` URL into a Blob so fal-client can auto-upload it via\n * `fal.storage.upload`. fal's inference API rejects data URLs with a 422\n * \"Unsupported data URL\", so we convert them before handing them off.\n *\n * Supports both base64 and URL-encoded data URLs. Returns `undefined` for\n * anything that isn't a data URL, so callers can fall through to other\n * handling (http URLs are passed to fal as-is).\n */\nexport function dataUrlToBlob(value: string): Blob | undefined {\n if (!value.startsWith('data:')) return undefined\n const commaIndex = value.indexOf(',')\n if (commaIndex === -1) return undefined\n\n const header = value.slice(5, commaIndex)\n const payload = value.slice(commaIndex + 1)\n const isBase64 = /;base64$/i.test(header)\n const mimeType = header.split(';')[0] || 'application/octet-stream'\n\n if (isBase64) {\n const binary = atob(payload)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) {\n bytes[i] = binary.charCodeAt(i)\n }\n return new Blob([bytes], { type: mimeType })\n }\n\n return new Blob([decodeURIComponent(payload)], { type: mimeType })\n}\n\n/**\n * Convert an ArrayBuffer to base64 in a cross-runtime way.\n *\n * The naive `btoa(String.fromCharCode(...bytes))` form blows up V8's argument\n * limit (~65k) on realistic audio payloads, so we either use `Buffer` (Node /\n * Bun) or walk the byte array in a single loop (browser).\n */\nexport function arrayBufferToBase64(bytes: ArrayBuffer): string {\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\n return Buffer.from(bytes).toString('base64')\n }\n const view = new Uint8Array(bytes)\n let binary = ''\n for (const byte of view) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n"],"names":["_generateId"],"mappings":";;AAQO,SAAS,sBAA8B;AAC5C,SAAO,iBAAiB,SAAS;AACnC;AAEO,SAAS,mBAAmB,QAAgC;AACjE,QAAM,SAAS,QAAQ,UAAU,oBAAA;AACjC,MAAI,OAAO;AAAA,IACT,aAAa;AAAA,IACb,GAAI,QAAQ,WAAW,EAAE,UAAU,OAAO,SAAA,IAAa,CAAA;AAAA,EAAC,CACzD;AACH;AAEO,SAAS,WAAW,QAAwB;AACjD,SAAOA,aAAY,MAAM;AAC3B;AAQO,SAAS,oBAAoB,KAAiC;AAGnE,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,eAAW,OAAO;AAAA,EACpB,QAAQ;AAGN,eAAW,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,KAAK;AAAA,EACjD;AAEA,QAAM,aAAa,SAAS,QAAQ,QAAQ,EAAE;AAI9C,MAAI,CAAC,WAAW,SAAS,GAAG,EAAG,QAAO;AACtC,QAAM,cAAc,WAAW,MAAM,GAAG,EAAE,IAAA;AAC1C,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,YAAY,YAAY,MAAM,GAAG,EAAE,IAAA;AACzC,MAAI,CAAC,aAAa,cAAc,YAAa,QAAO;AACpD,SAAO,mBAAmB,KAAK,SAAS,IAAI,YAAY;AAC1D;AAOO,SAAS,uBACd,qBACA,KACQ;AACR,QAAM,WAAW,qBAAqB,MAAM,GAAG,EAAE,CAAC,GAAG,KAAA;AACrD,MAAI,SAAU,QAAO;AAErB,QAAM,MAAM,oBAAoB,GAAG,GAAG,YAAA;AACtC,UAAQ,KAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EAAA;AAEb;AAWO,SAAS,cAAc,OAAiC;AAC7D,MAAI,CAAC,MAAM,WAAW,OAAO,EAAG,QAAO;AACvC,QAAM,aAAa,MAAM,QAAQ,GAAG;AACpC,MAAI,eAAe,GAAI,QAAO;AAE9B,QAAM,SAAS,MAAM,MAAM,GAAG,UAAU;AACxC,QAAM,UAAU,MAAM,MAAM,aAAa,CAAC;AAC1C,QAAM,WAAW,YAAY,KAAK,MAAM;AACxC,QAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK;AAEzC,MAAI,UAAU;AACZ,UAAM,SAAS,KAAK,OAAO;AAC3B,UAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;AACzC,YAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AAAA,IAChC;AACA,WAAO,IAAI,KAAK,CAAC,KAAK,GAAG,EAAE,MAAM,UAAU;AAAA,EAC7C;AAEA,SAAO,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,GAAG,EAAE,MAAM,UAAU;AACnE;AASO,SAAS,oBAAoB,OAA4B;AAC9D,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,SAAS,YAAY;AACtE,WAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAAA,EAC7C;AACA,QAAM,OAAO,IAAI,WAAW,KAAK;AACjC,MAAI,SAAS;AACb,aAAW,QAAQ,MAAM;AACvB,cAAU,OAAO,aAAa,IAAI;AAAA,EACpC;AACA,SAAO,KAAK,MAAM;AACpB;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video-provider-options.js","sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["import type { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta'\n\nexport function mapVideoSizeToFalFormat<TModel extends string>(\n size: FalModelVideoSize<TModel> | undefined,\n): FalModelVideoSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_720p\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // \"720p\" → { resolution }\n if (size.includes('_')) {\n const [aspect_ratio, resolution] = size.split('_')\n return {\n aspect_ratio,\n resolution,\n } as
|
|
1
|
+
{"version":3,"file":"video-provider-options.js","sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["import type { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta'\n\nexport function mapVideoSizeToFalFormat<TModel extends string>(\n size: FalModelVideoSize<TModel> | undefined,\n): FalModelVideoSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_720p\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // \"720p\" → { resolution }\n if (size.includes('_')) {\n const [aspect_ratio, resolution] = size.split('_')\n return {\n aspect_ratio,\n resolution,\n } as FalModelVideoSizeInput<TModel>\n }\n\n if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelVideoSizeInput<TModel>\n }\n\n return { resolution: size } as FalModelVideoSizeInput<TModel>\n}\n"],"names":[],"mappings":"AAEO,SAAS,wBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAKlB,MAAI,KAAK,SAAS,GAAG,GAAG;AACtB,UAAM,CAAC,cAAc,UAAU,IAAI,KAAK,MAAM,GAAG;AACjD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,KAAK,SAAS,GAAG,GAAG;AACtB,WAAO,EAAE,cAAc,KAAA;AAAA,EACzB;AAEA,SAAO,EAAE,YAAY,KAAA;AACvB;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-fal",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "fal.ai adapter for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@vitest/coverage-v8": "4.0.14",
|
|
44
44
|
"vite": "^7.3.3",
|
|
45
|
-
"@tanstack/ai": "0.20.
|
|
45
|
+
"@tanstack/ai": "0.20.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@tanstack/ai": "0.20.
|
|
48
|
+
"@tanstack/ai": "0.20.1"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "vite build",
|
package/src/adapters/audio.ts
CHANGED
|
@@ -117,8 +117,12 @@ export class FalAudioAdapter<TModel extends FalModel> extends BaseAudioAdapter<
|
|
|
117
117
|
let audioUrl: string | undefined
|
|
118
118
|
let contentType: string | undefined
|
|
119
119
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
120
|
+
if (
|
|
121
|
+
data['audio'] &&
|
|
122
|
+
typeof data['audio'] === 'object' &&
|
|
123
|
+
'url' in data['audio']
|
|
124
|
+
) {
|
|
125
|
+
const audioObj = data['audio'] as { url: string; content_type?: string }
|
|
122
126
|
audioUrl = audioObj.url
|
|
123
127
|
contentType = audioObj.content_type
|
|
124
128
|
} else if (typeof data.audio_url === 'string') {
|
package/src/adapters/image.ts
CHANGED
|
@@ -42,7 +42,7 @@ export class FalImageAdapter<TModel extends FalModel> extends BaseImageAdapter<
|
|
|
42
42
|
Record<TModel, FalImageProviderOptions<TModel>>,
|
|
43
43
|
Record<TModel, FalModelImageSize<TModel>>
|
|
44
44
|
> {
|
|
45
|
-
readonly kind = 'image' as const
|
|
45
|
+
override readonly kind = 'image' as const
|
|
46
46
|
readonly name = 'fal' as const
|
|
47
47
|
|
|
48
48
|
constructor(model: TModel, config?: FalClientConfig) {
|
package/src/adapters/speech.ts
CHANGED
|
@@ -93,8 +93,12 @@ export class FalSpeechAdapter<TModel extends FalModel> extends BaseTTSAdapter<
|
|
|
93
93
|
let audioUrl: string | undefined
|
|
94
94
|
let contentType: string | undefined
|
|
95
95
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
96
|
+
if (
|
|
97
|
+
data['audio'] &&
|
|
98
|
+
typeof data['audio'] === 'object' &&
|
|
99
|
+
'url' in data['audio']
|
|
100
|
+
) {
|
|
101
|
+
const audioObj = data['audio'] as { url: string; content_type?: string }
|
|
98
102
|
audioUrl = audioObj.url
|
|
99
103
|
contentType = audioObj.content_type
|
|
100
104
|
} else if (typeof data.audio_url === 'string') {
|
|
@@ -155,8 +155,8 @@ export class FalTranscriptionAdapter<
|
|
|
155
155
|
id: response.requestId || this.generateId(),
|
|
156
156
|
model: this.model,
|
|
157
157
|
text,
|
|
158
|
-
language,
|
|
159
|
-
segments,
|
|
158
|
+
...(language !== undefined ? { language } : {}),
|
|
159
|
+
...(segments !== undefined ? { segments } : {}),
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
}
|
package/src/adapters/video.ts
CHANGED
|
@@ -66,7 +66,7 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
|
|
|
66
66
|
Record<TModel, FalVideoProviderOptions<TModel>>,
|
|
67
67
|
Record<TModel, FalModelVideoSize<TModel>>
|
|
68
68
|
> {
|
|
69
|
-
readonly kind = 'video' as const
|
|
69
|
+
override readonly kind = 'video' as const
|
|
70
70
|
readonly name = 'fal' as const
|
|
71
71
|
|
|
72
72
|
constructor(model: TModel, config?: FalClientConfig) {
|
|
@@ -124,10 +124,11 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
|
|
|
124
124
|
return {
|
|
125
125
|
jobId,
|
|
126
126
|
status: mapFalStatusToVideoStatus(statusResponse.status),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
...(statusResponse.queue_position != null
|
|
128
|
+
? {
|
|
129
|
+
progress: Math.max(0, 100 - statusResponse.queue_position * 10),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -137,10 +138,11 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
|
|
|
137
138
|
result = await fal.queue.result(this.model, {
|
|
138
139
|
requestId: jobId,
|
|
139
140
|
})
|
|
140
|
-
} catch (error:
|
|
141
|
+
} catch (error: unknown) {
|
|
141
142
|
// fal.ai may report COMPLETED status but throw on result fetch
|
|
142
143
|
// (e.g. 422 validation errors). Extract the detailed error info.
|
|
143
|
-
const
|
|
144
|
+
const err = error as { body?: { detail?: unknown }; message?: string }
|
|
145
|
+
const detail = err.body?.detail
|
|
144
146
|
if (Array.isArray(detail)) {
|
|
145
147
|
const messages = detail.map(
|
|
146
148
|
(d: { msg?: string; loc?: Array<string> }) =>
|
|
@@ -149,7 +151,7 @@ export class FalVideoAdapter<TModel extends FalModel> extends BaseVideoAdapter<
|
|
|
149
151
|
throw new Error(`Video generation failed: ${messages.join('; ')}`)
|
|
150
152
|
}
|
|
151
153
|
throw new Error(
|
|
152
|
-
`Failed to retrieve video result: ${
|
|
154
|
+
`Failed to retrieve video result: ${err.message || String(error)}`,
|
|
153
155
|
)
|
|
154
156
|
}
|
|
155
157
|
|
package/src/model-meta.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type FalModel = keyof EndpointTypeMap | (string & {})
|
|
|
23
23
|
export type FalModelInput<TModel extends string> =
|
|
24
24
|
TModel extends keyof EndpointTypeMap
|
|
25
25
|
? EndpointTypeMap[TModel]['input']
|
|
26
|
-
: Record<string,
|
|
26
|
+
: Record<string, unknown>
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Utility type to extract the output type for a specific fal model.
|
|
@@ -125,7 +125,7 @@ export type FalModelVideoSizeInput<TModel extends string> =
|
|
|
125
125
|
export type FalVideoProviderOptions<TModel extends string> =
|
|
126
126
|
TModel extends keyof EndpointTypeMap
|
|
127
127
|
? Omit<FalModelInput<TModel>, 'prompt'>
|
|
128
|
-
: Record<string,
|
|
128
|
+
: Record<string, unknown>
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Provider options for TTS, excluding fields TanStack AI handles.
|
package/src/utils/client.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function extractUrlExtension(url: string): string | undefined {
|
|
|
38
38
|
} catch {
|
|
39
39
|
// Fall back to treating the input as a raw path when URL parsing fails
|
|
40
40
|
// (e.g. the caller passed a bare path). Still strip ?query and #fragment.
|
|
41
|
-
pathname = url.split('?')[0]
|
|
41
|
+
pathname = url.split('?')[0]?.split('#')[0] ?? url
|
|
42
42
|
}
|
|
43
43
|
// Drop trailing slashes so `/path/audio.mp3/` still yields `mp3`.
|
|
44
44
|
const normalized = pathname.replace(/\/+$/, '')
|
|
@@ -83,6 +83,7 @@ export function deriveAudioContentType(
|
|
|
83
83
|
return 'audio/mp4'
|
|
84
84
|
case 'webm':
|
|
85
85
|
return 'audio/webm'
|
|
86
|
+
case undefined:
|
|
86
87
|
default:
|
|
87
88
|
return 'audio/mpeg'
|
|
88
89
|
}
|
|
@@ -132,8 +133,8 @@ export function arrayBufferToBase64(bytes: ArrayBuffer): string {
|
|
|
132
133
|
}
|
|
133
134
|
const view = new Uint8Array(bytes)
|
|
134
135
|
let binary = ''
|
|
135
|
-
for (
|
|
136
|
-
binary += String.fromCharCode(
|
|
136
|
+
for (const byte of view) {
|
|
137
|
+
binary += String.fromCharCode(byte)
|
|
137
138
|
}
|
|
138
139
|
return btoa(binary)
|
|
139
140
|
}
|
|
@@ -13,12 +13,12 @@ export function mapVideoSizeToFalFormat<TModel extends string>(
|
|
|
13
13
|
return {
|
|
14
14
|
aspect_ratio,
|
|
15
15
|
resolution,
|
|
16
|
-
} as
|
|
16
|
+
} as FalModelVideoSizeInput<TModel>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
if (size.includes(':')) {
|
|
20
|
-
return { aspect_ratio: size } as
|
|
20
|
+
return { aspect_ratio: size } as FalModelVideoSizeInput<TModel>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
return { resolution: size } as
|
|
23
|
+
return { resolution: size } as FalModelVideoSizeInput<TModel>
|
|
24
24
|
}
|