@umituz/react-native-ai-fal-provider 2.0.17 → 2.0.19
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/types/input-builders.types.ts +1 -1
- package/src/domain/types/provider.types.ts +1 -2
- package/src/exports/infrastructure.ts +0 -10
- package/src/infrastructure/services/fal-provider-subscription.ts +11 -60
- package/src/infrastructure/services/fal-provider.constants.ts +2 -11
- package/src/infrastructure/services/fal-provider.ts +25 -35
- package/src/infrastructure/services/fal-queue-operations.ts +10 -7
- package/src/infrastructure/services/fal-status-mapper.ts +7 -6
- package/src/infrastructure/services/request-store.ts +9 -15
- package/src/infrastructure/utils/cost-tracker.ts +2 -9
- package/src/infrastructure/utils/cost-tracking-executor.util.ts +3 -6
- package/src/infrastructure/utils/error-mapper.ts +0 -7
- package/src/infrastructure/utils/fal-storage.util.ts +14 -26
- package/src/infrastructure/utils/index.ts +0 -10
- package/src/infrastructure/utils/input-builders.util.ts +0 -2
- package/src/infrastructure/utils/input-preprocessor.util.ts +4 -34
- package/src/infrastructure/utils/input-validator.util.ts +16 -10
- package/src/infrastructure/utils/type-guards.util.ts +4 -3
- package/src/infrastructure/validators/nsfw-validator.ts +0 -17
- package/src/init/createAiProviderInitModule.ts +2 -36
- package/src/presentation/hooks/use-fal-generation.ts +0 -5
- package/src/presentation/hooks/use-models.ts +1 -11
- package/src/infrastructure/builders/image-feature-builder.ts +0 -64
- package/src/infrastructure/builders/video-feature-builder.ts +0 -52
- package/src/infrastructure/services/fal-feature-models.ts +0 -48
- package/src/infrastructure/utils/image-feature-builders.util.ts +0 -103
- package/src/infrastructure/utils/video-feature-builders.util.ts +0 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
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",
|
|
@@ -39,8 +39,6 @@ export interface AIProviderConfig {
|
|
|
39
39
|
textToImageModel?: string;
|
|
40
40
|
imageEditModel?: string;
|
|
41
41
|
videoGenerationModel?: string;
|
|
42
|
-
videoFeatureModels?: Partial<Record<VideoFeatureType, string>>;
|
|
43
|
-
imageFeatureModels?: Partial<Record<ImageFeatureType, string>>;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
// =============================================================================
|
|
@@ -93,6 +91,7 @@ export interface SubscribeOptions<T = unknown> {
|
|
|
93
91
|
|
|
94
92
|
export interface RunOptions {
|
|
95
93
|
onProgress?: (progress: ProviderProgressInfo) => void;
|
|
94
|
+
signal?: AbortSignal;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
// =============================================================================
|
|
@@ -14,20 +14,10 @@ export type { FalProviderType } from "../infrastructure/services";
|
|
|
14
14
|
|
|
15
15
|
export {
|
|
16
16
|
categorizeFalError,
|
|
17
|
-
falErrorMapper,
|
|
18
17
|
mapFalError,
|
|
19
18
|
isFalErrorRetryable,
|
|
20
19
|
buildSingleImageInput,
|
|
21
20
|
buildDualImageInput,
|
|
22
|
-
buildUpscaleInput,
|
|
23
|
-
buildPhotoRestoreInput,
|
|
24
|
-
buildVideoFromImageInput,
|
|
25
|
-
buildFaceSwapInput,
|
|
26
|
-
buildImageToImageInput,
|
|
27
|
-
buildRemoveBackgroundInput,
|
|
28
|
-
buildRemoveObjectInput,
|
|
29
|
-
buildReplaceBackgroundInput,
|
|
30
|
-
buildHDTouchUpInput,
|
|
31
21
|
} from "../infrastructure/utils";
|
|
32
22
|
|
|
33
23
|
export { CostTracker } from "../infrastructure/utils/cost-tracker";
|
|
@@ -11,8 +11,6 @@ import { mapFalStatusToJobStatus } from "./fal-status-mapper";
|
|
|
11
11
|
import { validateNSFWContent } from "../validators/nsfw-validator";
|
|
12
12
|
import { NSFWContentError } from "./nsfw-content-error";
|
|
13
13
|
|
|
14
|
-
declare const __DEV__: boolean | undefined;
|
|
15
|
-
|
|
16
14
|
interface FalApiErrorDetail {
|
|
17
15
|
msg?: string;
|
|
18
16
|
type?: string;
|
|
@@ -59,22 +57,16 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
59
57
|
signal?: AbortSignal
|
|
60
58
|
): Promise<{ result: T; requestId: string | null }> {
|
|
61
59
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
|
|
60
|
+
|
|
61
|
+
if (timeoutMs <= 0 || timeoutMs > 3600000) {
|
|
62
|
+
throw new Error(`Invalid timeout: ${timeoutMs}ms. Must be between 1 and 3600000ms (1 hour)`);
|
|
63
|
+
}
|
|
64
|
+
|
|
62
65
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
63
66
|
let currentRequestId: string | null = null;
|
|
64
67
|
let abortHandler: (() => void) | null = null;
|
|
68
|
+
let listenerAdded = false;
|
|
65
69
|
|
|
66
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
-
console.log("[FalProvider] Subscribe started:", {
|
|
68
|
-
model,
|
|
69
|
-
timeoutMs,
|
|
70
|
-
inputKeys: Object.keys(input),
|
|
71
|
-
hasImageUrl: !!input.image_url,
|
|
72
|
-
hasPrompt: !!input.prompt,
|
|
73
|
-
promptPreview: input.prompt ? String(input.prompt).substring(0, 50) + "..." : "N/A",
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check if already aborted before starting
|
|
78
70
|
if (signal?.aborted) {
|
|
79
71
|
throw new Error("Request cancelled by user");
|
|
80
72
|
}
|
|
@@ -82,7 +74,6 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
82
74
|
let lastStatus = "";
|
|
83
75
|
|
|
84
76
|
try {
|
|
85
|
-
// Create promises array conditionally to avoid unnecessary abort promise creation
|
|
86
77
|
const promises: Promise<unknown>[] = [
|
|
87
78
|
fal.subscribe(model, {
|
|
88
79
|
input,
|
|
@@ -91,14 +82,13 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
91
82
|
onQueueUpdate: (update: { status: string; logs?: unknown[]; request_id?: string }) => {
|
|
92
83
|
currentRequestId = update.request_id ?? currentRequestId;
|
|
93
84
|
const jobStatus = mapFalStatusToJobStatus({
|
|
94
|
-
|
|
85
|
+
status: update.status as FalQueueStatus["status"],
|
|
95
86
|
requestId: currentRequestId ?? "",
|
|
87
|
+
logs: update.logs as FalQueueStatus["logs"],
|
|
88
|
+
queuePosition: undefined,
|
|
96
89
|
});
|
|
97
90
|
if (jobStatus.status !== lastStatus) {
|
|
98
91
|
lastStatus = jobStatus.status;
|
|
99
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
|
-
console.log("[FalProvider] Status:", jobStatus.status, "RequestId:", currentRequestId);
|
|
101
|
-
}
|
|
102
92
|
}
|
|
103
93
|
options?.onQueueUpdate?.(jobStatus);
|
|
104
94
|
},
|
|
@@ -110,55 +100,33 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
110
100
|
}),
|
|
111
101
|
];
|
|
112
102
|
|
|
113
|
-
// Add abort promise only if signal is provided and not already aborted
|
|
114
103
|
if (signal && !signal.aborted) {
|
|
115
104
|
const abortPromise = new Promise<never>((_, reject) => {
|
|
116
105
|
abortHandler = () => {
|
|
117
106
|
reject(new Error("Request cancelled by user"));
|
|
118
107
|
};
|
|
119
108
|
signal.addEventListener("abort", abortHandler);
|
|
109
|
+
listenerAdded = true;
|
|
120
110
|
});
|
|
121
111
|
promises.push(abortPromise);
|
|
122
112
|
}
|
|
123
113
|
|
|
124
114
|
const result = await Promise.race(promises);
|
|
125
115
|
|
|
126
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
127
|
-
console.log("[FalProvider] Subscribe completed:", {
|
|
128
|
-
model,
|
|
129
|
-
requestId: currentRequestId,
|
|
130
|
-
resultKeys: result ? Object.keys(result as object) : "null",
|
|
131
|
-
hasVideo: !!(result as Record<string, unknown>)?.video,
|
|
132
|
-
hasOutput: !!(result as Record<string, unknown>)?.output,
|
|
133
|
-
hasData: !!(result as Record<string, unknown>)?.data,
|
|
134
|
-
});
|
|
135
|
-
// Log full result structure for debugging
|
|
136
|
-
console.log("[FalProvider] Result structure:", JSON.stringify(result, null, 2).substring(0, 1000));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
116
|
validateNSFWContent(result as Record<string, unknown>);
|
|
140
117
|
|
|
141
118
|
options?.onResult?.(result as T);
|
|
142
119
|
return { result: result as T, requestId: currentRequestId };
|
|
143
120
|
} catch (error) {
|
|
144
|
-
// Preserve NSFWContentError type
|
|
145
121
|
if (error instanceof NSFWContentError) {
|
|
146
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
147
|
-
console.error("[FalProvider] NSFW content detected");
|
|
148
|
-
}
|
|
149
122
|
throw error;
|
|
150
123
|
}
|
|
151
124
|
|
|
152
|
-
// Parse FAL error and throw with user-friendly message
|
|
153
125
|
const userMessage = parseFalError(error);
|
|
154
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
155
|
-
console.error("[FalProvider] Error:", userMessage);
|
|
156
|
-
}
|
|
157
126
|
throw new Error(userMessage);
|
|
158
127
|
} finally {
|
|
159
128
|
if (timeoutId) clearTimeout(timeoutId);
|
|
160
|
-
|
|
161
|
-
if (abortHandler && signal && !signal.aborted) {
|
|
129
|
+
if (listenerAdded && abortHandler && signal) {
|
|
162
130
|
signal.removeEventListener("abort", abortHandler);
|
|
163
131
|
}
|
|
164
132
|
}
|
|
@@ -174,36 +142,19 @@ export async function handleFalRun<T = unknown>(
|
|
|
174
142
|
): Promise<T> {
|
|
175
143
|
options?.onProgress?.({ progress: 10, status: "IN_PROGRESS" as const });
|
|
176
144
|
|
|
177
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
178
|
-
console.log("[FalProvider] run() model:", model, "inputKeys:", Object.keys(input));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
145
|
try {
|
|
182
146
|
const result = await fal.run(model, { input });
|
|
183
147
|
|
|
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
148
|
validateNSFWContent(result as Record<string, unknown>);
|
|
191
149
|
|
|
192
150
|
options?.onProgress?.({ progress: 100, status: "COMPLETED" as const });
|
|
193
151
|
return result as T;
|
|
194
152
|
} catch (error) {
|
|
195
|
-
// Preserve NSFWContentError type
|
|
196
153
|
if (error instanceof NSFWContentError) {
|
|
197
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
198
|
-
console.error("[FalProvider] run() NSFW content detected");
|
|
199
|
-
}
|
|
200
154
|
throw error;
|
|
201
155
|
}
|
|
202
156
|
|
|
203
157
|
const userMessage = parseFalError(error);
|
|
204
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
205
|
-
console.error("[FalProvider] run() Error:", userMessage);
|
|
206
|
-
}
|
|
207
158
|
throw new Error(userMessage);
|
|
208
159
|
}
|
|
209
160
|
}
|
|
@@ -14,17 +14,8 @@ export const DEFAULT_FAL_CONFIG = {
|
|
|
14
14
|
} as const;
|
|
15
15
|
|
|
16
16
|
export const FAL_CAPABILITIES: ProviderCapabilities = {
|
|
17
|
-
imageFeatures: [
|
|
18
|
-
|
|
19
|
-
"photo-restore",
|
|
20
|
-
"face-swap",
|
|
21
|
-
"anime-selfie",
|
|
22
|
-
"remove-background",
|
|
23
|
-
"remove-object",
|
|
24
|
-
"hd-touch-up",
|
|
25
|
-
"replace-background",
|
|
26
|
-
] as const,
|
|
27
|
-
videoFeatures: ["image-to-video", "text-to-video"] as const,
|
|
17
|
+
imageFeatures: [] as const,
|
|
18
|
+
videoFeatures: [] as const,
|
|
28
19
|
textToImage: true,
|
|
29
20
|
textToVideo: true,
|
|
30
21
|
imageToVideo: true,
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { fal } from "@fal-ai/client";
|
|
7
7
|
import type {
|
|
8
8
|
IAIProvider, AIProviderConfig, JobSubmission, JobStatus, SubscribeOptions,
|
|
9
|
-
RunOptions, ImageFeatureType, VideoFeatureType,
|
|
10
|
-
|
|
9
|
+
RunOptions, ProviderCapabilities, ImageFeatureType, VideoFeatureType,
|
|
10
|
+
ImageFeatureInputData, VideoFeatureInputData,
|
|
11
11
|
} from "../../domain/types";
|
|
12
12
|
import type { CostTrackerConfig } from "../../domain/entities/cost-tracking.types";
|
|
13
13
|
import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
|
|
@@ -18,11 +18,8 @@ import {
|
|
|
18
18
|
removeRequest, cancelAllRequests, hasActiveRequests,
|
|
19
19
|
} from "./request-store";
|
|
20
20
|
import * as queueOps from "./fal-queue-operations";
|
|
21
|
-
import * as featureModels from "./fal-feature-models";
|
|
22
21
|
import { validateInput } from "../utils/input-validator.util";
|
|
23
22
|
|
|
24
|
-
declare const __DEV__: boolean | undefined;
|
|
25
|
-
|
|
26
23
|
export class FalProvider implements IAIProvider {
|
|
27
24
|
readonly providerId = "fal";
|
|
28
25
|
readonly providerName = "FAL AI";
|
|
@@ -30,13 +27,9 @@ export class FalProvider implements IAIProvider {
|
|
|
30
27
|
private apiKey: string | null = null;
|
|
31
28
|
private initialized = false;
|
|
32
29
|
private costTracker: CostTracker | null = null;
|
|
33
|
-
private videoFeatureModels: Record<string, string> = {};
|
|
34
|
-
private imageFeatureModels: Record<string, string> = {};
|
|
35
30
|
|
|
36
31
|
initialize(config: AIProviderConfig): void {
|
|
37
32
|
this.apiKey = config.apiKey;
|
|
38
|
-
this.videoFeatureModels = config.videoFeatureModels ?? {};
|
|
39
|
-
this.imageFeatureModels = config.imageFeatureModels ?? {};
|
|
40
33
|
fal.config({
|
|
41
34
|
credentials: config.apiKey,
|
|
42
35
|
retry: {
|
|
@@ -46,9 +39,6 @@ export class FalProvider implements IAIProvider {
|
|
|
46
39
|
},
|
|
47
40
|
});
|
|
48
41
|
this.initialized = true;
|
|
49
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
50
|
-
console.log("[FalProvider] Initialized");
|
|
51
|
-
}
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
enableCostTracking(config?: CostTrackerConfig): void {
|
|
@@ -75,10 +65,24 @@ export class FalProvider implements IAIProvider {
|
|
|
75
65
|
return FAL_CAPABILITIES;
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
isFeatureSupported(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
isFeatureSupported(_feature: ImageFeatureType | VideoFeatureType): boolean {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getImageFeatureModel(_feature: ImageFeatureType): string {
|
|
73
|
+
throw new Error("Feature-specific models are not supported in this provider. Use the main app's feature implementations.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
buildImageFeatureInput(_feature: ImageFeatureType, _data: ImageFeatureInputData): Record<string, unknown> {
|
|
77
|
+
throw new Error("Feature-specific input building is not supported in this provider. Use the main app's feature implementations.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getVideoFeatureModel(_feature: VideoFeatureType): string {
|
|
81
|
+
throw new Error("Feature-specific models are not supported in this provider. Use the main app's feature implementations.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
buildVideoFeatureInput(_feature: VideoFeatureType, _data: VideoFeatureInputData): Record<string, unknown> {
|
|
85
|
+
throw new Error("Feature-specific input building is not supported in this provider. Use the main app's feature implementations.");
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
private validateInit(): void {
|
|
@@ -116,9 +120,6 @@ export class FalProvider implements IAIProvider {
|
|
|
116
120
|
|
|
117
121
|
const existing = getExistingRequest<T>(key);
|
|
118
122
|
if (existing) {
|
|
119
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
-
console.log(`[FalProvider] Dedup: returning existing promise for ${model}`);
|
|
121
|
-
}
|
|
122
123
|
return existing.promise;
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -142,6 +143,11 @@ export class FalProvider implements IAIProvider {
|
|
|
142
143
|
validateInput(model, input);
|
|
143
144
|
const processedInput = await preprocessInput(input);
|
|
144
145
|
|
|
146
|
+
const signal = options?.signal;
|
|
147
|
+
if (signal?.aborted) {
|
|
148
|
+
throw new Error("Request cancelled by user");
|
|
149
|
+
}
|
|
150
|
+
|
|
145
151
|
return executeWithCostTracking({
|
|
146
152
|
tracker: this.costTracker,
|
|
147
153
|
model,
|
|
@@ -163,22 +169,6 @@ export class FalProvider implements IAIProvider {
|
|
|
163
169
|
hasRunningRequest(): boolean {
|
|
164
170
|
return hasActiveRequests();
|
|
165
171
|
}
|
|
166
|
-
|
|
167
|
-
getImageFeatureModel(feature: ImageFeatureType): string {
|
|
168
|
-
return featureModels.getImageFeatureModel(this.imageFeatureModels, feature);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
buildImageFeatureInput(feature: ImageFeatureType, data: ImageFeatureInputData): Record<string, unknown> {
|
|
172
|
-
return featureModels.buildImageFeatureInput(feature, data);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
getVideoFeatureModel(feature: VideoFeatureType): string {
|
|
176
|
-
return featureModels.getVideoFeatureModel(this.videoFeatureModels, feature);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
buildVideoFeatureInput(feature: VideoFeatureType, data: VideoFeatureInputData): Record<string, unknown> {
|
|
180
|
-
return featureModels.buildVideoFeatureInput(feature, data);
|
|
181
|
-
}
|
|
182
172
|
}
|
|
183
173
|
|
|
184
174
|
export const falProvider = new FalProvider();
|
|
@@ -37,14 +37,10 @@ export async function submitJob(model: string, input: Record<string, unknown>):
|
|
|
37
37
|
export async function getJobStatus(model: string, requestId: string): Promise<JobStatus> {
|
|
38
38
|
const status = await fal.queue.status(model, { requestId, logs: true });
|
|
39
39
|
|
|
40
|
-
// Validate the response structure before mapping
|
|
41
40
|
if (!isValidFalQueueStatus(status)) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
logs: [],
|
|
46
|
-
queuePosition: undefined,
|
|
47
|
-
};
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid FAL queue status response for model ${model}, requestId ${requestId}`
|
|
43
|
+
);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
return mapFalStatusToJobStatus(status);
|
|
@@ -52,5 +48,12 @@ export async function getJobStatus(model: string, requestId: string): Promise<Jo
|
|
|
52
48
|
|
|
53
49
|
export async function getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
|
|
54
50
|
const result = await fal.queue.result(model, { requestId });
|
|
51
|
+
|
|
52
|
+
if (!result || typeof result !== 'object') {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Invalid FAL queue result for model ${model}, requestId ${requestId}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
55
58
|
return result.data as T;
|
|
56
59
|
}
|
|
@@ -24,13 +24,14 @@ export function mapFalStatusToJobStatus(status: FalQueueStatus): JobStatus {
|
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
status: mappedStatus,
|
|
27
|
-
logs: status.logs
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
logs: Array.isArray(status.logs)
|
|
28
|
+
? status.logs.map((log: FalLogEntry) => ({
|
|
29
|
+
message: log.message,
|
|
30
|
+
level: log.level ?? "info",
|
|
31
|
+
timestamp: log.timestamp ?? new Date().toISOString(),
|
|
32
|
+
}))
|
|
33
|
+
: [],
|
|
32
34
|
queuePosition: status.queuePosition ?? undefined,
|
|
33
|
-
// Preserve requestId from FalQueueStatus for use in hooks
|
|
34
35
|
requestId: status.requestId,
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* Survives hot reloads for React Native development
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
declare const __DEV__: boolean | undefined;
|
|
7
|
-
|
|
8
6
|
export interface ActiveRequest<T = unknown> {
|
|
9
7
|
promise: Promise<T>;
|
|
10
8
|
abortController: AbortController;
|
|
@@ -13,9 +11,6 @@ export interface ActiveRequest<T = unknown> {
|
|
|
13
11
|
const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
|
|
14
12
|
type RequestStore = Map<string, ActiveRequest>;
|
|
15
13
|
|
|
16
|
-
// Counter for generating unique request IDs
|
|
17
|
-
let requestCounter = 0;
|
|
18
|
-
|
|
19
14
|
export function getRequestStore(): RequestStore {
|
|
20
15
|
if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
|
|
21
16
|
(globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
|
|
@@ -27,18 +22,20 @@ export function getRequestStore(): RequestStore {
|
|
|
27
22
|
* Create a collision-resistant request key using combination of:
|
|
28
23
|
* - Model name
|
|
29
24
|
* - Input hash (for quick comparison)
|
|
30
|
-
* - Unique
|
|
25
|
+
* - Unique ID (guarantees uniqueness)
|
|
31
26
|
*/
|
|
32
27
|
export function createRequestKey(model: string, input: Record<string, unknown>): string {
|
|
33
28
|
const inputStr = JSON.stringify(input, Object.keys(input).sort());
|
|
34
|
-
// Use DJB2 hash for input fingerprinting
|
|
29
|
+
// Use DJB2 hash for input fingerprinting
|
|
35
30
|
let hash = 0;
|
|
36
31
|
for (let i = 0; i < inputStr.length; i++) {
|
|
37
32
|
const char = inputStr.charCodeAt(i);
|
|
38
33
|
hash = ((hash << 5) - hash + char) | 0;
|
|
39
34
|
}
|
|
40
|
-
//
|
|
41
|
-
const uniqueId =
|
|
35
|
+
// Use crypto.randomUUID() for guaranteed uniqueness without race conditions
|
|
36
|
+
const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
37
|
+
? crypto.randomUUID()
|
|
38
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
42
39
|
return `${model}:${hash.toString(36)}:${uniqueId}`;
|
|
43
40
|
}
|
|
44
41
|
|
|
@@ -56,10 +53,7 @@ export function removeRequest(key: string): void {
|
|
|
56
53
|
|
|
57
54
|
export function cancelAllRequests(): void {
|
|
58
55
|
const store = getRequestStore();
|
|
59
|
-
store.forEach((req
|
|
60
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
console.log(`[RequestStore] Cancelling: ${key}`);
|
|
62
|
-
}
|
|
56
|
+
store.forEach((req) => {
|
|
63
57
|
req.abortController.abort();
|
|
64
58
|
});
|
|
65
59
|
store.clear();
|
|
@@ -81,7 +75,7 @@ export function cleanupRequestStore(_maxAge: number = 300000): void {
|
|
|
81
75
|
|
|
82
76
|
// Requests are automatically removed when they complete (via finally block)
|
|
83
77
|
// This function exists for future enhancements like time-based cleanup
|
|
84
|
-
if (store.size > 50
|
|
85
|
-
|
|
78
|
+
if (store.size > 50) {
|
|
79
|
+
// Store size exceeds threshold - indicates potential memory leak
|
|
86
80
|
}
|
|
87
81
|
}
|
|
@@ -10,8 +10,6 @@ import type {
|
|
|
10
10
|
} from "../../domain/entities/cost-tracking.types";
|
|
11
11
|
import { findModelById } from "../../domain/constants/default-models.constants";
|
|
12
12
|
|
|
13
|
-
declare const __DEV__: boolean | undefined;
|
|
14
|
-
|
|
15
13
|
interface CostSummary {
|
|
16
14
|
totalEstimatedCost: number;
|
|
17
15
|
totalActualCost: number;
|
|
@@ -69,14 +67,8 @@ export class CostTracker {
|
|
|
69
67
|
currency: this.config.currency,
|
|
70
68
|
};
|
|
71
69
|
}
|
|
72
|
-
|
|
73
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
74
|
-
console.warn("[CostTracker] No pricing found for model:", modelId);
|
|
75
|
-
}
|
|
76
70
|
} catch (error) {
|
|
77
|
-
|
|
78
|
-
console.warn("[CostTracker] Error finding model:", modelId, error);
|
|
79
|
-
}
|
|
71
|
+
// Silently return default cost info on error
|
|
80
72
|
}
|
|
81
73
|
|
|
82
74
|
return {
|
|
@@ -164,6 +156,7 @@ export class CostTracker {
|
|
|
164
156
|
clearHistory(): void {
|
|
165
157
|
this.costHistory = [];
|
|
166
158
|
this.currentOperationCosts.clear();
|
|
159
|
+
this.operationCounter = 0;
|
|
167
160
|
}
|
|
168
161
|
|
|
169
162
|
getCostsByModel(modelId: string): GenerationCost[] {
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import type { CostTracker } from "./cost-tracker";
|
|
7
7
|
|
|
8
|
-
declare const __DEV__: boolean | undefined;
|
|
9
|
-
|
|
10
8
|
interface ExecuteWithCostTrackingOptions<T> {
|
|
11
9
|
tracker: CostTracker | null;
|
|
12
10
|
model: string;
|
|
@@ -37,9 +35,8 @@ export async function executeWithCostTracking<T>(
|
|
|
37
35
|
const requestId = getRequestId?.(result);
|
|
38
36
|
tracker.completeOperation(operationId, model, operation, requestId);
|
|
39
37
|
} catch (costError) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
38
|
+
// Cost tracking failure shouldn't break the operation
|
|
39
|
+
// Log for debugging but don't throw
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
return result;
|
|
@@ -47,7 +44,7 @@ export async function executeWithCostTracking<T>(
|
|
|
47
44
|
try {
|
|
48
45
|
tracker.failOperation(operationId);
|
|
49
46
|
} catch {
|
|
50
|
-
//
|
|
47
|
+
// Cost tracking cleanup failure on error path - ignore
|
|
51
48
|
}
|
|
52
49
|
throw error;
|
|
53
50
|
}
|
|
@@ -42,10 +42,3 @@ export function mapFalError(error: unknown): FalErrorInfo {
|
|
|
42
42
|
export function isFalErrorRetryable(error: unknown): boolean {
|
|
43
43
|
return categorizeFalError(error).retryable;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
// Backward compatibility
|
|
47
|
-
export const falErrorMapper = {
|
|
48
|
-
mapToErrorInfo: mapFalError,
|
|
49
|
-
isRetryable: isFalErrorRetryable,
|
|
50
|
-
getErrorType: (error: unknown) => categorizeFalError(error).type,
|
|
51
|
-
};
|
|
@@ -7,12 +7,8 @@ import { fal } from "@fal-ai/client";
|
|
|
7
7
|
import {
|
|
8
8
|
base64ToTempFile,
|
|
9
9
|
deleteTempFile,
|
|
10
|
-
getFileSize,
|
|
11
|
-
detectMimeType,
|
|
12
10
|
} from "@umituz/react-native-design-system/filesystem";
|
|
13
11
|
|
|
14
|
-
declare const __DEV__: boolean | undefined;
|
|
15
|
-
|
|
16
12
|
/**
|
|
17
13
|
* Upload base64 image to FAL storage
|
|
18
14
|
* Uses design system's filesystem utilities for React Native compatibility
|
|
@@ -20,30 +16,22 @@ declare const __DEV__: boolean | undefined;
|
|
|
20
16
|
export async function uploadToFalStorage(base64: string): Promise<string> {
|
|
21
17
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
|
|
22
18
|
const tempUri = (await base64ToTempFile(base64));
|
|
23
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
24
|
-
const fileSize = getFileSize(tempUri);
|
|
25
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
26
|
-
const mimeType = detectMimeType(base64);
|
|
27
|
-
|
|
28
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
29
|
-
console.log("[FalStorage] Uploading image", {
|
|
30
|
-
size: `${(fileSize / 1024).toFixed(1)}KB`,
|
|
31
|
-
type: mimeType,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
19
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(tempUri);
|
|
22
|
+
const blob = await response.blob();
|
|
23
|
+
const url = await fal.storage.upload(blob);
|
|
24
|
+
return url;
|
|
25
|
+
} finally {
|
|
26
|
+
if (tempUri) {
|
|
27
|
+
try {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
29
|
+
await deleteTempFile(tempUri);
|
|
30
|
+
} catch {
|
|
31
|
+
// Silently ignore cleanup failures
|
|
32
|
+
}
|
|
33
|
+
}
|
|
44
34
|
}
|
|
45
|
-
|
|
46
|
-
return url;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
37
|
/**
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
export { categorizeFalError } from "./error-categorizer";
|
|
6
6
|
export {
|
|
7
|
-
falErrorMapper,
|
|
8
7
|
mapFalError,
|
|
9
8
|
isFalErrorRetryable,
|
|
10
9
|
} from "./error-mapper";
|
|
@@ -12,15 +11,6 @@ export {
|
|
|
12
11
|
export {
|
|
13
12
|
buildSingleImageInput,
|
|
14
13
|
buildDualImageInput,
|
|
15
|
-
buildUpscaleInput,
|
|
16
|
-
buildPhotoRestoreInput,
|
|
17
|
-
buildVideoFromImageInput,
|
|
18
|
-
buildFaceSwapInput,
|
|
19
|
-
buildImageToImageInput,
|
|
20
|
-
buildRemoveBackgroundInput,
|
|
21
|
-
buildRemoveObjectInput,
|
|
22
|
-
buildReplaceBackgroundInput,
|
|
23
|
-
buildHDTouchUpInput,
|
|
24
14
|
} from "./input-builders.util";
|
|
25
15
|
|
|
26
16
|
export {
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { uploadToFalStorage } from "./fal-storage.util";
|
|
7
7
|
|
|
8
|
-
declare const __DEV__: boolean | undefined;
|
|
9
|
-
|
|
10
8
|
const IMAGE_URL_KEYS = [
|
|
11
9
|
"image_url",
|
|
12
10
|
"second_image_url",
|
|
@@ -37,23 +35,11 @@ export async function preprocessInput(
|
|
|
37
35
|
for (const key of IMAGE_URL_KEYS) {
|
|
38
36
|
const value = result[key];
|
|
39
37
|
if (isBase64DataUri(value)) {
|
|
40
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
41
|
-
console.log(`[FalPreprocessor] Uploading ${key} to storage...`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
38
|
const uploadPromise = uploadToFalStorage(value)
|
|
45
39
|
.then((url) => {
|
|
46
40
|
result[key] = url;
|
|
47
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
-
console.log(`[FalPreprocessor] ${key} uploaded`, {
|
|
49
|
-
url: url.slice(0, 50) + "...",
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
41
|
})
|
|
53
42
|
.catch((error) => {
|
|
54
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
55
|
-
console.error(`[FalPreprocessor] Failed to upload ${key}:`, error);
|
|
56
|
-
}
|
|
57
43
|
throw new Error(`Failed to upload ${key}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
58
44
|
});
|
|
59
45
|
|
|
@@ -62,52 +48,36 @@ export async function preprocessInput(
|
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
// Handle image_urls array (for multi-person generation)
|
|
65
|
-
if (Array.isArray(result.image_urls)) {
|
|
51
|
+
if (Array.isArray(result.image_urls) && result.image_urls.length > 0) {
|
|
66
52
|
const imageUrls = result.image_urls as unknown[];
|
|
67
|
-
|
|
68
|
-
const processedUrls: string[] = new Array(imageUrls.length).fill("") as string[];
|
|
53
|
+
const processedUrls: string[] = [];
|
|
69
54
|
|
|
70
55
|
for (let i = 0; i < imageUrls.length; i++) {
|
|
71
56
|
const imageUrl = imageUrls[i];
|
|
72
57
|
if (isBase64DataUri(imageUrl)) {
|
|
73
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
74
|
-
console.log(`[FalPreprocessor] Uploading image_urls[${i}] to storage...`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Capture index in closure to ensure correct assignment
|
|
78
58
|
const index = i;
|
|
79
59
|
const uploadPromise = uploadToFalStorage(imageUrl)
|
|
80
60
|
.then((url) => {
|
|
81
61
|
processedUrls[index] = url;
|
|
82
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
83
|
-
console.log(`[FalPreprocessor] image_urls[${index}] uploaded`, {
|
|
84
|
-
url: url.slice(0, 50) + "...",
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
62
|
})
|
|
88
63
|
.catch((error) => {
|
|
89
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
90
|
-
console.error(`[FalPreprocessor] Failed to upload image_urls[${index}]:`, error);
|
|
91
|
-
}
|
|
92
64
|
throw new Error(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
93
65
|
});
|
|
94
66
|
|
|
95
67
|
uploadPromises.push(uploadPromise);
|
|
96
68
|
} else if (typeof imageUrl === "string") {
|
|
97
69
|
processedUrls[i] = imageUrl;
|
|
70
|
+
} else {
|
|
71
|
+
processedUrls[i] = "";
|
|
98
72
|
}
|
|
99
73
|
}
|
|
100
74
|
|
|
101
|
-
// Always set processed URLs after all uploads complete
|
|
102
75
|
result.image_urls = processedUrls;
|
|
103
76
|
}
|
|
104
77
|
|
|
105
78
|
// Wait for ALL uploads to complete (both individual keys and array)
|
|
106
79
|
if (uploadPromises.length > 0) {
|
|
107
80
|
await Promise.all(uploadPromises);
|
|
108
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
109
|
-
console.log(`[FalPreprocessor] All images uploaded (${uploadPromises.length})`);
|
|
110
|
-
}
|
|
111
81
|
}
|
|
112
82
|
|
|
113
83
|
return result;
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { isValidModelId, isValidPrompt } from "./type-guards.util";
|
|
7
7
|
|
|
8
|
-
declare const __DEV__: boolean | undefined;
|
|
9
|
-
|
|
10
8
|
export interface ValidationError {
|
|
11
9
|
field: string;
|
|
12
10
|
message: string;
|
|
@@ -75,18 +73,26 @@ export function validateInput(
|
|
|
75
73
|
|
|
76
74
|
for (const field of imageFields) {
|
|
77
75
|
const value = input[field];
|
|
78
|
-
if (value !== undefined
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
if (value !== undefined) {
|
|
77
|
+
if (typeof value !== "string") {
|
|
78
|
+
errors.push({
|
|
79
|
+
field,
|
|
80
|
+
message: `${field} must be a string`,
|
|
81
|
+
});
|
|
82
|
+
} else if (value.length > 0) {
|
|
83
|
+
const isValidUrl = value.startsWith('http://') || value.startsWith('https://');
|
|
84
|
+
const isValidBase64 = value.startsWith('data:image/');
|
|
85
|
+
if (!isValidUrl && !isValidBase64) {
|
|
86
|
+
errors.push({
|
|
87
|
+
field,
|
|
88
|
+
message: `${field} must be a valid URL or base64 data URI`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
if (errors.length > 0) {
|
|
87
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
88
|
-
console.warn("[InputValidator] Validation errors:", errors);
|
|
89
|
-
}
|
|
90
96
|
throw new InputValidationError(errors);
|
|
91
97
|
}
|
|
92
98
|
}
|
|
@@ -92,9 +92,10 @@ export function isValidModelId(value: unknown): boolean {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// FAL model IDs
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// FAL model IDs follow pattern: "owner/model-name" or "owner/model/version"
|
|
96
|
+
// Allow uppercase, dots, underscores, hyphens
|
|
97
|
+
const modelIdPattern = /^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+(\/[a-zA-Z0-9-_.]+)?$/;
|
|
98
|
+
return modelIdPattern.test(value) && value.length >= 3;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { NSFWContentError } from "../services/nsfw-content-error";
|
|
7
7
|
|
|
8
|
-
declare const __DEV__: boolean | undefined;
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Check if value indicates NSFW content
|
|
12
10
|
*/
|
|
@@ -33,9 +31,6 @@ export function validateNSFWContent(result: Record<string, unknown>): void {
|
|
|
33
31
|
if (nsfwConcepts && Array.isArray(nsfwConcepts)) {
|
|
34
32
|
const hasNSFW = nsfwConcepts.some((value) => value === true);
|
|
35
33
|
if (hasNSFW) {
|
|
36
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
37
|
-
console.log("[FalProvider] NSFW content detected (has_nsfw_concepts), rejecting result");
|
|
38
|
-
}
|
|
39
34
|
throw new NSFWContentError();
|
|
40
35
|
}
|
|
41
36
|
}
|
|
@@ -43,27 +38,18 @@ export function validateNSFWContent(result: Record<string, unknown>): void {
|
|
|
43
38
|
// Format 2: nsfw_detected boolean
|
|
44
39
|
const nsfwDetected = result?.nsfw_detected;
|
|
45
40
|
if (nsfwDetected !== undefined && isNSFWIndicator(nsfwDetected)) {
|
|
46
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
-
console.log("[FalProvider] NSFW content detected (nsfw_detected), rejecting result");
|
|
48
|
-
}
|
|
49
41
|
throw new NSFWContentError();
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
// Format 3: is_nsfw boolean
|
|
53
45
|
const isNsfw = result?.is_nsfw;
|
|
54
46
|
if (isNsfw !== undefined && isNSFWIndicator(isNsfw)) {
|
|
55
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
-
console.log("[FalProvider] NSFW content detected (is_nsfw), rejecting result");
|
|
57
|
-
}
|
|
58
47
|
throw new NSFWContentError();
|
|
59
48
|
}
|
|
60
49
|
|
|
61
50
|
// Format 4: nsfw_score number (> 0.5 threshold)
|
|
62
51
|
const nsfwScore = result?.nsfw_score as number | undefined;
|
|
63
52
|
if (typeof nsfwScore === "number" && nsfwScore > 0.5) {
|
|
64
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
65
|
-
console.log("[FalProvider] NSFW content detected (nsfw_score: " + nsfwScore + "), rejecting result");
|
|
66
|
-
}
|
|
67
53
|
throw new NSFWContentError();
|
|
68
54
|
}
|
|
69
55
|
|
|
@@ -72,9 +58,6 @@ export function validateNSFWContent(result: Record<string, unknown>): void {
|
|
|
72
58
|
if (policyViolation && typeof policyViolation === "object") {
|
|
73
59
|
const type = (policyViolation.type || "").toLowerCase();
|
|
74
60
|
if (type.includes("nsfw") || type.includes("adult") || type.includes("explicit")) {
|
|
75
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
-
console.log("[FalProvider] Content policy violation detected:", policyViolation);
|
|
77
|
-
}
|
|
78
61
|
throw new NSFWContentError();
|
|
79
62
|
}
|
|
80
63
|
}
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import { falProvider } from '../infrastructure/services';
|
|
7
7
|
|
|
8
|
-
declare const __DEV__: boolean;
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* InitModule interface (from @umituz/react-native-design-system)
|
|
12
10
|
*/
|
|
@@ -24,18 +22,6 @@ export interface AiProviderInitModuleConfig {
|
|
|
24
22
|
*/
|
|
25
23
|
getApiKey: () => string | undefined;
|
|
26
24
|
|
|
27
|
-
/**
|
|
28
|
-
* Video feature models mapping
|
|
29
|
-
* Maps feature types to FAL model IDs
|
|
30
|
-
*/
|
|
31
|
-
videoFeatureModels?: Record<string, string>;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Image feature models mapping
|
|
35
|
-
* Maps feature types to FAL model IDs
|
|
36
|
-
*/
|
|
37
|
-
imageFeatureModels?: Record<string, string>;
|
|
38
|
-
|
|
39
25
|
/**
|
|
40
26
|
* Whether this module is critical for app startup
|
|
41
27
|
* @default false
|
|
@@ -63,10 +49,6 @@ export interface AiProviderInitModuleConfig {
|
|
|
63
49
|
* createFirebaseInitModule(),
|
|
64
50
|
* createAiProviderInitModule({
|
|
65
51
|
* getApiKey: () => getFalApiKey(),
|
|
66
|
-
* videoFeatureModels: {
|
|
67
|
-
* "image-to-video": "fal-ai/wan-25-preview/image-to-video",
|
|
68
|
-
* "text-to-video": "fal-ai/wan-25-preview/text-to-video",
|
|
69
|
-
* },
|
|
70
52
|
* }),
|
|
71
53
|
* ],
|
|
72
54
|
* });
|
|
@@ -77,8 +59,6 @@ export function createAiProviderInitModule(
|
|
|
77
59
|
): InitModule {
|
|
78
60
|
const {
|
|
79
61
|
getApiKey,
|
|
80
|
-
videoFeatureModels,
|
|
81
|
-
imageFeatureModels,
|
|
82
62
|
critical = false,
|
|
83
63
|
dependsOn = ['firebase'],
|
|
84
64
|
} = config;
|
|
@@ -92,30 +72,16 @@ export function createAiProviderInitModule(
|
|
|
92
72
|
const apiKey = getApiKey();
|
|
93
73
|
|
|
94
74
|
if (!apiKey) {
|
|
95
|
-
|
|
96
|
-
console.log('[createAiProviderInitModule] No API key - skipping');
|
|
97
|
-
}
|
|
98
|
-
return Promise.resolve(true); // Not an error, just skip
|
|
75
|
+
return Promise.resolve(false);
|
|
99
76
|
}
|
|
100
77
|
|
|
101
|
-
// Initialize FAL provider
|
|
102
78
|
falProvider.initialize({
|
|
103
79
|
apiKey,
|
|
104
|
-
videoFeatureModels,
|
|
105
|
-
imageFeatureModels,
|
|
106
80
|
});
|
|
107
81
|
|
|
108
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
109
|
-
console.log('[createAiProviderInitModule] FAL provider initialized');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
82
|
return Promise.resolve(true);
|
|
113
83
|
} catch (error) {
|
|
114
|
-
|
|
115
|
-
console.error('[createAiProviderInitModule] Error:', error);
|
|
116
|
-
}
|
|
117
|
-
// Continue on error - AI provider is not critical
|
|
118
|
-
return Promise.resolve(true);
|
|
84
|
+
return Promise.resolve(false);
|
|
119
85
|
}
|
|
120
86
|
},
|
|
121
87
|
};
|
|
@@ -52,11 +52,9 @@ export function useFalGeneration<T = unknown>(
|
|
|
52
52
|
const result = await falProvider.subscribe<T>(modelEndpoint, input, {
|
|
53
53
|
timeoutMs: options?.timeoutMs,
|
|
54
54
|
onQueueUpdate: (status) => {
|
|
55
|
-
// Update requestId ref when we receive it from status
|
|
56
55
|
if (status.requestId) {
|
|
57
56
|
currentRequestIdRef.current = status.requestId;
|
|
58
57
|
}
|
|
59
|
-
// Map JobStatus to FalQueueStatus for backward compatibility
|
|
60
58
|
options?.onProgress?.({
|
|
61
59
|
status: status.status,
|
|
62
60
|
requestId: status.requestId ?? currentRequestIdRef.current ?? "",
|
|
@@ -95,9 +93,6 @@ export function useFalGeneration<T = unknown>(
|
|
|
95
93
|
if (falProvider.hasRunningRequest()) {
|
|
96
94
|
setIsCancelling(true);
|
|
97
95
|
falProvider.cancelCurrentRequest();
|
|
98
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
99
|
-
console.log("[useFalGeneration] Request cancelled");
|
|
100
|
-
}
|
|
101
96
|
}
|
|
102
97
|
}, []);
|
|
103
98
|
|
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
UseModelsReturn,
|
|
20
20
|
} from "../../domain/types/model-selection.types";
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
export type { UseModelsReturn } from "../../domain/types/model-selection.types";
|
|
23
23
|
|
|
24
24
|
export interface UseModelsProps {
|
|
25
25
|
/** Model type to fetch */
|
|
@@ -56,11 +56,6 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
|
|
|
56
56
|
setSelectedModel(initial);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
60
|
-
// eslint-disable-next-line no-console
|
|
61
|
-
console.log(`[useModels] Loaded ${fetchedModels.length} ${type} models`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
59
|
setIsLoading(false);
|
|
65
60
|
}, [type, config?.initialModelId, defaultModelId]);
|
|
66
61
|
|
|
@@ -73,11 +68,6 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
|
|
|
73
68
|
const model = models.find((m) => m.id === modelId);
|
|
74
69
|
if (model) {
|
|
75
70
|
setSelectedModel(model);
|
|
76
|
-
|
|
77
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
|
-
// eslint-disable-next-line no-console
|
|
79
|
-
console.log(`[useModels] Selected: ${model.name} (${model.id})`);
|
|
80
|
-
}
|
|
81
71
|
}
|
|
82
72
|
},
|
|
83
73
|
[models],
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image Feature Input Builder
|
|
3
|
-
* Builds inputs for image-based AI features
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ImageFeatureType,
|
|
8
|
-
ImageFeatureInputData,
|
|
9
|
-
} from "../../domain/types";
|
|
10
|
-
import { buildSingleImageInput } from "../utils/base-builders.util";
|
|
11
|
-
import {
|
|
12
|
-
buildUpscaleInput,
|
|
13
|
-
buildPhotoRestoreInput,
|
|
14
|
-
buildFaceSwapInput,
|
|
15
|
-
buildRemoveBackgroundInput,
|
|
16
|
-
buildRemoveObjectInput,
|
|
17
|
-
buildReplaceBackgroundInput,
|
|
18
|
-
buildKontextStyleTransferInput,
|
|
19
|
-
} from "../utils/image-feature-builders.util";
|
|
20
|
-
|
|
21
|
-
const DEFAULT_ANIME_SELFIE_PROMPT = "Transform this person into anime style illustration. Keep the same gender, face structure, hair color, eye color, and expression. Make it look like a high-quality anime character portrait with vibrant colors and clean lineart.";
|
|
22
|
-
|
|
23
|
-
export function buildImageFeatureInput(
|
|
24
|
-
feature: ImageFeatureType,
|
|
25
|
-
data: ImageFeatureInputData,
|
|
26
|
-
): Record<string, unknown> {
|
|
27
|
-
const { imageBase64, targetImageBase64, prompt, options } = data;
|
|
28
|
-
|
|
29
|
-
switch (feature) {
|
|
30
|
-
case "upscale":
|
|
31
|
-
case "hd-touch-up":
|
|
32
|
-
return buildUpscaleInput(imageBase64, options);
|
|
33
|
-
|
|
34
|
-
case "photo-restore":
|
|
35
|
-
return buildPhotoRestoreInput(imageBase64, options);
|
|
36
|
-
|
|
37
|
-
case "face-swap":
|
|
38
|
-
if (!targetImageBase64) {
|
|
39
|
-
throw new Error("Face swap requires target image");
|
|
40
|
-
}
|
|
41
|
-
return buildFaceSwapInput(imageBase64, targetImageBase64, options);
|
|
42
|
-
|
|
43
|
-
case "remove-background":
|
|
44
|
-
return buildRemoveBackgroundInput(imageBase64, options);
|
|
45
|
-
|
|
46
|
-
case "remove-object":
|
|
47
|
-
return buildRemoveObjectInput(imageBase64, { prompt, ...options });
|
|
48
|
-
|
|
49
|
-
case "replace-background":
|
|
50
|
-
if (!prompt) {
|
|
51
|
-
throw new Error("Replace background requires prompt");
|
|
52
|
-
}
|
|
53
|
-
return buildReplaceBackgroundInput(imageBase64, { prompt, ...options });
|
|
54
|
-
|
|
55
|
-
case "anime-selfie":
|
|
56
|
-
return buildKontextStyleTransferInput(imageBase64, {
|
|
57
|
-
prompt: prompt || (options?.prompt as string) || DEFAULT_ANIME_SELFIE_PROMPT,
|
|
58
|
-
guidance_scale: options?.guidance_scale as number | undefined,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
default:
|
|
62
|
-
return buildSingleImageInput(imageBase64, options);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Video Feature Input Builder
|
|
3
|
-
* Builds inputs for video-based AI features
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
VideoFeatureType,
|
|
8
|
-
VideoFeatureInputData,
|
|
9
|
-
} from "../../domain/types";
|
|
10
|
-
import {
|
|
11
|
-
buildVideoFromImageInput,
|
|
12
|
-
buildTextToVideoInput,
|
|
13
|
-
} from "../utils/video-feature-builders.util";
|
|
14
|
-
|
|
15
|
-
const DEFAULT_VIDEO_PROMPTS: Partial<Record<VideoFeatureType, string>> = {
|
|
16
|
-
"image-to-video": "Animate this image with natural, smooth motion while preserving all details",
|
|
17
|
-
"text-to-video": "Generate a high-quality video based on the description, smooth motion",
|
|
18
|
-
} as const;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Features that require image input
|
|
22
|
-
*/
|
|
23
|
-
const IMAGE_REQUIRED_FEATURES: readonly VideoFeatureType[] = [
|
|
24
|
-
"image-to-video",
|
|
25
|
-
] as const;
|
|
26
|
-
|
|
27
|
-
function isImageRequiredFeature(feature: VideoFeatureType): boolean {
|
|
28
|
-
return IMAGE_REQUIRED_FEATURES.includes(feature);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function buildVideoFeatureInput(
|
|
32
|
-
feature: VideoFeatureType,
|
|
33
|
-
data: VideoFeatureInputData,
|
|
34
|
-
): Record<string, unknown> {
|
|
35
|
-
const { sourceImageBase64, prompt, options } = data;
|
|
36
|
-
const effectivePrompt = prompt || DEFAULT_VIDEO_PROMPTS[feature] || "Generate video";
|
|
37
|
-
|
|
38
|
-
if (isImageRequiredFeature(feature)) {
|
|
39
|
-
return buildVideoFromImageInput(sourceImageBase64 || "", {
|
|
40
|
-
prompt: effectivePrompt,
|
|
41
|
-
duration: options?.duration as number | undefined,
|
|
42
|
-
resolution: options?.resolution as string | undefined,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return buildTextToVideoInput({
|
|
47
|
-
prompt: effectivePrompt,
|
|
48
|
-
duration: options?.duration as number | undefined,
|
|
49
|
-
aspectRatio: options?.aspect_ratio as string | undefined,
|
|
50
|
-
resolution: options?.resolution as string | undefined,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FAL Feature Models - Model resolution and input building
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
ImageFeatureType,
|
|
7
|
-
VideoFeatureType,
|
|
8
|
-
ImageFeatureInputData,
|
|
9
|
-
VideoFeatureInputData,
|
|
10
|
-
} from "../../domain/types";
|
|
11
|
-
import {
|
|
12
|
-
buildImageFeatureInput as buildImageFeatureInputImpl,
|
|
13
|
-
} from "../builders/image-feature-builder";
|
|
14
|
-
import {
|
|
15
|
-
buildVideoFeatureInput as buildVideoFeatureInputImpl,
|
|
16
|
-
} from "../builders/video-feature-builder";
|
|
17
|
-
|
|
18
|
-
export function getImageFeatureModel(
|
|
19
|
-
imageFeatureModels: Record<string, string>,
|
|
20
|
-
feature: ImageFeatureType,
|
|
21
|
-
): string {
|
|
22
|
-
const model = imageFeatureModels[feature];
|
|
23
|
-
if (!model) throw new Error(`No model for image feature: ${feature}`);
|
|
24
|
-
return model;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getVideoFeatureModel(
|
|
28
|
-
videoFeatureModels: Record<string, string>,
|
|
29
|
-
feature: VideoFeatureType,
|
|
30
|
-
): string {
|
|
31
|
-
const model = videoFeatureModels[feature];
|
|
32
|
-
if (!model) throw new Error(`No model for video feature: ${feature}`);
|
|
33
|
-
return model;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function buildImageFeatureInput(
|
|
37
|
-
feature: ImageFeatureType,
|
|
38
|
-
data: ImageFeatureInputData,
|
|
39
|
-
): Record<string, unknown> {
|
|
40
|
-
return buildImageFeatureInputImpl(feature, data);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function buildVideoFeatureInput(
|
|
44
|
-
feature: VideoFeatureType,
|
|
45
|
-
data: VideoFeatureInputData,
|
|
46
|
-
): Record<string, unknown> {
|
|
47
|
-
return buildVideoFeatureInputImpl(feature, data);
|
|
48
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image Feature Input Builders
|
|
3
|
-
* Builder functions for specific image features
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
UpscaleOptions,
|
|
8
|
-
PhotoRestoreOptions,
|
|
9
|
-
RemoveBackgroundOptions,
|
|
10
|
-
RemoveObjectOptions,
|
|
11
|
-
ReplaceBackgroundOptions,
|
|
12
|
-
FaceSwapOptions,
|
|
13
|
-
} from "../../domain/types";
|
|
14
|
-
import { buildSingleImageInput } from "./base-builders.util";
|
|
15
|
-
import { formatImageDataUri } from "./image-helpers.util";
|
|
16
|
-
|
|
17
|
-
export function buildUpscaleInput(
|
|
18
|
-
base64: string,
|
|
19
|
-
options?: UpscaleOptions,
|
|
20
|
-
): Record<string, unknown> {
|
|
21
|
-
return buildSingleImageInput(base64, {
|
|
22
|
-
scale: options?.scaleFactor ?? 2,
|
|
23
|
-
face_enhance: options?.enhanceFaces ?? false,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildPhotoRestoreInput(
|
|
28
|
-
base64: string,
|
|
29
|
-
options?: PhotoRestoreOptions,
|
|
30
|
-
): Record<string, unknown> {
|
|
31
|
-
return buildSingleImageInput(base64, {
|
|
32
|
-
face_enhance: options?.enhanceFaces ?? true,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function buildFaceSwapInput(
|
|
37
|
-
sourceBase64: string,
|
|
38
|
-
targetBase64: string,
|
|
39
|
-
_options?: FaceSwapOptions,
|
|
40
|
-
): Record<string, unknown> {
|
|
41
|
-
return {
|
|
42
|
-
base_image_url: formatImageDataUri(sourceBase64),
|
|
43
|
-
swap_image_url: formatImageDataUri(targetBase64),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function buildRemoveBackgroundInput(
|
|
48
|
-
base64: string,
|
|
49
|
-
options?: RemoveBackgroundOptions & {
|
|
50
|
-
model?: string;
|
|
51
|
-
operating_resolution?: string;
|
|
52
|
-
output_format?: string;
|
|
53
|
-
refine_foreground?: boolean;
|
|
54
|
-
},
|
|
55
|
-
): Record<string, unknown> {
|
|
56
|
-
return buildSingleImageInput(base64, {
|
|
57
|
-
model: options?.model ?? "General Use (Light)",
|
|
58
|
-
operating_resolution: options?.operating_resolution ?? "1024x1024",
|
|
59
|
-
output_format: options?.output_format ?? "png",
|
|
60
|
-
refine_foreground: options?.refine_foreground ?? true,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function buildRemoveObjectInput(
|
|
65
|
-
base64: string,
|
|
66
|
-
options?: RemoveObjectOptions,
|
|
67
|
-
): Record<string, unknown> {
|
|
68
|
-
return buildSingleImageInput(base64, {
|
|
69
|
-
mask_url: options?.mask,
|
|
70
|
-
prompt: options?.prompt || "Remove the object and fill with background",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function buildReplaceBackgroundInput(
|
|
75
|
-
base64: string,
|
|
76
|
-
options: ReplaceBackgroundOptions,
|
|
77
|
-
): Record<string, unknown> {
|
|
78
|
-
return buildSingleImageInput(base64, {
|
|
79
|
-
prompt: options.prompt,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function buildHDTouchUpInput(
|
|
84
|
-
base64: string,
|
|
85
|
-
options?: UpscaleOptions,
|
|
86
|
-
): Record<string, unknown> {
|
|
87
|
-
return buildUpscaleInput(base64, options);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface KontextStyleTransferOptions {
|
|
91
|
-
prompt: string;
|
|
92
|
-
guidance_scale?: number;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function buildKontextStyleTransferInput(
|
|
96
|
-
base64: string,
|
|
97
|
-
options: KontextStyleTransferOptions,
|
|
98
|
-
): Record<string, unknown> {
|
|
99
|
-
return buildSingleImageInput(base64, {
|
|
100
|
-
prompt: options.prompt,
|
|
101
|
-
guidance_scale: options.guidance_scale ?? 3.5,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Video Feature Input Builders
|
|
3
|
-
* Builder functions for video features
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
ImageToImagePromptConfig,
|
|
8
|
-
VideoFromImageOptions,
|
|
9
|
-
TextToVideoOptions,
|
|
10
|
-
} from "../../domain/types";
|
|
11
|
-
import { buildSingleImageInput } from "./base-builders.util";
|
|
12
|
-
import { formatImageDataUri } from "./image-helpers.util";
|
|
13
|
-
|
|
14
|
-
export function buildImageToImageInput(
|
|
15
|
-
base64: string,
|
|
16
|
-
promptConfig: ImageToImagePromptConfig,
|
|
17
|
-
): Record<string, unknown> {
|
|
18
|
-
return buildSingleImageInput(base64, {
|
|
19
|
-
prompt: promptConfig.prompt,
|
|
20
|
-
negative_prompt: promptConfig.negativePrompt,
|
|
21
|
-
strength: promptConfig.strength ?? 0.85,
|
|
22
|
-
num_inference_steps: promptConfig.num_inference_steps ?? 50,
|
|
23
|
-
guidance_scale: promptConfig.guidance_scale ?? 7.5,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function buildVideoFromImageInput(
|
|
28
|
-
base64: string,
|
|
29
|
-
options?: VideoFromImageOptions & {
|
|
30
|
-
enable_safety_checker?: boolean;
|
|
31
|
-
default_prompt?: string;
|
|
32
|
-
},
|
|
33
|
-
): Record<string, unknown> {
|
|
34
|
-
return {
|
|
35
|
-
prompt: options?.prompt || options?.default_prompt || "Generate natural motion video",
|
|
36
|
-
image_url: formatImageDataUri(base64),
|
|
37
|
-
enable_safety_checker: options?.enable_safety_checker ?? false,
|
|
38
|
-
...(options?.duration && { duration: options.duration }),
|
|
39
|
-
...(options?.resolution && { resolution: options.resolution }),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Build input for text-to-video generation (no image required)
|
|
45
|
-
*/
|
|
46
|
-
export function buildTextToVideoInput(
|
|
47
|
-
options: TextToVideoOptions,
|
|
48
|
-
): Record<string, unknown> {
|
|
49
|
-
const { prompt, duration, aspectRatio, resolution } = options;
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
prompt,
|
|
53
|
-
enable_safety_checker: false,
|
|
54
|
-
...(duration && { duration }),
|
|
55
|
-
...(aspectRatio && { aspect_ratio: aspectRatio }),
|
|
56
|
-
...(resolution && { resolution }),
|
|
57
|
-
};
|
|
58
|
-
}
|