@umituz/react-native-ai-fal-provider 1.1.8 → 1.1.9
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
CHANGED
|
@@ -12,6 +12,42 @@ import { validateNSFWContent } from "../validators/nsfw-validator";
|
|
|
12
12
|
|
|
13
13
|
declare const __DEV__: boolean | undefined;
|
|
14
14
|
|
|
15
|
+
interface FalApiErrorDetail {
|
|
16
|
+
msg?: string;
|
|
17
|
+
type?: string;
|
|
18
|
+
loc?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FalApiError {
|
|
22
|
+
body?: { detail?: FalApiErrorDetail[] } | string;
|
|
23
|
+
message?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse FAL API error and extract user-friendly message
|
|
28
|
+
*/
|
|
29
|
+
function parseFalError(error: unknown): string {
|
|
30
|
+
const fallback = error instanceof Error ? error.message : String(error);
|
|
31
|
+
|
|
32
|
+
const falError = error as FalApiError;
|
|
33
|
+
if (!falError?.body) return fallback;
|
|
34
|
+
|
|
35
|
+
const body = typeof falError.body === "string"
|
|
36
|
+
? safeJsonParse(falError.body)
|
|
37
|
+
: falError.body;
|
|
38
|
+
|
|
39
|
+
const detail = body?.detail?.[0];
|
|
40
|
+
return detail?.msg ?? falError.message ?? fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safeJsonParse(str: string): { detail?: FalApiErrorDetail[] } | null {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(str);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
/**
|
|
16
52
|
* Handle FAL subscription with timeout and cancellation
|
|
17
53
|
*/
|
|
@@ -91,6 +127,13 @@ export async function handleFalSubscription<T = unknown>(
|
|
|
91
127
|
|
|
92
128
|
options?.onResult?.(result as T);
|
|
93
129
|
return { result: result as T, requestId: currentRequestId };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Parse FAL error and throw with user-friendly message
|
|
132
|
+
const userMessage = parseFalError(error);
|
|
133
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
134
|
+
console.error("[FalProvider] Error:", userMessage);
|
|
135
|
+
}
|
|
136
|
+
throw new Error(userMessage);
|
|
94
137
|
} finally {
|
|
95
138
|
if (timeoutId) clearTimeout(timeoutId);
|
|
96
139
|
}
|
|
@@ -110,16 +153,24 @@ export async function handleFalRun<T = unknown>(
|
|
|
110
153
|
console.log("[FalProvider] run() model:", model, "inputKeys:", Object.keys(input));
|
|
111
154
|
}
|
|
112
155
|
|
|
113
|
-
|
|
156
|
+
try {
|
|
157
|
+
const result = await fal.run(model, { input });
|
|
114
158
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
159
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
160
|
+
console.log("[FalProvider] run() raw result:", JSON.stringify(result, null, 2));
|
|
161
|
+
console.log("[FalProvider] run() result type:", typeof result);
|
|
162
|
+
console.log("[FalProvider] run() result keys:", result ? Object.keys(result as object) : "null");
|
|
163
|
+
}
|
|
120
164
|
|
|
121
|
-
|
|
165
|
+
validateNSFWContent(result as Record<string, unknown>);
|
|
122
166
|
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
options?.onProgress?.({ progress: 100, status: "COMPLETED" as const });
|
|
168
|
+
return result as T;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const userMessage = parseFalError(error);
|
|
171
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
172
|
+
console.error("[FalProvider] run() Error:", userMessage);
|
|
173
|
+
}
|
|
174
|
+
throw new Error(userMessage);
|
|
175
|
+
}
|
|
125
176
|
}
|
|
@@ -7,6 +7,7 @@ import { FalErrorType, type FalErrorCategory } from "../../domain/entities/error
|
|
|
7
7
|
const PATTERNS: Record<FalErrorType, string[]> = {
|
|
8
8
|
[FalErrorType.NETWORK]: ["network", "fetch", "connection", "econnrefused", "enotfound", "etimedout"],
|
|
9
9
|
[FalErrorType.TIMEOUT]: ["timeout", "timed out"],
|
|
10
|
+
[FalErrorType.IMAGE_TOO_SMALL]: ["image_too_small", "image dimensions are too small", "minimum dimensions"],
|
|
10
11
|
[FalErrorType.VALIDATION]: ["validation", "invalid", "unprocessable", "422", "bad request", "400"],
|
|
11
12
|
[FalErrorType.CONTENT_POLICY]: ["content_policy", "content policy", "policy violation", "nsfw", "inappropriate"],
|
|
12
13
|
[FalErrorType.RATE_LIMIT]: ["rate limit", "too many requests", "429", "quota"],
|
|
@@ -10,71 +10,17 @@ import {
|
|
|
10
10
|
getFileSize,
|
|
11
11
|
detectMimeType,
|
|
12
12
|
} from "@umituz/react-native-design-system/filesystem";
|
|
13
|
-
import { ImageTransformService } from "@umituz/react-native-design-system/image";
|
|
14
13
|
|
|
15
14
|
declare const __DEV__: boolean | undefined;
|
|
16
15
|
|
|
17
|
-
// FAL.ai minimum image dimensions
|
|
18
|
-
const MIN_WIDTH = 300;
|
|
19
|
-
const MIN_HEIGHT = 300;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Ensure image meets minimum dimension requirements
|
|
23
|
-
* Upscales small images to 300x300 minimum
|
|
24
|
-
*/
|
|
25
|
-
async function ensureMinimumDimensions(uri: string): Promise<string> {
|
|
26
|
-
try {
|
|
27
|
-
// Get image dimensions by reading the file
|
|
28
|
-
const { Image } = await import("react-native");
|
|
29
|
-
|
|
30
|
-
return new Promise((resolve) => {
|
|
31
|
-
Image.getSize(
|
|
32
|
-
uri,
|
|
33
|
-
async (width, height) => {
|
|
34
|
-
if (width >= MIN_WIDTH && height >= MIN_HEIGHT) {
|
|
35
|
-
resolve(uri);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Calculate new dimensions (maintain aspect ratio, ensure minimum)
|
|
40
|
-
const scale = Math.max(MIN_WIDTH / width, MIN_HEIGHT / height);
|
|
41
|
-
const newWidth = Math.ceil(width * scale);
|
|
42
|
-
const newHeight = Math.ceil(height * scale);
|
|
43
|
-
|
|
44
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
|
-
console.log("[FalStorage] Upscaling image", {
|
|
46
|
-
original: `${width}x${height}`,
|
|
47
|
-
upscaled: `${newWidth}x${newHeight}`,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const result = await ImageTransformService.resize(uri, newWidth, newHeight);
|
|
52
|
-
resolve(result.uri);
|
|
53
|
-
},
|
|
54
|
-
() => {
|
|
55
|
-
// If getSize fails, return original URI
|
|
56
|
-
resolve(uri);
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
} catch {
|
|
61
|
-
return uri;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
16
|
/**
|
|
66
17
|
* Upload base64 image to FAL storage
|
|
67
18
|
* Uses design system's filesystem utilities for React Native compatibility
|
|
68
|
-
* Automatically upscales images below 300x300 minimum
|
|
69
19
|
*/
|
|
70
20
|
export async function uploadToFalStorage(base64: string): Promise<string> {
|
|
71
|
-
|
|
72
|
-
const mimeType = detectMimeType(base64);
|
|
73
|
-
|
|
74
|
-
// Ensure minimum dimensions (FAL requires 300x300)
|
|
75
|
-
tempUri = await ensureMinimumDimensions(tempUri);
|
|
76
|
-
|
|
21
|
+
const tempUri = await base64ToTempFile(base64);
|
|
77
22
|
const fileSize = getFileSize(tempUri);
|
|
23
|
+
const mimeType = detectMimeType(base64);
|
|
78
24
|
|
|
79
25
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
26
|
console.log("[FalStorage] Uploading image", {
|