@tanstack/ai-fal 0.10.0 → 0.10.1
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.
|
@@ -69,7 +69,8 @@ var FalSpeechAdapter = class extends BaseTTSAdapter {
|
|
|
69
69
|
if (!audioUrl) throw new Error("Audio URL not found in fal TTS response");
|
|
70
70
|
const audioResponse = await fetch(audioUrl);
|
|
71
71
|
if (!audioResponse.ok) throw new Error(`Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`);
|
|
72
|
-
const
|
|
72
|
+
const arrayBuffer = await audioResponse.arrayBuffer();
|
|
73
|
+
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
73
74
|
const contentTypeMime = contentType?.split(";")[0]?.trim();
|
|
74
75
|
const rawFormat = extractUrlExtension(audioUrl) || contentTypeMime?.split("/")[1] || "wav";
|
|
75
76
|
const format = rawFormat === "mpeg" ? "mp3" : rawFormat;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"speech.js","names":[],"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/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils/client'\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 // Request-specific abortSignal only — not fal.config() (global).\n const result = await fal.subscribe(this.model, {\n input,\n ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),\n })\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAa,mBAAb,cAA+D,eAG7D;CACA,OAAgB;CAEhB,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,eACJ,SACoB;EACpB,MAAM,EAAE,WAAW;EACnB,OAAO,QAAQ,8CAA8C,KAAK,SAAS;GACzE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EACD,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,OAAO;GAErC,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO;IAC7C;IACA,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GACpE,CAAC;GACD,OAAO,MAAM,KAAK,kBAAkB,MAAM;EAC5C,SAAS,OAAO;GACd,OAAO,OAAO,4BAA4B;IACxC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SACuB;EASvB,OAAO;GAPL,GAAG,QAAQ;GAEX,QAAQ,QAAQ;GAChB,MAAM,QAAQ;GACd,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAChD,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;EAE3C;CACT;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,MAAc,kBACZ,UACoB;EACpB,MAAM,OAAO,SAAS;EAGtB,IAAI;EACJ,IAAI;EAEJ,IACE,KAAK,YACL,OAAO,KAAK,aAAa,YACzB,SAAS,KAAK,UACd;GACA,MAAM,WAAW,KAAK;GACtB,WAAW,SAAS;GACpB,cAAc,SAAS;EACzB,OAAO,IAAI,OAAO,KAAK,cAAc,UACnC,WAAW,KAAK;EAGlB,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,yCAAyC;EAM3D,MAAM,gBAAgB,MAAM,MAAM,QAAQ;EAC1C,IAAI,CAAC,cAAc,IACjB,MAAM,IAAI,MACR,8BAA8B,SAAS,IAAI,cAAc,OAAO,GAAG,cAAc,YACnF;
|
|
1
|
+
{"version":3,"file":"speech.js","names":[],"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/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\nimport type { OutputType, Result } from '@fal-ai/client'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils/client'\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 // Request-specific abortSignal only — not fal.config() (global).\n const result = await fal.subscribe(this.model, {\n input,\n ...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),\n })\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAa,mBAAb,cAA+D,eAG7D;CACA,OAAgB;CAEhB,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,eACJ,SACoB;EACpB,MAAM,EAAE,WAAW;EACnB,OAAO,QAAQ,8CAA8C,KAAK,SAAS;GACzE,UAAU;GACV,OAAO,KAAK;EACd,CAAC;EACD,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,OAAO;GAErC,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO;IAC7C;IACA,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GACpE,CAAC;GACD,OAAO,MAAM,KAAK,kBAAkB,MAAM;EAC5C,SAAS,OAAO;GACd,OAAO,OAAO,4BAA4B;IACxC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SACuB;EASvB,OAAO;GAPL,GAAG,QAAQ;GAEX,QAAQ,QAAQ;GAChB,MAAM,QAAQ;GACd,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAChD,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;EAE3C;CACT;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,MAAc,kBACZ,UACoB;EACpB,MAAM,OAAO,SAAS;EAGtB,IAAI;EACJ,IAAI;EAEJ,IACE,KAAK,YACL,OAAO,KAAK,aAAa,YACzB,SAAS,KAAK,UACd;GACA,MAAM,WAAW,KAAK;GACtB,WAAW,SAAS;GACpB,cAAc,SAAS;EACzB,OAAO,IAAI,OAAO,KAAK,cAAc,UACnC,WAAW,KAAK;EAGlB,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,yCAAyC;EAM3D,MAAM,gBAAgB,MAAM,MAAM,QAAQ;EAC1C,IAAI,CAAC,cAAc,IACjB,MAAM,IAAI,MACR,8BAA8B,SAAS,IAAI,cAAc,OAAO,GAAG,cAAc,YACnF;EAEF,MAAM,cAAc,MAAM,cAAc,YAAY;EACpD,MAAM,SAAS,oBAAoB,WAAW;EAI9C,MAAM,kBAAkB,aAAa,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK;EAMzD,MAAM,YALmB,oBAAoB,QAM3C,KAAoB,iBAAiB,MAAM,GAAG,CAAC,CAAC,MAAM;EACxD,MAAM,SAAS,cAAc,SAAS,QAAQ;EAE9C,MAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;EAEjE,OAAO;GACL,IAAI,SAAS,aAAa,KAAK,WAAW;GAC1C,OAAO,KAAK;GACZ,OAAO;GACP;GACA,aAAa,mBAAmB,SAAS;GACzC,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;AACF;AAEA,SAAgB,UACd,OACA,QAC0B;CAC1B,OAAO,IAAI,iBAAiB,OAAO,MAAM;AAC3C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-provider-options.js","names":[],"sources":["../../../src/image/image-provider-options.ts"],"sourcesContent":["import type { FalModelImageSize, FalModelImageSizeInput } from '../model-meta'\n\nexport function mapSizeToFalFormat<TModel extends string>(\n size: FalModelImageSize<TModel> | undefined,\n): FalModelImageSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_4K\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // \"4K\" → { resolution } (no colon, no underscore, model has `resolution`)\n // \"square_hd\" → { image_size } (no colon, no resolution field on model)\n if (typeof size === 'string') {\n if (size.includes('_')) {\n const [first, second] = size.split('_')\n if (first && first.includes(':')) {\n return {\n aspect_ratio: first,\n resolution: second,\n } as FalModelImageSizeInput<TModel>\n }\n } else if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelImageSizeInput<TModel>\n }\n }\n\n return {\n image_size: size,\n } as FalModelImageSizeInput<TModel>\n}\n"],"mappings":";AAEA,SAAgB,mBACd,MAC4C;CAC5C,IAAI,CAAC,MAAM,OAAO,KAAA;CAMlB,IAAI,OAAO,SAAS;
|
|
1
|
+
{"version":3,"file":"image-provider-options.js","names":[],"sources":["../../../src/image/image-provider-options.ts"],"sourcesContent":["import type { FalModelImageSize, FalModelImageSizeInput } from '../model-meta'\n\nexport function mapSizeToFalFormat<TModel extends string>(\n size: FalModelImageSize<TModel> | undefined,\n): FalModelImageSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_4K\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // \"4K\" → { resolution } (no colon, no underscore, model has `resolution`)\n // \"square_hd\" → { image_size } (no colon, no resolution field on model)\n if (typeof size === 'string') {\n if (size.includes('_')) {\n const [first, second] = size.split('_')\n if (first && first.includes(':')) {\n return {\n aspect_ratio: first,\n resolution: second,\n } as FalModelImageSizeInput<TModel>\n }\n } else if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelImageSizeInput<TModel>\n }\n }\n\n return {\n image_size: size,\n } as FalModelImageSizeInput<TModel>\n}\n"],"mappings":";AAEA,SAAgB,mBACd,MAC4C;CAC5C,IAAI,CAAC,MAAM,OAAO,KAAA;CAMlB,IAAI,OAAO,SAAS,UAAU;EAC5B,IAAI,KAAK,SAAS,GAAG,GAAG;GACtB,MAAM,CAAC,OAAO,UAAU,KAAK,MAAM,GAAG;GACtC,IAAI,SAAS,MAAM,SAAS,GAAG,GAC7B,OAAO;IACL,cAAc;IACd,YAAY;GACd;EAEJ,OAAO,IAAI,KAAK,SAAS,GAAG,GAC1B,OAAO,EAAE,cAAc,KAAK;CAEhC;CAEA,OAAO,EACL,YAAY,KACd;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-fal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "fal.ai adapter for TanStack AI image, video, audio, speech, and transcription generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"@tanstack/ai-utils": "^0.4.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@vitest/coverage-v8": "4.
|
|
56
|
-
"vite": "^8.1
|
|
57
|
-
"@tanstack/ai": "0.
|
|
55
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
56
|
+
"vite": "^8.2.1",
|
|
57
|
+
"@tanstack/ai": "0.45.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@tanstack/ai": "^0.
|
|
60
|
+
"@tanstack/ai": "^0.45.0"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "vite build",
|