@tanstack/ai-fal 0.13.4 → 0.14.0
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/live.d.ts +66 -0
- package/dist/esm/adapters/live.js +149 -0
- package/dist/esm/adapters/live.js.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -1
- package/package.json +5 -4
- package/src/adapters/live.ts +212 -0
- package/src/index.ts +19 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { BaseLiveVideoAdapter } from '@tanstack/ai/adapters';
|
|
2
|
+
import { LiveVideoGenerationOptions, LiveVideoGenerationResult } from '@tanstack/ai';
|
|
3
|
+
import { FalClientConfig } from '../utils/client.js';
|
|
4
|
+
export declare const FAL_LIVE_VIDEO_MODELS: readonly ["minimax/h3-max/director"];
|
|
5
|
+
export type FalLiveVideoModel = (typeof FAL_LIVE_VIDEO_MODELS)[number];
|
|
6
|
+
/**
|
|
7
|
+
* WMA / token app id for each public live model. Director's AsyncAPI lives at
|
|
8
|
+
* `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:
|
|
9
|
+
* `minimax/h3-max/director` parses as alias `h3-max`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FAL_LIVE_VIDEO_APP: {
|
|
12
|
+
readonly 'minimax/h3-max/director': "fal-ai/minimax-h3-max-director";
|
|
13
|
+
};
|
|
14
|
+
export declare function isFalLiveVideoModel(model: string): model is FalLiveVideoModel;
|
|
15
|
+
/**
|
|
16
|
+
* URLs a Director live proxy may forward. WMA talks to `wma.fal.run`
|
|
17
|
+
* (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to
|
|
18
|
+
* `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is
|
|
19
|
+
* rejected so the proxy cannot run arbitrary fal apps.
|
|
20
|
+
*/
|
|
21
|
+
export declare function allowedFalLiveVideoProxyTarget(raw: string): URL | null;
|
|
22
|
+
/**
|
|
23
|
+
* Provider options for fal live sessions. The browser applies resolution and
|
|
24
|
+
* aspect ratio on the WMA `configure` message. `tokenDuration` is the only
|
|
25
|
+
* server-side field: it sets how long the minted JWT lasts.
|
|
26
|
+
*/
|
|
27
|
+
export interface FalLiveVideoProviderOptions {
|
|
28
|
+
/** JWT lifetime in seconds. Defaults to 300. */
|
|
29
|
+
tokenDuration?: number;
|
|
30
|
+
}
|
|
31
|
+
export type FalLiveVideoModelProviderOptionsByName = {
|
|
32
|
+
[M in FalLiveVideoModel]: FalLiveVideoProviderOptions;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* fal live-video adapter. Mints a scoped JWT and returns the WMA app id on
|
|
36
|
+
* `result.model`. Open with `fal.realtime.open(wma(live.model))` through a
|
|
37
|
+
* server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key
|
|
38
|
+
* credentials.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { generateLiveVideo } from '@tanstack/ai'
|
|
43
|
+
* import { falLiveVideo } from '@tanstack/ai-fal'
|
|
44
|
+
*
|
|
45
|
+
* const live = await generateLiveVideo({
|
|
46
|
+
* adapter: falLiveVideo('minimax/h3-max/director'),
|
|
47
|
+
* prompt: 'A chef tosses noodles in a steel wok, flames leaping',
|
|
48
|
+
* })
|
|
49
|
+
* // live.model is 'fal-ai/minimax-h3-max-director'
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare class FalLiveVideoAdapter<TModel extends FalLiveVideoModel> extends BaseLiveVideoAdapter<TModel, FalLiveVideoProviderOptions> {
|
|
53
|
+
readonly name: "fal";
|
|
54
|
+
private readonly clientConfig;
|
|
55
|
+
private readonly apiKey;
|
|
56
|
+
constructor(model: TModel, config?: FalClientConfig);
|
|
57
|
+
createLiveVideo(options: LiveVideoGenerationOptions<FalLiveVideoProviderOptions>): Promise<LiveVideoGenerationResult>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.
|
|
61
|
+
*/
|
|
62
|
+
export declare function falLiveVideo<TModel extends FalLiveVideoModel>(model: TModel, config?: FalClientConfig): FalLiveVideoAdapter<TModel>;
|
|
63
|
+
/**
|
|
64
|
+
* Create a fal live-video adapter with an explicit API key.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createFalLiveVideo<TModel extends FalLiveVideoModel>(model: TModel, apiKey: string, config?: Omit<FalClientConfig, 'apiKey'>): FalLiveVideoAdapter<TModel>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { getFalApiKeyFromEnv } from "../utils/client.js";
|
|
2
|
+
import { BaseLiveVideoAdapter } from "@tanstack/ai/adapters";
|
|
3
|
+
import { generateId } from "@tanstack/ai-utils";
|
|
4
|
+
//#region src/adapters/live.ts
|
|
5
|
+
var DEFAULT_FAL_TOKEN_URL = "https://rest.fal.ai/tokens/";
|
|
6
|
+
var DEFAULT_TOKEN_DURATION_SECONDS = 300;
|
|
7
|
+
var FAL_LIVE_VIDEO_MODELS = ["minimax/h3-max/director"];
|
|
8
|
+
/**
|
|
9
|
+
* WMA / token app id for each public live model. Director's AsyncAPI lives at
|
|
10
|
+
* `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:
|
|
11
|
+
* `minimax/h3-max/director` parses as alias `h3-max`.
|
|
12
|
+
*/
|
|
13
|
+
var FAL_LIVE_VIDEO_APP = { "minimax/h3-max/director": "fal-ai/minimax-h3-max-director" };
|
|
14
|
+
function isFalLiveVideoModel(model) {
|
|
15
|
+
return FAL_LIVE_VIDEO_MODELS.includes(model);
|
|
16
|
+
}
|
|
17
|
+
var WMA_PROXY_PATHS = /* @__PURE__ */ new Set([
|
|
18
|
+
"/ice",
|
|
19
|
+
"/session",
|
|
20
|
+
"/session/heartbeat"
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* URLs a Director live proxy may forward. WMA talks to `wma.fal.run`
|
|
24
|
+
* (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to
|
|
25
|
+
* `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is
|
|
26
|
+
* rejected so the proxy cannot run arbitrary fal apps.
|
|
27
|
+
*/
|
|
28
|
+
function allowedFalLiveVideoProxyTarget(raw) {
|
|
29
|
+
let url;
|
|
30
|
+
try {
|
|
31
|
+
url = new URL(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (url.protocol !== "https:") return null;
|
|
36
|
+
if (url.username || url.password || url.port || url.search || url.hash) return null;
|
|
37
|
+
const path = url.pathname.replace(/\/$/, "") || "/";
|
|
38
|
+
if (url.hostname === "wma.fal.run") return WMA_PROXY_PATHS.has(path) ? url : null;
|
|
39
|
+
if (url.hostname === "fal.run") {
|
|
40
|
+
for (const app of Object.values(FAL_LIVE_VIDEO_APP)) if (path === `/${app}/ice`) return url;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function resolveFalLiveApiKey(config) {
|
|
45
|
+
return config?.apiKey ?? getFalApiKeyFromEnv();
|
|
46
|
+
}
|
|
47
|
+
function readFalToken(body) {
|
|
48
|
+
if (typeof body === "string" && body.length > 0) return body;
|
|
49
|
+
if (typeof body === "object" && body !== null && "token" in body) {
|
|
50
|
+
const token = body.token;
|
|
51
|
+
if (typeof token === "string" && token.length > 0) return token;
|
|
52
|
+
}
|
|
53
|
+
throw new Error("fal token response did not include a token.");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* fal live-video adapter. Mints a scoped JWT and returns the WMA app id on
|
|
57
|
+
* `result.model`. Open with `fal.realtime.open(wma(live.model))` through a
|
|
58
|
+
* server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key
|
|
59
|
+
* credentials.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { generateLiveVideo } from '@tanstack/ai'
|
|
64
|
+
* import { falLiveVideo } from '@tanstack/ai-fal'
|
|
65
|
+
*
|
|
66
|
+
* const live = await generateLiveVideo({
|
|
67
|
+
* adapter: falLiveVideo('minimax/h3-max/director'),
|
|
68
|
+
* prompt: 'A chef tosses noodles in a steel wok, flames leaping',
|
|
69
|
+
* })
|
|
70
|
+
* // live.model is 'fal-ai/minimax-h3-max-director'
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
var FalLiveVideoAdapter = class extends BaseLiveVideoAdapter {
|
|
74
|
+
name = "fal";
|
|
75
|
+
clientConfig;
|
|
76
|
+
apiKey;
|
|
77
|
+
constructor(model, config) {
|
|
78
|
+
super(model, {});
|
|
79
|
+
this.apiKey = resolveFalLiveApiKey(config);
|
|
80
|
+
this.clientConfig = config;
|
|
81
|
+
}
|
|
82
|
+
async createLiveVideo(options) {
|
|
83
|
+
const { logger, prompt, abortSignal, modelOptions } = options;
|
|
84
|
+
const tokenDuration = modelOptions?.tokenDuration ?? DEFAULT_TOKEN_DURATION_SECONDS;
|
|
85
|
+
const fetchImpl = this.clientConfig?.fetch ?? globalThis.fetch;
|
|
86
|
+
logger.request(`activity=generateLiveVideo provider=fal model=${this.model}`, {
|
|
87
|
+
provider: "fal",
|
|
88
|
+
model: this.model
|
|
89
|
+
});
|
|
90
|
+
if (typeof fetchImpl !== "function") throw new Error("global fetch is not available. Pass `fetch` in the fal adapter config.");
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetchImpl(DEFAULT_FAL_TOKEN_URL, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Key ${this.apiKey}`,
|
|
96
|
+
"Content-Type": "application/json"
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
allowed_apps: [FAL_LIVE_VIDEO_APP[this.model]],
|
|
100
|
+
token_expiration: tokenDuration
|
|
101
|
+
}),
|
|
102
|
+
...abortSignal ? { signal: abortSignal } : {}
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const detail = await response.text().catch(() => "");
|
|
106
|
+
throw new Error(`fal token request failed (${response.status}${response.statusText ? ` ${response.statusText}` : ""})${detail ? `: ${detail}` : ""}`);
|
|
107
|
+
}
|
|
108
|
+
const token = readFalToken(await response.json());
|
|
109
|
+
const result = {
|
|
110
|
+
id: generateId("liveVideo"),
|
|
111
|
+
model: FAL_LIVE_VIDEO_APP[this.model],
|
|
112
|
+
token,
|
|
113
|
+
expiresAt: Date.now() + tokenDuration * 1e3,
|
|
114
|
+
prompt,
|
|
115
|
+
status: "ready"
|
|
116
|
+
};
|
|
117
|
+
logger.output(`activity=generateLiveVideo provider=fal model=${this.model}`, {
|
|
118
|
+
model: this.model,
|
|
119
|
+
status: result.status
|
|
120
|
+
});
|
|
121
|
+
return result;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.errors("fal.createLiveVideo failed", {
|
|
124
|
+
error,
|
|
125
|
+
source: "fal.createLiveVideo"
|
|
126
|
+
});
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.
|
|
133
|
+
*/
|
|
134
|
+
function falLiveVideo(model, config) {
|
|
135
|
+
return new FalLiveVideoAdapter(model, config);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create a fal live-video adapter with an explicit API key.
|
|
139
|
+
*/
|
|
140
|
+
function createFalLiveVideo(model, apiKey, config) {
|
|
141
|
+
return new FalLiveVideoAdapter(model, {
|
|
142
|
+
...config,
|
|
143
|
+
apiKey
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalLiveVideoAdapter, allowedFalLiveVideoProxyTarget, createFalLiveVideo, falLiveVideo, isFalLiveVideoModel };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.js","names":[],"sources":["../../../src/adapters/live.ts"],"sourcesContent":["import { BaseLiveVideoAdapter } from '@tanstack/ai/adapters'\nimport { generateId } from '@tanstack/ai-utils'\nimport { getFalApiKeyFromEnv } from '../utils/client'\nimport type {\n LiveVideoGenerationOptions,\n LiveVideoGenerationResult,\n} from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils/client'\n\nconst DEFAULT_FAL_TOKEN_URL = 'https://rest.fal.ai/tokens/'\nconst DEFAULT_TOKEN_DURATION_SECONDS = 300\n\nexport const FAL_LIVE_VIDEO_MODELS = ['minimax/h3-max/director'] as const\n\nexport type FalLiveVideoModel = (typeof FAL_LIVE_VIDEO_MODELS)[number]\n\n/**\n * WMA / token app id for each public live model. Director's AsyncAPI lives at\n * `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:\n * `minimax/h3-max/director` parses as alias `h3-max`.\n */\nexport const FAL_LIVE_VIDEO_APP = {\n 'minimax/h3-max/director': 'fal-ai/minimax-h3-max-director',\n} as const satisfies Record<FalLiveVideoModel, string>\n\nexport function isFalLiveVideoModel(model: string): model is FalLiveVideoModel {\n return (FAL_LIVE_VIDEO_MODELS as ReadonlyArray<string>).includes(model)\n}\n\nconst WMA_PROXY_PATHS = new Set(['/ice', '/session', '/session/heartbeat'])\n\n/**\n * URLs a Director live proxy may forward. WMA talks to `wma.fal.run`\n * (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to\n * `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is\n * rejected so the proxy cannot run arbitrary fal apps.\n */\nexport function allowedFalLiveVideoProxyTarget(raw: string): URL | null {\n let url: URL\n try {\n url = new URL(raw)\n } catch {\n return null\n }\n if (url.protocol !== 'https:') return null\n if (url.username || url.password || url.port || url.search || url.hash) {\n return null\n }\n const path = url.pathname.replace(/\\/$/, '') || '/'\n if (url.hostname === 'wma.fal.run') {\n return WMA_PROXY_PATHS.has(path) ? url : null\n }\n if (url.hostname === 'fal.run') {\n for (const app of Object.values(FAL_LIVE_VIDEO_APP)) {\n if (path === `/${app}/ice`) return url\n }\n }\n return null\n}\n\n/**\n * Provider options for fal live sessions. The browser applies resolution and\n * aspect ratio on the WMA `configure` message. `tokenDuration` is the only\n * server-side field: it sets how long the minted JWT lasts.\n */\nexport interface FalLiveVideoProviderOptions {\n /** JWT lifetime in seconds. Defaults to 300. */\n tokenDuration?: number\n}\n\nexport type FalLiveVideoModelProviderOptionsByName = {\n [M in FalLiveVideoModel]: FalLiveVideoProviderOptions\n}\n\nfunction resolveFalLiveApiKey(config?: FalClientConfig): string {\n return config?.apiKey ?? getFalApiKeyFromEnv()\n}\n\nfunction readFalToken(body: unknown): string {\n if (typeof body === 'string' && body.length > 0) return body\n if (typeof body === 'object' && body !== null && 'token' in body) {\n const token = body.token\n if (typeof token === 'string' && token.length > 0) return token\n }\n throw new Error('fal token response did not include a token.')\n}\n\n/**\n * fal live-video adapter. Mints a scoped JWT and returns the WMA app id on\n * `result.model`. Open with `fal.realtime.open(wma(live.model))` through a\n * server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key\n * credentials.\n *\n * @example\n * ```ts\n * import { generateLiveVideo } from '@tanstack/ai'\n * import { falLiveVideo } from '@tanstack/ai-fal'\n *\n * const live = await generateLiveVideo({\n * adapter: falLiveVideo('minimax/h3-max/director'),\n * prompt: 'A chef tosses noodles in a steel wok, flames leaping',\n * })\n * // live.model is 'fal-ai/minimax-h3-max-director'\n * ```\n */\nexport class FalLiveVideoAdapter<\n TModel extends FalLiveVideoModel,\n> extends BaseLiveVideoAdapter<TModel, FalLiveVideoProviderOptions> {\n readonly name = 'fal' as const\n private readonly clientConfig: FalClientConfig | undefined\n private readonly apiKey: string\n\n constructor(model: TModel, config?: FalClientConfig) {\n super(model, {})\n this.apiKey = resolveFalLiveApiKey(config)\n this.clientConfig = config\n }\n\n async createLiveVideo(\n options: LiveVideoGenerationOptions<FalLiveVideoProviderOptions>,\n ): Promise<LiveVideoGenerationResult> {\n const { logger, prompt, abortSignal, modelOptions } = options\n const tokenDuration =\n modelOptions?.tokenDuration ?? DEFAULT_TOKEN_DURATION_SECONDS\n const fetchImpl = this.clientConfig?.fetch ?? globalThis.fetch\n\n logger.request(\n `activity=generateLiveVideo provider=fal model=${this.model}`,\n {\n provider: 'fal',\n model: this.model,\n },\n )\n\n if (typeof fetchImpl !== 'function') {\n throw new Error(\n 'global fetch is not available. Pass `fetch` in the fal adapter config.',\n )\n }\n\n try {\n const response = await fetchImpl(DEFAULT_FAL_TOKEN_URL, {\n method: 'POST',\n headers: {\n Authorization: `Key ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n allowed_apps: [FAL_LIVE_VIDEO_APP[this.model]],\n token_expiration: tokenDuration,\n }),\n ...(abortSignal ? { signal: abortSignal } : {}),\n })\n\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `fal token request failed (${response.status}${\n response.statusText ? ` ${response.statusText}` : ''\n })${detail ? `: ${detail}` : ''}`,\n )\n }\n\n const token = readFalToken(await response.json())\n const result: LiveVideoGenerationResult = {\n id: generateId('liveVideo'),\n model: FAL_LIVE_VIDEO_APP[this.model],\n token,\n expiresAt: Date.now() + tokenDuration * 1000,\n prompt,\n status: 'ready',\n }\n\n logger.output(\n `activity=generateLiveVideo provider=fal model=${this.model}`,\n {\n model: this.model,\n status: result.status,\n },\n )\n\n return result\n } catch (error) {\n logger.errors('fal.createLiveVideo failed', {\n error,\n source: 'fal.createLiveVideo',\n })\n throw error\n }\n }\n}\n\n/**\n * Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.\n */\nexport function falLiveVideo<TModel extends FalLiveVideoModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalLiveVideoAdapter<TModel> {\n return new FalLiveVideoAdapter(model, config)\n}\n\n/**\n * Create a fal live-video adapter with an explicit API key.\n */\nexport function createFalLiveVideo<TModel extends FalLiveVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<FalClientConfig, 'apiKey'>,\n): FalLiveVideoAdapter<TModel> {\n return new FalLiveVideoAdapter(model, { ...config, apiKey })\n}\n"],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { FalImageAdapter, falImage } from './adapters/image.js';
|
|
2
2
|
export { FalVideoAdapter, falVideo } from './adapters/video.js';
|
|
3
|
+
export { FalLiveVideoAdapter, falLiveVideo, createFalLiveVideo, FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, isFalLiveVideoModel, allowedFalLiveVideoProxyTarget, } from './adapters/live.js';
|
|
4
|
+
export type { FalLiveVideoModel, FalLiveVideoProviderOptions, FalLiveVideoModelProviderOptionsByName, } from './adapters/live.js';
|
|
3
5
|
export { FalSpeechAdapter, falSpeech } from './adapters/speech.js';
|
|
4
6
|
export { FalTranscriptionAdapter, falTranscription, } from './adapters/transcription.js';
|
|
5
7
|
export { FalAudioAdapter, falAudio } from './adapters/audio.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { configureFalClient, generateId, getFalApiKeyFromEnv } from "./utils/client.js";
|
|
2
2
|
import { FalImageAdapter, falImage } from "./adapters/image.js";
|
|
3
3
|
import { FalVideoAdapter, falVideo } from "./adapters/video.js";
|
|
4
|
+
import { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalLiveVideoAdapter, allowedFalLiveVideoProxyTarget, createFalLiveVideo, falLiveVideo, isFalLiveVideoModel } from "./adapters/live.js";
|
|
4
5
|
import { FalSpeechAdapter, falSpeech } from "./adapters/speech.js";
|
|
5
6
|
import { FalTranscriptionAdapter, falTranscription } from "./adapters/transcription.js";
|
|
6
7
|
import { FalAudioAdapter, falAudio } from "./adapters/audio.js";
|
|
7
|
-
export { FalAudioAdapter, FalImageAdapter, FalSpeechAdapter, FalTranscriptionAdapter, FalVideoAdapter, configureFalClient, falAudio, falImage, falSpeech, falTranscription, falVideo, generateId, getFalApiKeyFromEnv };
|
|
8
|
+
export { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalAudioAdapter, FalImageAdapter, FalLiveVideoAdapter, FalSpeechAdapter, FalTranscriptionAdapter, FalVideoAdapter, allowedFalLiveVideoProxyTarget, configureFalClient, createFalLiveVideo, falAudio, falImage, falLiveVideo, falSpeech, falTranscription, falVideo, generateId, getFalApiKeyFromEnv, isFalLiveVideoModel };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-fal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "fal.ai adapter for TanStack AI image, video, audio, speech, and transcription generation.",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "fal.ai adapter for TanStack AI image, video, live, audio, speech, and transcription generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://tanstack.com/ai",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"adapter",
|
|
45
45
|
"image-generation",
|
|
46
46
|
"video-generation",
|
|
47
|
+
"live-generation",
|
|
47
48
|
"audio-generation",
|
|
48
49
|
"text-to-speech",
|
|
49
50
|
"speech-to-text",
|
|
@@ -58,10 +59,10 @@
|
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@vitest/coverage-v8": "4.1.10",
|
|
60
61
|
"vite": "^8.2.1",
|
|
61
|
-
"@tanstack/ai": "0.
|
|
62
|
+
"@tanstack/ai": "0.54.0"
|
|
62
63
|
},
|
|
63
64
|
"peerDependencies": {
|
|
64
|
-
"@tanstack/ai": "^0.
|
|
65
|
+
"@tanstack/ai": "^0.54.0"
|
|
65
66
|
},
|
|
66
67
|
"scripts": {
|
|
67
68
|
"build": "vite build",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { BaseLiveVideoAdapter } from '@tanstack/ai/adapters'
|
|
2
|
+
import { generateId } from '@tanstack/ai-utils'
|
|
3
|
+
import { getFalApiKeyFromEnv } from '../utils/client'
|
|
4
|
+
import type {
|
|
5
|
+
LiveVideoGenerationOptions,
|
|
6
|
+
LiveVideoGenerationResult,
|
|
7
|
+
} from '@tanstack/ai'
|
|
8
|
+
import type { FalClientConfig } from '../utils/client'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_FAL_TOKEN_URL = 'https://rest.fal.ai/tokens/'
|
|
11
|
+
const DEFAULT_TOKEN_DURATION_SECONDS = 300
|
|
12
|
+
|
|
13
|
+
export const FAL_LIVE_VIDEO_MODELS = ['minimax/h3-max/director'] as const
|
|
14
|
+
|
|
15
|
+
export type FalLiveVideoModel = (typeof FAL_LIVE_VIDEO_MODELS)[number]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WMA / token app id for each public live model. Director's AsyncAPI lives at
|
|
19
|
+
* `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:
|
|
20
|
+
* `minimax/h3-max/director` parses as alias `h3-max`.
|
|
21
|
+
*/
|
|
22
|
+
export const FAL_LIVE_VIDEO_APP = {
|
|
23
|
+
'minimax/h3-max/director': 'fal-ai/minimax-h3-max-director',
|
|
24
|
+
} as const satisfies Record<FalLiveVideoModel, string>
|
|
25
|
+
|
|
26
|
+
export function isFalLiveVideoModel(model: string): model is FalLiveVideoModel {
|
|
27
|
+
return (FAL_LIVE_VIDEO_MODELS as ReadonlyArray<string>).includes(model)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const WMA_PROXY_PATHS = new Set(['/ice', '/session', '/session/heartbeat'])
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* URLs a Director live proxy may forward. WMA talks to `wma.fal.run`
|
|
34
|
+
* (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to
|
|
35
|
+
* `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is
|
|
36
|
+
* rejected so the proxy cannot run arbitrary fal apps.
|
|
37
|
+
*/
|
|
38
|
+
export function allowedFalLiveVideoProxyTarget(raw: string): URL | null {
|
|
39
|
+
let url: URL
|
|
40
|
+
try {
|
|
41
|
+
url = new URL(raw)
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
if (url.protocol !== 'https:') return null
|
|
46
|
+
if (url.username || url.password || url.port || url.search || url.hash) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
const path = url.pathname.replace(/\/$/, '') || '/'
|
|
50
|
+
if (url.hostname === 'wma.fal.run') {
|
|
51
|
+
return WMA_PROXY_PATHS.has(path) ? url : null
|
|
52
|
+
}
|
|
53
|
+
if (url.hostname === 'fal.run') {
|
|
54
|
+
for (const app of Object.values(FAL_LIVE_VIDEO_APP)) {
|
|
55
|
+
if (path === `/${app}/ice`) return url
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Provider options for fal live sessions. The browser applies resolution and
|
|
63
|
+
* aspect ratio on the WMA `configure` message. `tokenDuration` is the only
|
|
64
|
+
* server-side field: it sets how long the minted JWT lasts.
|
|
65
|
+
*/
|
|
66
|
+
export interface FalLiveVideoProviderOptions {
|
|
67
|
+
/** JWT lifetime in seconds. Defaults to 300. */
|
|
68
|
+
tokenDuration?: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type FalLiveVideoModelProviderOptionsByName = {
|
|
72
|
+
[M in FalLiveVideoModel]: FalLiveVideoProviderOptions
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveFalLiveApiKey(config?: FalClientConfig): string {
|
|
76
|
+
return config?.apiKey ?? getFalApiKeyFromEnv()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readFalToken(body: unknown): string {
|
|
80
|
+
if (typeof body === 'string' && body.length > 0) return body
|
|
81
|
+
if (typeof body === 'object' && body !== null && 'token' in body) {
|
|
82
|
+
const token = body.token
|
|
83
|
+
if (typeof token === 'string' && token.length > 0) return token
|
|
84
|
+
}
|
|
85
|
+
throw new Error('fal token response did not include a token.')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* fal live-video adapter. Mints a scoped JWT and returns the WMA app id on
|
|
90
|
+
* `result.model`. Open with `fal.realtime.open(wma(live.model))` through a
|
|
91
|
+
* server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key
|
|
92
|
+
* credentials.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { generateLiveVideo } from '@tanstack/ai'
|
|
97
|
+
* import { falLiveVideo } from '@tanstack/ai-fal'
|
|
98
|
+
*
|
|
99
|
+
* const live = await generateLiveVideo({
|
|
100
|
+
* adapter: falLiveVideo('minimax/h3-max/director'),
|
|
101
|
+
* prompt: 'A chef tosses noodles in a steel wok, flames leaping',
|
|
102
|
+
* })
|
|
103
|
+
* // live.model is 'fal-ai/minimax-h3-max-director'
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export class FalLiveVideoAdapter<
|
|
107
|
+
TModel extends FalLiveVideoModel,
|
|
108
|
+
> extends BaseLiveVideoAdapter<TModel, FalLiveVideoProviderOptions> {
|
|
109
|
+
readonly name = 'fal' as const
|
|
110
|
+
private readonly clientConfig: FalClientConfig | undefined
|
|
111
|
+
private readonly apiKey: string
|
|
112
|
+
|
|
113
|
+
constructor(model: TModel, config?: FalClientConfig) {
|
|
114
|
+
super(model, {})
|
|
115
|
+
this.apiKey = resolveFalLiveApiKey(config)
|
|
116
|
+
this.clientConfig = config
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async createLiveVideo(
|
|
120
|
+
options: LiveVideoGenerationOptions<FalLiveVideoProviderOptions>,
|
|
121
|
+
): Promise<LiveVideoGenerationResult> {
|
|
122
|
+
const { logger, prompt, abortSignal, modelOptions } = options
|
|
123
|
+
const tokenDuration =
|
|
124
|
+
modelOptions?.tokenDuration ?? DEFAULT_TOKEN_DURATION_SECONDS
|
|
125
|
+
const fetchImpl = this.clientConfig?.fetch ?? globalThis.fetch
|
|
126
|
+
|
|
127
|
+
logger.request(
|
|
128
|
+
`activity=generateLiveVideo provider=fal model=${this.model}`,
|
|
129
|
+
{
|
|
130
|
+
provider: 'fal',
|
|
131
|
+
model: this.model,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (typeof fetchImpl !== 'function') {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'global fetch is not available. Pass `fetch` in the fal adapter config.',
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetchImpl(DEFAULT_FAL_TOKEN_URL, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Key ${this.apiKey}`,
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
allowed_apps: [FAL_LIVE_VIDEO_APP[this.model]],
|
|
150
|
+
token_expiration: tokenDuration,
|
|
151
|
+
}),
|
|
152
|
+
...(abortSignal ? { signal: abortSignal } : {}),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const detail = await response.text().catch(() => '')
|
|
157
|
+
throw new Error(
|
|
158
|
+
`fal token request failed (${response.status}${
|
|
159
|
+
response.statusText ? ` ${response.statusText}` : ''
|
|
160
|
+
})${detail ? `: ${detail}` : ''}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const token = readFalToken(await response.json())
|
|
165
|
+
const result: LiveVideoGenerationResult = {
|
|
166
|
+
id: generateId('liveVideo'),
|
|
167
|
+
model: FAL_LIVE_VIDEO_APP[this.model],
|
|
168
|
+
token,
|
|
169
|
+
expiresAt: Date.now() + tokenDuration * 1000,
|
|
170
|
+
prompt,
|
|
171
|
+
status: 'ready',
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logger.output(
|
|
175
|
+
`activity=generateLiveVideo provider=fal model=${this.model}`,
|
|
176
|
+
{
|
|
177
|
+
model: this.model,
|
|
178
|
+
status: result.status,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.errors('fal.createLiveVideo failed', {
|
|
185
|
+
error,
|
|
186
|
+
source: 'fal.createLiveVideo',
|
|
187
|
+
})
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.
|
|
195
|
+
*/
|
|
196
|
+
export function falLiveVideo<TModel extends FalLiveVideoModel>(
|
|
197
|
+
model: TModel,
|
|
198
|
+
config?: FalClientConfig,
|
|
199
|
+
): FalLiveVideoAdapter<TModel> {
|
|
200
|
+
return new FalLiveVideoAdapter(model, config)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create a fal live-video adapter with an explicit API key.
|
|
205
|
+
*/
|
|
206
|
+
export function createFalLiveVideo<TModel extends FalLiveVideoModel>(
|
|
207
|
+
model: TModel,
|
|
208
|
+
apiKey: string,
|
|
209
|
+
config?: Omit<FalClientConfig, 'apiKey'>,
|
|
210
|
+
): FalLiveVideoAdapter<TModel> {
|
|
211
|
+
return new FalLiveVideoAdapter(model, { ...config, apiKey })
|
|
212
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,25 @@ export { FalImageAdapter, falImage } from './adapters/image'
|
|
|
10
10
|
|
|
11
11
|
export { FalVideoAdapter, falVideo } from './adapters/video'
|
|
12
12
|
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Live Adapter (Experimental)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
FalLiveVideoAdapter,
|
|
19
|
+
falLiveVideo,
|
|
20
|
+
createFalLiveVideo,
|
|
21
|
+
FAL_LIVE_VIDEO_APP,
|
|
22
|
+
FAL_LIVE_VIDEO_MODELS,
|
|
23
|
+
isFalLiveVideoModel,
|
|
24
|
+
allowedFalLiveVideoProxyTarget,
|
|
25
|
+
} from './adapters/live'
|
|
26
|
+
export type {
|
|
27
|
+
FalLiveVideoModel,
|
|
28
|
+
FalLiveVideoProviderOptions,
|
|
29
|
+
FalLiveVideoModelProviderOptionsByName,
|
|
30
|
+
} from './adapters/live'
|
|
31
|
+
|
|
13
32
|
// ============================================================================
|
|
14
33
|
// Speech Adapter (TTS)
|
|
15
34
|
// ============================================================================
|