@umituz/react-native-ai-fal-provider 1.0.62 → 1.0.64
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/package.json +1 -1
- package/src/domain/constants/default-models.constants.ts +30 -143
- package/src/domain/constants/models/image-to-video.models.ts +18 -0
- package/src/domain/constants/models/index.ts +10 -0
- package/src/domain/constants/models/text-to-image.models.ts +38 -0
- package/src/domain/constants/models/text-to-text.models.ts +18 -0
- package/src/domain/constants/models/text-to-video.models.ts +48 -0
- package/src/domain/constants/models/text-to-voice.models.ts +28 -0
- package/src/infrastructure/services/fal-provider-subscription.ts +109 -0
- package/src/infrastructure/services/fal-provider.ts +5 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.64",
|
|
4
4
|
"description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
import type { FalModelType } from "../entities/fal.types";
|
|
7
7
|
import type { ModelType } from "../types/model-selection.types";
|
|
8
|
+
import { DEFAULT_TEXT_TO_IMAGE_MODELS } from "./models/text-to-image.models";
|
|
9
|
+
import { DEFAULT_TEXT_TO_VOICE_MODELS } from "./models/text-to-voice.models";
|
|
10
|
+
import { DEFAULT_TEXT_TO_VIDEO_MODELS } from "./models/text-to-video.models";
|
|
11
|
+
import { DEFAULT_IMAGE_TO_VIDEO_MODELS } from "./models/image-to-video.models";
|
|
12
|
+
import { DEFAULT_TEXT_TO_TEXT_MODELS } from "./models/text-to-text.models";
|
|
13
|
+
|
|
14
|
+
// Export model lists
|
|
15
|
+
export { DEFAULT_TEXT_TO_IMAGE_MODELS };
|
|
16
|
+
export { DEFAULT_TEXT_TO_VOICE_MODELS };
|
|
17
|
+
export { DEFAULT_TEXT_TO_VIDEO_MODELS };
|
|
18
|
+
export { DEFAULT_IMAGE_TO_VIDEO_MODELS };
|
|
19
|
+
export { DEFAULT_TEXT_TO_TEXT_MODELS };
|
|
8
20
|
|
|
9
21
|
export interface FalModelConfig {
|
|
10
22
|
readonly id: string;
|
|
@@ -20,130 +32,25 @@ export interface FalModelConfig {
|
|
|
20
32
|
readonly order?: number;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
order: 1,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: "fal-ai/flux/dev",
|
|
36
|
-
name: "Flux Dev",
|
|
37
|
-
type: "text-to-image",
|
|
38
|
-
isDefault: false,
|
|
39
|
-
isActive: true,
|
|
40
|
-
pricing: { freeUserCost: 2, premiumUserCost: 1 },
|
|
41
|
-
description: "High-quality text-to-image generation",
|
|
42
|
-
order: 2,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "fal-ai/flux-pro",
|
|
46
|
-
name: "Flux Pro",
|
|
47
|
-
type: "text-to-image",
|
|
48
|
-
isDefault: false,
|
|
49
|
-
isActive: true,
|
|
50
|
-
pricing: { freeUserCost: 3, premiumUserCost: 1.5 },
|
|
51
|
-
description: "Professional-grade text-to-image generation",
|
|
52
|
-
order: 3,
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
export const DEFAULT_TEXT_TO_VOICE_MODELS: FalModelConfig[] = [
|
|
57
|
-
{
|
|
58
|
-
id: "fal-ai/playai/tts/v3",
|
|
59
|
-
name: "PlayAI TTS v3",
|
|
60
|
-
type: "text-to-voice",
|
|
61
|
-
isDefault: true,
|
|
62
|
-
isActive: true,
|
|
63
|
-
pricing: { freeUserCost: 1, premiumUserCost: 0.5 },
|
|
64
|
-
description: "High-quality text-to-speech synthesis",
|
|
65
|
-
order: 1,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: "fal-ai/eleven-labs/tts",
|
|
69
|
-
name: "ElevenLabs TTS",
|
|
70
|
-
type: "text-to-voice",
|
|
71
|
-
isDefault: false,
|
|
72
|
-
isActive: true,
|
|
73
|
-
pricing: { freeUserCost: 2, premiumUserCost: 1 },
|
|
74
|
-
description: "Premium voice synthesis with multiple voice options",
|
|
75
|
-
order: 2,
|
|
76
|
-
},
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
export const DEFAULT_TEXT_TO_VIDEO_MODELS: FalModelConfig[] = [
|
|
80
|
-
{
|
|
81
|
-
id: "fal-ai/hunyuan-video/1",
|
|
82
|
-
name: "Hunyuan",
|
|
83
|
-
type: "text-to-video",
|
|
84
|
-
isDefault: true,
|
|
85
|
-
isActive: true,
|
|
86
|
-
pricing: { freeUserCost: 10, premiumUserCost: 5 },
|
|
87
|
-
description: "High-quality video generation",
|
|
88
|
-
order: 1,
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: "fal-ai/minimax-video",
|
|
92
|
-
name: "MiniMax",
|
|
93
|
-
type: "text-to-video",
|
|
94
|
-
isDefault: false,
|
|
95
|
-
isActive: true,
|
|
96
|
-
pricing: { freeUserCost: 15, premiumUserCost: 8 },
|
|
97
|
-
description: "Advanced video generation with better dynamics",
|
|
98
|
-
order: 2,
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "fal-ai/kling-video/v1.5/pro/text-to-video",
|
|
102
|
-
name: "Kling 1.5",
|
|
103
|
-
type: "text-to-video",
|
|
104
|
-
isDefault: false,
|
|
105
|
-
isActive: true,
|
|
106
|
-
pricing: { freeUserCost: 20, premiumUserCost: 10 },
|
|
107
|
-
description: "Professional video generation",
|
|
108
|
-
order: 3,
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: "fal-ai/mochi-v1",
|
|
112
|
-
name: "Mochi",
|
|
113
|
-
type: "text-to-video",
|
|
114
|
-
isDefault: false,
|
|
115
|
-
isActive: true,
|
|
116
|
-
pricing: { freeUserCost: 8, premiumUserCost: 4 },
|
|
117
|
-
description: "Fast video generation",
|
|
118
|
-
order: 4,
|
|
119
|
-
},
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
export const DEFAULT_IMAGE_TO_VIDEO_MODELS: FalModelConfig[] = [
|
|
123
|
-
{
|
|
124
|
-
id: "fal-ai/kling-video/v1.5/pro/image-to-video",
|
|
125
|
-
name: "Kling I2V",
|
|
126
|
-
type: "image-to-video",
|
|
127
|
-
isDefault: true,
|
|
128
|
-
isActive: true,
|
|
129
|
-
pricing: { freeUserCost: 15, premiumUserCost: 8 },
|
|
130
|
-
description: "High-quality image to video generation",
|
|
131
|
-
order: 1,
|
|
132
|
-
},
|
|
133
|
-
];
|
|
35
|
+
/**
|
|
36
|
+
* Default credit costs for each model type
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_CREDIT_COSTS: Record<ModelType, number> = {
|
|
39
|
+
"text-to-image": 2,
|
|
40
|
+
"text-to-video": 20,
|
|
41
|
+
"image-to-video": 20,
|
|
42
|
+
"text-to-voice": 3,
|
|
43
|
+
} as const;
|
|
134
44
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
order: 1,
|
|
145
|
-
},
|
|
146
|
-
];
|
|
45
|
+
/**
|
|
46
|
+
* Default model IDs for each model type
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_MODEL_IDS: Record<ModelType, string> = {
|
|
49
|
+
"text-to-image": "fal-ai/flux/schnell",
|
|
50
|
+
"text-to-video": "fal-ai/minimax-video",
|
|
51
|
+
"image-to-video": "fal-ai/kling-video/v1.5/pro/image-to-video",
|
|
52
|
+
"text-to-voice": "fal-ai/playai/tts/v3",
|
|
53
|
+
} as const;
|
|
147
54
|
|
|
148
55
|
/**
|
|
149
56
|
* Get all default models
|
|
@@ -194,23 +101,3 @@ export function getDefaultModel(type: FalModelType): FalModelConfig | undefined
|
|
|
194
101
|
export function findModelById(id: string): FalModelConfig | undefined {
|
|
195
102
|
return getAllDefaultModels().find((m) => m.id === id);
|
|
196
103
|
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Default credit costs for each model type
|
|
200
|
-
*/
|
|
201
|
-
export const DEFAULT_CREDIT_COSTS: Record<ModelType, number> = {
|
|
202
|
-
"text-to-image": 2,
|
|
203
|
-
"text-to-video": 20,
|
|
204
|
-
"image-to-video": 20,
|
|
205
|
-
"text-to-voice": 3,
|
|
206
|
-
} as const;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Default model IDs for each model type
|
|
210
|
-
*/
|
|
211
|
-
export const DEFAULT_MODEL_IDS: Record<ModelType, string> = {
|
|
212
|
-
"text-to-image": "fal-ai/flux/schnell",
|
|
213
|
-
"text-to-video": "fal-ai/minimax-video",
|
|
214
|
-
"image-to-video": "fal-ai/kling-video/v1.5/pro/image-to-video",
|
|
215
|
-
"text-to-voice": "fal-ai/playai/tts/v3",
|
|
216
|
-
} as const;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image-to-Video Models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FalModelConfig } from "../default-models.constants";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_IMAGE_TO_VIDEO_MODELS: FalModelConfig[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "fal-ai/kling-video/v1.5/pro/image-to-video",
|
|
10
|
+
name: "Kling I2V",
|
|
11
|
+
type: "image-to-video",
|
|
12
|
+
isDefault: true,
|
|
13
|
+
isActive: true,
|
|
14
|
+
pricing: { freeUserCost: 15, premiumUserCost: 8 },
|
|
15
|
+
description: "High-quality image to video generation",
|
|
16
|
+
order: 1,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Catalog Index
|
|
3
|
+
* Exports all model lists
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { DEFAULT_TEXT_TO_IMAGE_MODELS } from "./text-to-image.models";
|
|
7
|
+
export { DEFAULT_TEXT_TO_VOICE_MODELS } from "./text-to-voice.models";
|
|
8
|
+
export { DEFAULT_TEXT_TO_VIDEO_MODELS } from "./text-to-video.models";
|
|
9
|
+
export { DEFAULT_IMAGE_TO_VIDEO_MODELS } from "./image-to-video.models";
|
|
10
|
+
export { DEFAULT_TEXT_TO_TEXT_MODELS } from "./text-to-text.models";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Image Models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FalModelConfig } from "../default-models.constants";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_TEXT_TO_IMAGE_MODELS: FalModelConfig[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "fal-ai/flux/schnell",
|
|
10
|
+
name: "Flux Schnell",
|
|
11
|
+
type: "text-to-image",
|
|
12
|
+
isDefault: true,
|
|
13
|
+
isActive: true,
|
|
14
|
+
pricing: { freeUserCost: 1, premiumUserCost: 0.5 },
|
|
15
|
+
description: "Fast and efficient text-to-image generation",
|
|
16
|
+
order: 1,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "fal-ai/flux/dev",
|
|
20
|
+
name: "Flux Dev",
|
|
21
|
+
type: "text-to-image",
|
|
22
|
+
isDefault: false,
|
|
23
|
+
isActive: true,
|
|
24
|
+
pricing: { freeUserCost: 2, premiumUserCost: 1 },
|
|
25
|
+
description: "High-quality text-to-image generation",
|
|
26
|
+
order: 2,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "fal-ai/flux-pro",
|
|
30
|
+
name: "Flux Pro",
|
|
31
|
+
type: "text-to-image",
|
|
32
|
+
isDefault: false,
|
|
33
|
+
isActive: true,
|
|
34
|
+
pricing: { freeUserCost: 3, premiumUserCost: 1.5 },
|
|
35
|
+
description: "Professional-grade text-to-image generation",
|
|
36
|
+
order: 3,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Text Models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FalModelConfig } from "../default-models.constants";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_TEXT_TO_TEXT_MODELS: FalModelConfig[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "fal-ai/llama-3-8b-instruct",
|
|
10
|
+
name: "Llama 3 8B Instruct",
|
|
11
|
+
type: "text-to-text",
|
|
12
|
+
isDefault: true,
|
|
13
|
+
isActive: true,
|
|
14
|
+
pricing: { freeUserCost: 0.1, premiumUserCost: 0.05 },
|
|
15
|
+
description: "Fast and reliable text generation",
|
|
16
|
+
order: 1,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Video Models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FalModelConfig } from "../default-models.constants";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_TEXT_TO_VIDEO_MODELS: FalModelConfig[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "fal-ai/hunyuan-video/1",
|
|
10
|
+
name: "Hunyuan",
|
|
11
|
+
type: "text-to-video",
|
|
12
|
+
isDefault: true,
|
|
13
|
+
isActive: true,
|
|
14
|
+
pricing: { freeUserCost: 10, premiumUserCost: 5 },
|
|
15
|
+
description: "High-quality video generation",
|
|
16
|
+
order: 1,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "fal-ai/minimax-video",
|
|
20
|
+
name: "MiniMax",
|
|
21
|
+
type: "text-to-video",
|
|
22
|
+
isDefault: false,
|
|
23
|
+
isActive: true,
|
|
24
|
+
pricing: { freeUserCost: 15, premiumUserCost: 8 },
|
|
25
|
+
description: "Advanced video generation with better dynamics",
|
|
26
|
+
order: 2,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "fal-ai/kling-video/v1.5/pro/text-to-video",
|
|
30
|
+
name: "Kling 1.5",
|
|
31
|
+
type: "text-to-video",
|
|
32
|
+
isDefault: false,
|
|
33
|
+
isActive: true,
|
|
34
|
+
pricing: { freeUserCost: 20, premiumUserCost: 10 },
|
|
35
|
+
description: "Professional video generation",
|
|
36
|
+
order: 3,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "fal-ai/mochi-v1",
|
|
40
|
+
name: "Mochi",
|
|
41
|
+
type: "text-to-video",
|
|
42
|
+
isDefault: false,
|
|
43
|
+
isActive: true,
|
|
44
|
+
pricing: { freeUserCost: 8, premiumUserCost: 4 },
|
|
45
|
+
description: "Fast video generation",
|
|
46
|
+
order: 4,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Voice Models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FalModelConfig } from "../default-models.constants";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_TEXT_TO_VOICE_MODELS: FalModelConfig[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "fal-ai/playai/tts/v3",
|
|
10
|
+
name: "PlayAI TTS v3",
|
|
11
|
+
type: "text-to-voice",
|
|
12
|
+
isDefault: true,
|
|
13
|
+
isActive: true,
|
|
14
|
+
pricing: { freeUserCost: 1, premiumUserCost: 0.5 },
|
|
15
|
+
description: "High-quality text-to-speech synthesis",
|
|
16
|
+
order: 1,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "fal-ai/eleven-labs/tts",
|
|
20
|
+
name: "ElevenLabs TTS",
|
|
21
|
+
type: "text-to-voice",
|
|
22
|
+
isDefault: false,
|
|
23
|
+
isActive: true,
|
|
24
|
+
pricing: { freeUserCost: 2, premiumUserCost: 1 },
|
|
25
|
+
description: "Premium voice synthesis with multiple voice options",
|
|
26
|
+
order: 2,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Provider Subscription Handlers
|
|
3
|
+
* Handles subscribe, run methods and cancellation logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fal } from "@fal-ai/client";
|
|
7
|
+
import type { SubscribeOptions, RunOptions } from "@umituz/react-native-ai-generation-content";
|
|
8
|
+
import type { FalQueueStatus } from "../../domain/entities/fal.types";
|
|
9
|
+
import { DEFAULT_FAL_CONFIG } from "./fal-provider.constants";
|
|
10
|
+
import { mapFalStatusToJobStatus } from "./fal-status-mapper";
|
|
11
|
+
import { validateNSFWContent } from "../validators/nsfw-validator";
|
|
12
|
+
|
|
13
|
+
declare const __DEV__: boolean | undefined;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handle FAL subscription with timeout and cancellation
|
|
17
|
+
*/
|
|
18
|
+
export async function handleFalSubscription<T = unknown>(
|
|
19
|
+
model: string,
|
|
20
|
+
input: Record<string, unknown>,
|
|
21
|
+
options?: SubscribeOptions<T>,
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
): Promise<{ result: T; requestId: string | null }> {
|
|
24
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
|
|
25
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
let currentRequestId: string | null = null;
|
|
27
|
+
|
|
28
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
29
|
+
console.log("[FalProvider] Subscribe started:", { model, timeoutMs });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let lastStatus = "";
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await Promise.race([
|
|
36
|
+
fal.subscribe(model, {
|
|
37
|
+
input,
|
|
38
|
+
logs: false,
|
|
39
|
+
pollInterval: DEFAULT_FAL_CONFIG.pollInterval,
|
|
40
|
+
onQueueUpdate: (update: { status: string; logs?: unknown[]; request_id?: string }) => {
|
|
41
|
+
currentRequestId = update.request_id ?? null;
|
|
42
|
+
const jobStatus = mapFalStatusToJobStatus({
|
|
43
|
+
...update as unknown as FalQueueStatus,
|
|
44
|
+
requestId: currentRequestId ?? "",
|
|
45
|
+
});
|
|
46
|
+
if (jobStatus.status !== lastStatus) {
|
|
47
|
+
lastStatus = jobStatus.status;
|
|
48
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
49
|
+
console.log("[FalProvider] Status:", jobStatus.status, "RequestId:", currentRequestId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
options?.onQueueUpdate?.(jobStatus);
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
new Promise<never>((_, reject) => {
|
|
56
|
+
timeoutId = setTimeout(() => {
|
|
57
|
+
reject(new Error("FAL subscription timeout"));
|
|
58
|
+
}, timeoutMs);
|
|
59
|
+
}),
|
|
60
|
+
// Abort promise
|
|
61
|
+
...(signal ? [
|
|
62
|
+
new Promise<never>((_, reject) => {
|
|
63
|
+
signal.addEventListener("abort", () => {
|
|
64
|
+
reject(new Error("Request cancelled by user"));
|
|
65
|
+
});
|
|
66
|
+
}),
|
|
67
|
+
] as const : []),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
71
|
+
console.log("[FalProvider] Subscribe completed:", { model, requestId: currentRequestId });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
validateNSFWContent(result as Record<string, unknown>);
|
|
75
|
+
|
|
76
|
+
options?.onResult?.(result as T);
|
|
77
|
+
return { result: result as T, requestId: currentRequestId };
|
|
78
|
+
} finally {
|
|
79
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle FAL run with NSFW validation
|
|
85
|
+
*/
|
|
86
|
+
export async function handleFalRun<T = unknown>(
|
|
87
|
+
model: string,
|
|
88
|
+
input: Record<string, unknown>,
|
|
89
|
+
options?: RunOptions
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
options?.onProgress?.({ progress: 10, status: "IN_PROGRESS" as const });
|
|
92
|
+
|
|
93
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
+
console.log("[FalProvider] run() model:", model, "inputKeys:", Object.keys(input));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = await fal.run(model, { input });
|
|
98
|
+
|
|
99
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
|
+
console.log("[FalProvider] run() raw result:", JSON.stringify(result, null, 2));
|
|
101
|
+
console.log("[FalProvider] run() result type:", typeof result);
|
|
102
|
+
console.log("[FalProvider] run() result keys:", result ? Object.keys(result as object) : "null");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
validateNSFWContent(result as Record<string, unknown>);
|
|
106
|
+
|
|
107
|
+
options?.onProgress?.({ progress: 100, status: "COMPLETED" as const });
|
|
108
|
+
return result as T;
|
|
109
|
+
}
|
|
@@ -22,7 +22,7 @@ import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
|
|
|
22
22
|
import { mapFalStatusToJobStatus } from "./fal-status-mapper";
|
|
23
23
|
import { FAL_IMAGE_FEATURE_MODELS, FAL_VIDEO_FEATURE_MODELS } from "../../domain/constants/feature-models.constants";
|
|
24
24
|
import { buildImageFeatureInput as buildImageFeatureInputImpl, buildVideoFeatureInput as buildVideoFeatureInputImpl } from "../builders";
|
|
25
|
-
import {
|
|
25
|
+
import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
|
|
26
26
|
|
|
27
27
|
declare const __DEV__: boolean | undefined;
|
|
28
28
|
|
|
@@ -31,13 +31,11 @@ export class FalProvider implements IAIProvider {
|
|
|
31
31
|
readonly providerName = "FAL AI";
|
|
32
32
|
|
|
33
33
|
private apiKey: string | null = null;
|
|
34
|
-
private config: AIProviderConfig | null = null;
|
|
35
34
|
private initialized = false;
|
|
36
35
|
private currentAbortController: AbortController | null = null;
|
|
37
36
|
|
|
38
37
|
initialize(configData: AIProviderConfig): void {
|
|
39
38
|
this.apiKey = configData.apiKey;
|
|
40
|
-
this.config = { ...DEFAULT_FAL_CONFIG, ...configData };
|
|
41
39
|
|
|
42
40
|
fal.config({
|
|
43
41
|
credentials: configData.apiKey,
|
|
@@ -105,104 +103,24 @@ export class FalProvider implements IAIProvider {
|
|
|
105
103
|
options?: SubscribeOptions<T>,
|
|
106
104
|
): Promise<T> {
|
|
107
105
|
this.validateInitialization();
|
|
108
|
-
|
|
109
|
-
// Cancel previous request if exists
|
|
110
106
|
this.cancelCurrentRequest();
|
|
111
|
-
|
|
112
|
-
// Create new abort controller
|
|
113
107
|
this.currentAbortController = new AbortController();
|
|
114
|
-
const { signal } = this.currentAbortController;
|
|
115
|
-
|
|
116
|
-
const timeoutMs = options?.timeoutMs ?? this.config?.defaultTimeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
|
|
117
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
118
|
-
let currentRequestId: string | null = null;
|
|
119
|
-
|
|
120
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
121
|
-
console.log("[FalProvider] Subscribe started:", { model, timeoutMs });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let lastStatus = "";
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const result = await Promise.race([
|
|
128
|
-
fal.subscribe(model, {
|
|
129
|
-
input,
|
|
130
|
-
logs: false,
|
|
131
|
-
pollInterval: DEFAULT_FAL_CONFIG.pollInterval,
|
|
132
|
-
onQueueUpdate: (update: { status: string; logs?: unknown[]; request_id?: string }) => {
|
|
133
|
-
currentRequestId = update.request_id ?? null;
|
|
134
|
-
const jobStatus = mapFalStatusToJobStatus({
|
|
135
|
-
...update as unknown as FalQueueStatus,
|
|
136
|
-
requestId: currentRequestId ?? "",
|
|
137
|
-
});
|
|
138
|
-
if (jobStatus.status !== lastStatus) {
|
|
139
|
-
lastStatus = jobStatus.status;
|
|
140
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
141
|
-
console.log("[FalProvider] Status:", jobStatus.status, "RequestId:", currentRequestId);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
options?.onQueueUpdate?.(jobStatus);
|
|
145
|
-
},
|
|
146
|
-
}),
|
|
147
|
-
new Promise<never>((_, reject) => {
|
|
148
|
-
timeoutId = setTimeout(() => {
|
|
149
|
-
reject(new Error("FAL subscription timeout"));
|
|
150
|
-
}, timeoutMs);
|
|
151
|
-
}),
|
|
152
|
-
// Abort promise
|
|
153
|
-
new Promise<never>((_, reject) => {
|
|
154
|
-
signal.addEventListener("abort", () => {
|
|
155
|
-
reject(new Error("Request cancelled by user"));
|
|
156
|
-
});
|
|
157
|
-
}),
|
|
158
|
-
]);
|
|
159
|
-
|
|
160
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
161
|
-
console.log("[FalProvider] Subscribe completed:", { model, requestId: currentRequestId });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
validateNSFWContent(result as Record<string, unknown>);
|
|
165
108
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} finally {
|
|
169
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
170
|
-
this.currentAbortController = null;
|
|
171
|
-
}
|
|
109
|
+
const { result } = await handleFalSubscription<T>(model, input, options, this.currentAbortController.signal);
|
|
110
|
+
return result;
|
|
172
111
|
}
|
|
173
112
|
|
|
174
113
|
async run<T = unknown>(model: string, input: Record<string, unknown>, options?: RunOptions): Promise<T> {
|
|
175
114
|
this.validateInitialization();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
179
|
-
console.log("[FalProvider] run() model:", model, "inputKeys:", Object.keys(input));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const result = await fal.run(model, { input });
|
|
183
|
-
|
|
184
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
185
|
-
console.log("[FalProvider] run() raw result:", JSON.stringify(result, null, 2));
|
|
186
|
-
console.log("[FalProvider] run() result type:", typeof result);
|
|
187
|
-
console.log("[FalProvider] run() result keys:", result ? Object.keys(result as object) : "null");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
validateNSFWContent(result as Record<string, unknown>);
|
|
191
|
-
|
|
192
|
-
options?.onProgress?.({ progress: 100, status: "COMPLETED" as const });
|
|
193
|
-
return result as T;
|
|
115
|
+
return handleFalRun<T>(model, input, options);
|
|
194
116
|
}
|
|
195
117
|
|
|
196
118
|
reset(): void {
|
|
197
119
|
this.cancelCurrentRequest();
|
|
198
120
|
this.apiKey = null;
|
|
199
|
-
this.config = null;
|
|
200
121
|
this.initialized = false;
|
|
201
122
|
}
|
|
202
123
|
|
|
203
|
-
/**
|
|
204
|
-
* Cancel the current running request
|
|
205
|
-
*/
|
|
206
124
|
cancelCurrentRequest(): void {
|
|
207
125
|
if (this.currentAbortController) {
|
|
208
126
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -213,9 +131,6 @@ export class FalProvider implements IAIProvider {
|
|
|
213
131
|
}
|
|
214
132
|
}
|
|
215
133
|
|
|
216
|
-
/**
|
|
217
|
-
* Check if there's a running request
|
|
218
|
-
*/
|
|
219
134
|
hasRunningRequest(): boolean {
|
|
220
135
|
return this.currentAbortController !== null;
|
|
221
136
|
}
|
|
@@ -237,4 +152,4 @@ export class FalProvider implements IAIProvider {
|
|
|
237
152
|
}
|
|
238
153
|
}
|
|
239
154
|
|
|
240
|
-
export const falProvider = new FalProvider();
|
|
155
|
+
export const falProvider = new FalProvider();
|