@tanstack/ai-fal 0.9.12 → 0.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapters/audio.js +92 -76
- package/dist/esm/adapters/audio.js.map +1 -1
- package/dist/esm/adapters/image.js +99 -108
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/speech.js +86 -83
- package/dist/esm/adapters/speech.js.map +1 -1
- package/dist/esm/adapters/transcription.js +90 -89
- package/dist/esm/adapters/transcription.js.map +1 -1
- package/dist/esm/adapters/video.js +146 -150
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/image/generated/image-field-overrides.js +809 -367
- package/dist/esm/image/generated/image-field-overrides.js.map +1 -1
- package/dist/esm/image/image-inputs.js +143 -104
- package/dist/esm/image/image-inputs.js.map +1 -1
- package/dist/esm/image/image-provider-options.js +16 -21
- package/dist/esm/image/image-provider-options.js.map +1 -1
- package/dist/esm/index.js +2 -17
- package/dist/esm/utils/billing.js +89 -45
- package/dist/esm/utils/billing.js.map +1 -1
- package/dist/esm/utils/client.js +90 -87
- package/dist/esm/utils/client.js.map +1 -1
- package/dist/esm/video/video-provider-options.js +15 -16
- package/dist/esm/video/video-provider-options.js.map +1 -1
- package/package.json +7 -7
- package/dist/esm/index.js.map +0 -1
|
@@ -1,87 +1,90 @@
|
|
|
1
|
+
import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
|
|
2
|
+
import { arrayBufferToBase64, configureFalClient, extractUrlExtension, generateId } from "../utils/client.js";
|
|
1
3
|
import { fal } from "@fal-ai/client";
|
|
2
4
|
import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
5
|
+
//#region src/adapters/speech.ts
|
|
6
|
+
/**
|
|
7
|
+
* fal.ai text-to-speech adapter.
|
|
8
|
+
*
|
|
9
|
+
* Supports fal.ai TTS models like kokoro, elevenlabs, etc.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const adapter = falSpeech('fal-ai/kokoro/american-english')
|
|
14
|
+
* const result = await generateSpeech({
|
|
15
|
+
* adapter,
|
|
16
|
+
* text: 'Hello, world!',
|
|
17
|
+
* voice: 'af_heart',
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var FalSpeechAdapter = class extends BaseTTSAdapter {
|
|
22
|
+
name = "fal";
|
|
23
|
+
constructor(model, config) {
|
|
24
|
+
super(model, {});
|
|
25
|
+
configureFalClient(config);
|
|
26
|
+
}
|
|
27
|
+
async generateSpeech(options) {
|
|
28
|
+
const { logger } = options;
|
|
29
|
+
logger.request(`activity=generateSpeech provider=fal model=${this.model}`, {
|
|
30
|
+
provider: "fal",
|
|
31
|
+
model: this.model
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const input = this.buildInput(options);
|
|
35
|
+
const result = await fal.subscribe(this.model, { input });
|
|
36
|
+
return await this.transformResponse(result);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.errors("fal.generateSpeech fatal", {
|
|
39
|
+
error,
|
|
40
|
+
source: "fal.generateSpeech"
|
|
41
|
+
});
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
buildInput(options) {
|
|
46
|
+
return {
|
|
47
|
+
...options.modelOptions,
|
|
48
|
+
prompt: options.text,
|
|
49
|
+
text: options.text,
|
|
50
|
+
...options.voice ? { voice: options.voice } : {},
|
|
51
|
+
...options.speed ? { speed: options.speed } : {}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
generateId() {
|
|
55
|
+
return generateId(this.name);
|
|
56
|
+
}
|
|
57
|
+
async transformResponse(response) {
|
|
58
|
+
const data = response.data;
|
|
59
|
+
let audioUrl;
|
|
60
|
+
let contentType;
|
|
61
|
+
if (data["audio"] && typeof data["audio"] === "object" && "url" in data["audio"]) {
|
|
62
|
+
const audioObj = data["audio"];
|
|
63
|
+
audioUrl = audioObj.url;
|
|
64
|
+
contentType = audioObj.content_type;
|
|
65
|
+
} else if (typeof data.audio_url === "string") audioUrl = data.audio_url;
|
|
66
|
+
if (!audioUrl) throw new Error("Audio URL not found in fal TTS response");
|
|
67
|
+
const audioResponse = await fetch(audioUrl);
|
|
68
|
+
if (!audioResponse.ok) throw new Error(`Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`);
|
|
69
|
+
const base64 = arrayBufferToBase64(await audioResponse.arrayBuffer());
|
|
70
|
+
const contentTypeMime = contentType?.split(";")[0]?.trim();
|
|
71
|
+
const rawFormat = extractUrlExtension(audioUrl) || contentTypeMime?.split("/")[1] || "wav";
|
|
72
|
+
const format = rawFormat === "mpeg" ? "mp3" : rawFormat;
|
|
73
|
+
const usage = buildFalUsage(takeBillableUnits(response.requestId));
|
|
74
|
+
return {
|
|
75
|
+
id: response.requestId || this.generateId(),
|
|
76
|
+
model: this.model,
|
|
77
|
+
audio: base64,
|
|
78
|
+
format,
|
|
79
|
+
contentType: contentTypeMime || `audio/${format}`,
|
|
80
|
+
...usage ? { usage } : {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
80
84
|
function falSpeech(model, config) {
|
|
81
|
-
|
|
85
|
+
return new FalSpeechAdapter(model, config);
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//# sourceMappingURL=speech.js.map
|
|
87
|
+
//#endregion
|
|
88
|
+
export { FalSpeechAdapter, falSpeech };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=speech.js.map
|
|
@@ -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/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 const result = await fal.subscribe(this.model, { input })\n return await this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateSpeech fatal', {\n error,\n source: 'fal.generateSpeech',\n })\n throw error\n }\n }\n\n private buildInput(\n options: TTSOptions<FalSpeechProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n const input = {\n ...options.modelOptions,\n // Map text to both prompt and text fields (different models use different fields)\n prompt: options.text,\n text: options.text,\n ...(options.voice ? { voice: options.voice } : {}),\n ...(options.speed ? { speed: options.speed } : {}),\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private async transformResponse(\n response: Result<OutputType<TModel>>,\n ): Promise<TTSResult> {\n const data = response.data as Record<string, unknown>\n\n // fal returns { audio: { url, content_type } } or { audio_url: string }\n let audioUrl: string | undefined\n let contentType: string | undefined\n\n if (\n data['audio'] &&\n typeof data['audio'] === 'object' &&\n 'url' in data['audio']\n ) {\n const audioObj = data['audio'] as { url: string; content_type?: string }\n audioUrl = audioObj.url\n contentType = audioObj.content_type\n } else if (typeof data.audio_url === 'string') {\n audioUrl = data.audio_url\n }\n\n if (!audioUrl) {\n throw new Error('Audio URL not found in fal TTS response')\n }\n\n // Fetch the audio and convert to base64 to match TTSResult contract.\n // Using a chunked helper here — spreading Uint8Array into btoa exceeds\n // V8's argument limit (~65k) for any realistic TTS clip.\n const audioResponse = await fetch(audioUrl)\n if (!audioResponse.ok) {\n throw new Error(\n `Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`,\n )\n }\n const arrayBuffer = await audioResponse.arrayBuffer()\n const base64 = arrayBufferToBase64(arrayBuffer)\n\n // Strip parameters like `; charset=...` from contentType, and only use\n // the URL extension as a fallback when it looks like a real extension.\n const contentTypeMime = contentType?.split(';')[0]?.trim()\n const safeUrlExtension = extractUrlExtension(audioUrl)\n // Prefer URL-derived extension when available (more canonical for file\n // consumers), otherwise derive from the content-type mime subtype, then\n // fall back to `wav`. Normalize `mpeg` → `mp3` so the format field is a\n // usable file extension rather than the IANA subtype.\n const rawFormat =\n safeUrlExtension || contentTypeMime?.split('/')[1] || 'wav'\n const format = rawFormat === 'mpeg' ? 'mp3' : rawFormat\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: base64,\n format,\n contentType: contentTypeMime || `audio/${format}`,\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falSpeech<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalSpeechAdapter<TModel> {\n return new FalSpeechAdapter(model, config)\n}\n"],"
|
|
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 const result = await fal.subscribe(this.model, { input })\n return await this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateSpeech fatal', {\n error,\n source: 'fal.generateSpeech',\n })\n throw error\n }\n }\n\n private buildInput(\n options: TTSOptions<FalSpeechProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n const input = {\n ...options.modelOptions,\n // Map text to both prompt and text fields (different models use different fields)\n prompt: options.text,\n text: options.text,\n ...(options.voice ? { voice: options.voice } : {}),\n ...(options.speed ? { speed: options.speed } : {}),\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private async transformResponse(\n response: Result<OutputType<TModel>>,\n ): Promise<TTSResult> {\n const data = response.data as Record<string, unknown>\n\n // fal returns { audio: { url, content_type } } or { audio_url: string }\n let audioUrl: string | undefined\n let contentType: string | undefined\n\n if (\n data['audio'] &&\n typeof data['audio'] === 'object' &&\n 'url' in data['audio']\n ) {\n const audioObj = data['audio'] as { url: string; content_type?: string }\n audioUrl = audioObj.url\n contentType = audioObj.content_type\n } else if (typeof data.audio_url === 'string') {\n audioUrl = data.audio_url\n }\n\n if (!audioUrl) {\n throw new Error('Audio URL not found in fal TTS response')\n }\n\n // Fetch the audio and convert to base64 to match TTSResult contract.\n // Using a chunked helper here — spreading Uint8Array into btoa exceeds\n // V8's argument limit (~65k) for any realistic TTS clip.\n const audioResponse = await fetch(audioUrl)\n if (!audioResponse.ok) {\n throw new Error(\n `Failed to fetch audio from ${audioUrl}: ${audioResponse.status} ${audioResponse.statusText}`,\n )\n }\n const arrayBuffer = await audioResponse.arrayBuffer()\n const base64 = arrayBufferToBase64(arrayBuffer)\n\n // Strip parameters like `; charset=...` from contentType, and only use\n // the URL extension as a fallback when it looks like a real extension.\n const contentTypeMime = contentType?.split(';')[0]?.trim()\n const safeUrlExtension = extractUrlExtension(audioUrl)\n // Prefer URL-derived extension when available (more canonical for file\n // consumers), otherwise derive from the content-type mime subtype, then\n // fall back to `wav`. Normalize `mpeg` → `mp3` so the format field is a\n // usable file extension rather than the IANA subtype.\n const rawFormat =\n safeUrlExtension || contentTypeMime?.split('/')[1] || 'wav'\n const format = rawFormat === 'mpeg' ? 'mp3' : rawFormat\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n audio: base64,\n format,\n contentType: contentTypeMime || `audio/${format}`,\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falSpeech<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalSpeechAdapter<TModel> {\n return new FalSpeechAdapter(model, config)\n}\n"],"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;GACrC,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,MAAM,CAAC;GACxD,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;EAGF,MAAM,SAAS,oBAAoB,MADT,cAAc,YAAY,CACN;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,93 +1,94 @@
|
|
|
1
|
+
import { buildFalUsage, takeBillableUnits } from "../utils/billing.js";
|
|
2
|
+
import { configureFalClient, dataUrlToBlob, generateId } from "../utils/client.js";
|
|
1
3
|
import { fal } from "@fal-ai/client";
|
|
2
4
|
import { BaseTranscriptionAdapter } from "@tanstack/ai/adapters";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
5
|
+
//#region src/adapters/transcription.ts
|
|
6
|
+
/**
|
|
7
|
+
* fal.ai transcription (speech-to-text) adapter.
|
|
8
|
+
*
|
|
9
|
+
* Supports fal.ai STT models like whisper, wizper, etc.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const adapter = falTranscription('fal-ai/whisper')
|
|
14
|
+
* const result = await generateTranscription({
|
|
15
|
+
* adapter,
|
|
16
|
+
* audio: 'https://example.com/audio.mp3',
|
|
17
|
+
* language: 'en',
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var FalTranscriptionAdapter = class extends BaseTranscriptionAdapter {
|
|
22
|
+
name = "fal";
|
|
23
|
+
constructor(model, config) {
|
|
24
|
+
super(model, {});
|
|
25
|
+
configureFalClient(config);
|
|
26
|
+
}
|
|
27
|
+
async transcribe(options) {
|
|
28
|
+
const { logger } = options;
|
|
29
|
+
logger.request(`activity=generateTranscription provider=fal model=${this.model}`, {
|
|
30
|
+
provider: "fal",
|
|
31
|
+
model: this.model
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const input = this.buildInput(options);
|
|
35
|
+
const result = await fal.subscribe(this.model, { input });
|
|
36
|
+
return this.transformResponse(result);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.errors("fal.generateTranscription fatal", {
|
|
39
|
+
error,
|
|
40
|
+
source: "fal.generateTranscription"
|
|
41
|
+
});
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
buildInput(options) {
|
|
46
|
+
let audioInput;
|
|
47
|
+
if (options.audio instanceof ArrayBuffer) audioInput = new Blob([options.audio]);
|
|
48
|
+
else if (typeof options.audio === "string") audioInput = dataUrlToBlob(options.audio) ?? options.audio;
|
|
49
|
+
else audioInput = options.audio;
|
|
50
|
+
return {
|
|
51
|
+
...options.modelOptions,
|
|
52
|
+
audio_url: audioInput,
|
|
53
|
+
...options.language ? { language: options.language } : {},
|
|
54
|
+
...options.prompt ? { prompt: options.prompt } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
generateId() {
|
|
58
|
+
return generateId(this.name);
|
|
59
|
+
}
|
|
60
|
+
transformResponse(response) {
|
|
61
|
+
const data = response.data;
|
|
62
|
+
const text = data.text || "";
|
|
63
|
+
let segments;
|
|
64
|
+
const chunks = data.chunks;
|
|
65
|
+
if (chunks && Array.isArray(chunks)) segments = chunks.flatMap((chunk, index) => {
|
|
66
|
+
const ts = chunk.timestamp;
|
|
67
|
+
if (!Array.isArray(ts) || ts.length < 2 || typeof ts[0] !== "number" || typeof ts[1] !== "number") return [];
|
|
68
|
+
return [{
|
|
69
|
+
id: index,
|
|
70
|
+
start: ts[0],
|
|
71
|
+
end: ts[1],
|
|
72
|
+
text: chunk.text,
|
|
73
|
+
...chunk.speaker ? { speaker: chunk.speaker } : {}
|
|
74
|
+
}];
|
|
75
|
+
});
|
|
76
|
+
const language = data.language || data.inferred_languages?.[0] || data.languages?.[0];
|
|
77
|
+
const usage = buildFalUsage(takeBillableUnits(response.requestId));
|
|
78
|
+
return {
|
|
79
|
+
id: response.requestId || this.generateId(),
|
|
80
|
+
model: this.model,
|
|
81
|
+
text,
|
|
82
|
+
...language !== void 0 ? { language } : {},
|
|
83
|
+
...segments !== void 0 ? { segments } : {},
|
|
84
|
+
...usage ? { usage } : {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
86
88
|
function falTranscription(model, config) {
|
|
87
|
-
|
|
89
|
+
return new FalTranscriptionAdapter(model, config);
|
|
88
90
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//# sourceMappingURL=transcription.js.map
|
|
91
|
+
//#endregion
|
|
92
|
+
export { FalTranscriptionAdapter, falTranscription };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=transcription.js.map
|
|
@@ -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/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\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/client'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Provider options for transcription, excluding fields TanStack AI handles.\n */\nexport type FalTranscriptionProviderOptions<TModel extends string> = Omit<\n FalModelInput<TModel>,\n 'audio_url'\n>\n\ninterface FalChunk {\n text: string\n timestamp?: [number, number] | null\n speaker?: string\n}\n\n/**\n * fal.ai transcription (speech-to-text) adapter.\n *\n * Supports fal.ai STT models like whisper, wizper, etc.\n *\n * @example\n * ```typescript\n * const adapter = falTranscription('fal-ai/whisper')\n * const result = await generateTranscription({\n * adapter,\n * audio: 'https://example.com/audio.mp3',\n * language: 'en',\n * })\n * ```\n */\nexport class FalTranscriptionAdapter<\n TModel extends FalModel,\n> extends BaseTranscriptionAdapter<\n TModel,\n FalTranscriptionProviderOptions<TModel>\n> {\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super(model, {})\n configureFalClient(config)\n }\n\n async transcribe(\n options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n logger.request(\n `activity=generateTranscription provider=fal model=${this.model}`,\n {\n provider: 'fal',\n model: this.model,\n },\n )\n try {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateTranscription fatal', {\n error,\n source: 'fal.generateTranscription',\n })\n throw error\n }\n }\n\n private buildInput(\n options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n // fal-client auto-uploads Blob/File inputs via fal.storage.upload, but\n // passes strings through unchanged — so a `data:` URL would reach fal's\n // API and get rejected with a 422 \"Unsupported data URL\". Decode data\n // URLs to a Blob up front so the auto-upload path handles them.\n let audioInput: string | Blob | File\n if (options.audio instanceof ArrayBuffer) {\n audioInput = new Blob([options.audio])\n } else if (typeof options.audio === 'string') {\n audioInput = dataUrlToBlob(options.audio) ?? options.audio\n } else {\n audioInput = options.audio\n }\n\n const input = {\n ...options.modelOptions,\n audio_url: audioInput,\n ...(options.language ? { language: options.language } : {}),\n ...(options.prompt ? { prompt: options.prompt } : {}),\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): TranscriptionResult {\n const data = response.data as Record<string, unknown>\n\n const text = (data.text as string) || ''\n\n // Map fal chunks to TanStack segments. fal whisper can return\n // `timestamp: null` on some chunks (e.g. when word-level timing is\n // disabled), and the runtime payload is not actually constrained to a\n // 2-tuple — treat it as unknown and validate before indexing.\n let segments: Array<TranscriptionSegment> | undefined\n const chunks = data.chunks as Array<FalChunk> | undefined\n if (chunks && Array.isArray(chunks)) {\n segments = chunks.flatMap((chunk, index) => {\n const ts = chunk.timestamp as unknown\n if (\n !Array.isArray(ts) ||\n ts.length < 2 ||\n typeof ts[0] !== 'number' ||\n typeof ts[1] !== 'number'\n ) {\n return []\n }\n return [\n {\n id: index,\n start: ts[0],\n end: ts[1],\n text: chunk.text,\n ...(chunk.speaker ? { speaker: chunk.speaker } : {}),\n },\n ]\n })\n }\n\n // Extract language from response\n const language =\n (data.language as string) ||\n (data.inferred_languages as Array<string> | undefined)?.[0] ||\n (data.languages as Array<string> | undefined)?.[0]\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n text,\n ...(language !== undefined ? { language } : {}),\n ...(segments !== undefined ? { segments } : {}),\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falTranscription<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalTranscriptionAdapter<TModel> {\n return new FalTranscriptionAdapter(model, config)\n}\n"],"
|
|
1
|
+
{"version":3,"file":"transcription.js","names":[],"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/client'\nimport { buildFalUsage, takeBillableUnits } from '../utils/billing'\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/client'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * Provider options for transcription, excluding fields TanStack AI handles.\n */\nexport type FalTranscriptionProviderOptions<TModel extends string> = Omit<\n FalModelInput<TModel>,\n 'audio_url'\n>\n\ninterface FalChunk {\n text: string\n timestamp?: [number, number] | null\n speaker?: string\n}\n\n/**\n * fal.ai transcription (speech-to-text) adapter.\n *\n * Supports fal.ai STT models like whisper, wizper, etc.\n *\n * @example\n * ```typescript\n * const adapter = falTranscription('fal-ai/whisper')\n * const result = await generateTranscription({\n * adapter,\n * audio: 'https://example.com/audio.mp3',\n * language: 'en',\n * })\n * ```\n */\nexport class FalTranscriptionAdapter<\n TModel extends FalModel,\n> extends BaseTranscriptionAdapter<\n TModel,\n FalTranscriptionProviderOptions<TModel>\n> {\n readonly name = 'fal' as const\n\n constructor(model: TModel, config?: FalClientConfig) {\n super(model, {})\n configureFalClient(config)\n }\n\n async transcribe(\n options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>,\n ): Promise<TranscriptionResult> {\n const { logger } = options\n logger.request(\n `activity=generateTranscription provider=fal model=${this.model}`,\n {\n provider: 'fal',\n model: this.model,\n },\n )\n try {\n const input = this.buildInput(options)\n const result = await fal.subscribe(this.model, { input })\n return this.transformResponse(result)\n } catch (error) {\n logger.errors('fal.generateTranscription fatal', {\n error,\n source: 'fal.generateTranscription',\n })\n throw error\n }\n }\n\n private buildInput(\n options: TranscriptionOptions<FalTranscriptionProviderOptions<TModel>>,\n ): FalModelInput<TModel> {\n // fal-client auto-uploads Blob/File inputs via fal.storage.upload, but\n // passes strings through unchanged — so a `data:` URL would reach fal's\n // API and get rejected with a 422 \"Unsupported data URL\". Decode data\n // URLs to a Blob up front so the auto-upload path handles them.\n let audioInput: string | Blob | File\n if (options.audio instanceof ArrayBuffer) {\n audioInput = new Blob([options.audio])\n } else if (typeof options.audio === 'string') {\n audioInput = dataUrlToBlob(options.audio) ?? options.audio\n } else {\n audioInput = options.audio\n }\n\n const input = {\n ...options.modelOptions,\n audio_url: audioInput,\n ...(options.language ? { language: options.language } : {}),\n ...(options.prompt ? { prompt: options.prompt } : {}),\n } as FalModelInput<TModel>\n return input\n }\n\n protected override generateId(): string {\n return utilGenerateId(this.name)\n }\n\n private transformResponse(\n response: Result<OutputType<TModel>>,\n ): TranscriptionResult {\n const data = response.data as Record<string, unknown>\n\n const text = (data.text as string) || ''\n\n // Map fal chunks to TanStack segments. fal whisper can return\n // `timestamp: null` on some chunks (e.g. when word-level timing is\n // disabled), and the runtime payload is not actually constrained to a\n // 2-tuple — treat it as unknown and validate before indexing.\n let segments: Array<TranscriptionSegment> | undefined\n const chunks = data.chunks as Array<FalChunk> | undefined\n if (chunks && Array.isArray(chunks)) {\n segments = chunks.flatMap((chunk, index) => {\n const ts = chunk.timestamp as unknown\n if (\n !Array.isArray(ts) ||\n ts.length < 2 ||\n typeof ts[0] !== 'number' ||\n typeof ts[1] !== 'number'\n ) {\n return []\n }\n return [\n {\n id: index,\n start: ts[0],\n end: ts[1],\n text: chunk.text,\n ...(chunk.speaker ? { speaker: chunk.speaker } : {}),\n },\n ]\n })\n }\n\n // Extract language from response\n const language =\n (data.language as string) ||\n (data.inferred_languages as Array<string> | undefined)?.[0] ||\n (data.languages as Array<string> | undefined)?.[0]\n\n const usage = buildFalUsage(takeBillableUnits(response.requestId))\n\n return {\n id: response.requestId || this.generateId(),\n model: this.model,\n text,\n ...(language !== undefined ? { language } : {}),\n ...(segments !== undefined ? { segments } : {}),\n ...(usage ? { usage } : {}),\n }\n }\n}\n\nexport function falTranscription<TModel extends FalModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalTranscriptionAdapter<TModel> {\n return new FalTranscriptionAdapter(model, config)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,0BAAb,cAEU,yBAGR;CACA,OAAgB;CAEhB,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,mBAAmB,MAAM;CAC3B;CAEA,MAAM,WACJ,SAC8B;EAC9B,MAAM,EAAE,WAAW;EACnB,OAAO,QACL,qDAAqD,KAAK,SAC1D;GACE,UAAU;GACV,OAAO,KAAK;EACd,CACF;EACA,IAAI;GACF,MAAM,QAAQ,KAAK,WAAW,OAAO;GACrC,MAAM,SAAS,MAAM,IAAI,UAAU,KAAK,OAAO,EAAE,MAAM,CAAC;GACxD,OAAO,KAAK,kBAAkB,MAAM;EACtC,SAAS,OAAO;GACd,OAAO,OAAO,mCAAmC;IAC/C;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;CAEA,WACE,SACuB;EAKvB,IAAI;EACJ,IAAI,QAAQ,iBAAiB,aAC3B,aAAa,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC;OAChC,IAAI,OAAO,QAAQ,UAAU,UAClC,aAAa,cAAc,QAAQ,KAAK,KAAK,QAAQ;OAErD,aAAa,QAAQ;EASvB,OAAO;GALL,GAAG,QAAQ;GACX,WAAW;GACX,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;GACzD,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;EAE9C;CACT;CAEA,aAAwC;EACtC,OAAO,WAAe,KAAK,IAAI;CACjC;CAEA,kBACE,UACqB;EACrB,MAAM,OAAO,SAAS;EAEtB,MAAM,OAAQ,KAAK,QAAmB;EAMtC,IAAI;EACJ,MAAM,SAAS,KAAK;EACpB,IAAI,UAAU,MAAM,QAAQ,MAAM,GAChC,WAAW,OAAO,SAAS,OAAO,UAAU;GAC1C,MAAM,KAAK,MAAM;GACjB,IACE,CAAC,MAAM,QAAQ,EAAE,KACjB,GAAG,SAAS,KACZ,OAAO,GAAG,OAAO,YACjB,OAAO,GAAG,OAAO,UAEjB,OAAO,CAAC;GAEV,OAAO,CACL;IACE,IAAI;IACJ,OAAO,GAAG;IACV,KAAK,GAAG;IACR,MAAM,MAAM;IACZ,GAAI,MAAM,UAAU,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;GACpD,CACF;EACF,CAAC;EAIH,MAAM,WACH,KAAK,YACL,KAAK,qBAAmD,MACxD,KAAK,YAA0C;EAElD,MAAM,QAAQ,cAAc,kBAAkB,SAAS,SAAS,CAAC;EAEjE,OAAO;GACL,IAAI,SAAS,aAAa,KAAK,WAAW;GAC1C,OAAO,KAAK;GACZ;GACA,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;GAC7C,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;GAC7C,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EAC3B;CACF;AACF;AAEA,SAAgB,iBACd,OACA,QACiC;CACjC,OAAO,IAAI,wBAAwB,OAAO,MAAM;AAClD"}
|