@umituz/react-native-ai-fal-provider 2.0.28 → 2.0.30
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 +1 -6
- package/src/exports/infrastructure.ts +3 -1
- package/src/exports/presentation.ts +9 -0
- package/src/infrastructure/services/fal-models.service.ts +88 -1
- package/src/infrastructure/services/fal-provider.ts +24 -4
- package/src/infrastructure/services/index.ts +14 -1
- package/src/infrastructure/services/request-store.ts +53 -6
- package/src/infrastructure/utils/cost-tracker.ts +12 -4
- package/src/infrastructure/utils/date-format.util.ts +64 -0
- package/src/infrastructure/utils/error-categorizer.ts +5 -43
- package/src/infrastructure/utils/error-mapper.ts +10 -69
- package/src/infrastructure/utils/fal-error-handler.util.ts +153 -0
- package/src/infrastructure/utils/fal-generation-state-manager.util.ts +93 -0
- package/src/infrastructure/utils/fal-storage.util.ts +9 -9
- package/src/infrastructure/utils/formatting.util.ts +28 -205
- package/src/infrastructure/utils/index.ts +7 -0
- package/src/infrastructure/utils/input-preprocessor.util.ts +16 -14
- package/src/infrastructure/utils/number-format.util.ts +79 -0
- package/src/infrastructure/utils/string-format.util.ts +73 -0
- package/src/presentation/hooks/use-fal-generation.ts +60 -38
- package/src/presentation/hooks/use-models.ts +11 -27
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Error Handler
|
|
3
|
+
* Unified error handling for FAL AI operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FalErrorInfo, FalErrorCategory, FalErrorType } from "../../domain/entities/error.types";
|
|
7
|
+
import { FalErrorType as ErrorTypeEnum } from "../../domain/entities/error.types";
|
|
8
|
+
import { safeJsonParseOrNull } from "./data-parsers.util";
|
|
9
|
+
|
|
10
|
+
const STATUS_CODES = ["400", "401", "402", "403", "404", "422", "429", "500", "502", "503", "504"];
|
|
11
|
+
|
|
12
|
+
interface FalApiErrorDetail {
|
|
13
|
+
msg?: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
loc?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface FalApiError {
|
|
19
|
+
body?: { detail?: FalApiErrorDetail[] } | string;
|
|
20
|
+
message?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ERROR_PATTERNS: Record<FalErrorType, string[]> = {
|
|
24
|
+
[ErrorTypeEnum.NETWORK]: ["network", "fetch", "connection", "econnrefused", "enotfound", "etimedout"],
|
|
25
|
+
[ErrorTypeEnum.TIMEOUT]: ["timeout", "timed out"],
|
|
26
|
+
[ErrorTypeEnum.IMAGE_TOO_SMALL]: ["image_too_small", "image dimensions are too small", "minimum dimensions"],
|
|
27
|
+
[ErrorTypeEnum.VALIDATION]: ["validation", "invalid", "unprocessable", "422", "bad request", "400"],
|
|
28
|
+
[ErrorTypeEnum.CONTENT_POLICY]: ["content_policy", "content policy", "policy violation", "nsfw", "inappropriate"],
|
|
29
|
+
[ErrorTypeEnum.RATE_LIMIT]: ["rate limit", "too many requests", "429"],
|
|
30
|
+
[ErrorTypeEnum.AUTHENTICATION]: ["unauthorized", "401", "forbidden", "403", "api key", "authentication"],
|
|
31
|
+
[ErrorTypeEnum.QUOTA_EXCEEDED]: ["quota exceeded", "insufficient credits", "billing", "payment required", "402"],
|
|
32
|
+
[ErrorTypeEnum.MODEL_NOT_FOUND]: ["model not found", "endpoint not found", "404", "not found"],
|
|
33
|
+
[ErrorTypeEnum.API_ERROR]: ["api error", "502", "503", "504", "500", "internal server error"],
|
|
34
|
+
[ErrorTypeEnum.UNKNOWN]: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const RETRYABLE_TYPES = new Set([
|
|
38
|
+
ErrorTypeEnum.NETWORK,
|
|
39
|
+
ErrorTypeEnum.TIMEOUT,
|
|
40
|
+
ErrorTypeEnum.RATE_LIMIT,
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract HTTP status code from error message
|
|
45
|
+
*/
|
|
46
|
+
function extractStatusCode(errorString: string): number | undefined {
|
|
47
|
+
const code = STATUS_CODES.find((c) => errorString.includes(c));
|
|
48
|
+
return code ? parseInt(code, 10) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse FAL API error and extract user-friendly message
|
|
53
|
+
*/
|
|
54
|
+
function parseFalApiError(error: unknown): string {
|
|
55
|
+
const fallback = error instanceof Error ? error.message : String(error);
|
|
56
|
+
|
|
57
|
+
const falError = error as FalApiError;
|
|
58
|
+
if (!falError?.body) return fallback;
|
|
59
|
+
|
|
60
|
+
const body = typeof falError.body === "string"
|
|
61
|
+
? safeJsonParseOrNull<{ detail?: FalApiErrorDetail[] }>(falError.body)
|
|
62
|
+
: falError.body;
|
|
63
|
+
|
|
64
|
+
const detail = body?.detail?.[0];
|
|
65
|
+
return detail?.msg ?? falError.message ?? fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if error string matches any of the provided patterns
|
|
70
|
+
*/
|
|
71
|
+
function matchesPatterns(errorString: string, patterns: string[]): boolean {
|
|
72
|
+
return patterns.some((pattern) => errorString.includes(pattern));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Categorize FAL error based on error message patterns
|
|
77
|
+
*/
|
|
78
|
+
function categorizeError(error: unknown): FalErrorCategory {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
const errorString = message.toLowerCase();
|
|
81
|
+
|
|
82
|
+
for (const [type, patterns] of Object.entries(ERROR_PATTERNS)) {
|
|
83
|
+
if (patterns.length > 0 && matchesPatterns(errorString, patterns)) {
|
|
84
|
+
const errorType = type as FalErrorType;
|
|
85
|
+
return {
|
|
86
|
+
type: errorType,
|
|
87
|
+
messageKey: errorType,
|
|
88
|
+
retryable: RETRYABLE_TYPES.has(errorType),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { type: ErrorTypeEnum.UNKNOWN, messageKey: "unknown", retryable: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Map error to FalErrorInfo with full error details
|
|
98
|
+
*/
|
|
99
|
+
export function mapFalError(error: unknown): FalErrorInfo {
|
|
100
|
+
const category = categorizeError(error);
|
|
101
|
+
|
|
102
|
+
if (error instanceof Error) {
|
|
103
|
+
return {
|
|
104
|
+
type: category.type,
|
|
105
|
+
messageKey: `fal.errors.${category.messageKey}`,
|
|
106
|
+
retryable: category.retryable,
|
|
107
|
+
originalError: error.message,
|
|
108
|
+
originalErrorName: error.name,
|
|
109
|
+
stack: error.stack,
|
|
110
|
+
statusCode: extractStatusCode(error.message),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const errorString = String(error);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
type: category.type,
|
|
118
|
+
messageKey: `fal.errors.${category.messageKey}`,
|
|
119
|
+
retryable: category.retryable,
|
|
120
|
+
originalError: errorString,
|
|
121
|
+
statusCode: extractStatusCode(errorString),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse FAL error and return user-friendly message
|
|
127
|
+
*/
|
|
128
|
+
export function parseFalError(error: unknown): string {
|
|
129
|
+
const userMessage = parseFalApiError(error);
|
|
130
|
+
if (!userMessage || userMessage.trim().length === 0) {
|
|
131
|
+
return "An unknown error occurred. Please try again.";
|
|
132
|
+
}
|
|
133
|
+
return userMessage;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Categorize FAL error
|
|
138
|
+
*/
|
|
139
|
+
export function categorizeFalError(error: unknown): FalErrorCategory {
|
|
140
|
+
return categorizeError(error);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if FAL error is retryable
|
|
145
|
+
*/
|
|
146
|
+
export function isFalErrorRetryable(error: unknown): boolean {
|
|
147
|
+
return categorizeFalError(error).retryable;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract status code from error
|
|
152
|
+
*/
|
|
153
|
+
export { extractStatusCode };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAL Generation State Manager
|
|
3
|
+
* Manages state and refs for FAL generation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FalJobInput, FalLogEntry, FalQueueStatus } from "../../domain/entities/fal.types";
|
|
7
|
+
|
|
8
|
+
export interface GenerationState<T> {
|
|
9
|
+
data: T | null;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
isCancelling: boolean;
|
|
13
|
+
requestId: string | null;
|
|
14
|
+
lastRequest: { endpoint: string; input: FalJobInput } | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GenerationStateOptions<T> {
|
|
18
|
+
onQueueUpdate?: (status: FalQueueStatus) => void;
|
|
19
|
+
onProgress?: (status: FalQueueStatus) => void;
|
|
20
|
+
onError?: (error: Error) => void;
|
|
21
|
+
onResult?: (result: T) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class FalGenerationStateManager<T> {
|
|
25
|
+
private isMounted = true;
|
|
26
|
+
private currentRequestId: string | null = null;
|
|
27
|
+
private lastRequest: { endpoint: string; input: FalJobInput } | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private options?: GenerationStateOptions<T>
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
setIsMounted(mounted: boolean): void {
|
|
34
|
+
this.isMounted = mounted;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
checkMounted(): boolean {
|
|
38
|
+
return this.isMounted;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setCurrentRequestId(requestId: string | null): void {
|
|
42
|
+
this.currentRequestId = requestId;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getCurrentRequestId(): string | null {
|
|
46
|
+
return this.currentRequestId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setLastRequest(endpoint: string, input: FalJobInput): void {
|
|
50
|
+
this.lastRequest = { endpoint, input };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getLastRequest(): { endpoint: string; input: FalJobInput } | null {
|
|
54
|
+
return this.lastRequest;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clearLastRequest(): void {
|
|
58
|
+
this.lastRequest = null;
|
|
59
|
+
this.currentRequestId = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handleQueueUpdate(status: FalQueueStatus): void {
|
|
63
|
+
if (!this.isMounted) return;
|
|
64
|
+
|
|
65
|
+
if (status.requestId) {
|
|
66
|
+
this.currentRequestId = status.requestId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedStatus: FalQueueStatus = {
|
|
70
|
+
status: status.status,
|
|
71
|
+
requestId: status.requestId ?? this.currentRequestId ?? "",
|
|
72
|
+
logs: status.logs?.map((log: FalLogEntry) => ({
|
|
73
|
+
message: log.message,
|
|
74
|
+
level: log.level,
|
|
75
|
+
timestamp: log.timestamp,
|
|
76
|
+
})),
|
|
77
|
+
queuePosition: status.queuePosition,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
this.options?.onQueueUpdate?.(normalizedStatus);
|
|
81
|
+
this.options?.onProgress?.(normalizedStatus);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
handleResult(result: T): void {
|
|
85
|
+
if (!this.isMounted) return;
|
|
86
|
+
this.options?.onResult?.(result);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
handleError(error: Error): void {
|
|
90
|
+
if (!this.isMounted) return;
|
|
91
|
+
this.options?.onError?.(error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -17,21 +17,21 @@ export async function uploadToFalStorage(base64: string): Promise<string> {
|
|
|
17
17
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
|
|
18
18
|
const tempUri = (await base64ToTempFile(base64));
|
|
19
19
|
|
|
20
|
+
if (!tempUri) {
|
|
21
|
+
throw new Error("Failed to create temporary file from base64 data");
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
try {
|
|
21
25
|
const response = await fetch(tempUri);
|
|
22
26
|
const blob = await response.blob();
|
|
23
27
|
const url = await fal.storage.upload(blob);
|
|
24
28
|
return url;
|
|
25
29
|
} finally {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Log cleanup failure but don't throw
|
|
32
|
-
// eslint-disable-next-line no-console
|
|
33
|
-
console.warn(`Failed to cleanup temp file ${tempUri}:`, cleanupError);
|
|
34
|
-
}
|
|
30
|
+
try {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
32
|
+
await deleteTempFile(tempUri);
|
|
33
|
+
} catch {
|
|
34
|
+
// Silently ignore cleanup errors
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -1,208 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Formatting Utilities
|
|
3
3
|
* Common formatting functions for display and data presentation
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*/
|
|
33
|
-
export function formatBytes(bytes: number, decimals: number = 2): string {
|
|
34
|
-
if (bytes === 0) return "0 Bytes";
|
|
35
|
-
|
|
36
|
-
const k = 1024;
|
|
37
|
-
const dm = decimals < 0 ? 0 : decimals;
|
|
38
|
-
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
|
39
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
40
|
-
|
|
41
|
-
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Format duration in milliseconds to human-readable string
|
|
46
|
-
*/
|
|
47
|
-
export function formatDuration(milliseconds: number): string {
|
|
48
|
-
if (milliseconds < 1000) {
|
|
49
|
-
return `${milliseconds}ms`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const seconds = Math.floor(milliseconds / 1000);
|
|
53
|
-
if (seconds < 60) {
|
|
54
|
-
return `${seconds}s`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const minutes = Math.floor(seconds / 60);
|
|
58
|
-
const remainingSeconds = seconds % 60;
|
|
59
|
-
if (minutes < 60) {
|
|
60
|
-
return remainingSeconds > 0
|
|
61
|
-
? `${minutes}m ${remainingSeconds}s`
|
|
62
|
-
: `${minutes}m`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const hours = Math.floor(minutes / 60);
|
|
66
|
-
const remainingMinutes = minutes % 60;
|
|
67
|
-
return remainingMinutes > 0
|
|
68
|
-
? `${hours}h ${remainingMinutes}m`
|
|
69
|
-
: `${hours}h`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Format percentage
|
|
74
|
-
*/
|
|
75
|
-
export function formatPercentage(value: number, decimals: number = 1): string {
|
|
76
|
-
if (value < 0) return "0%";
|
|
77
|
-
if (value > 100) return "100%";
|
|
78
|
-
return `${formatNumber(value, decimals)}%`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Format date to locale string
|
|
83
|
-
*/
|
|
84
|
-
export function formatDate(date: Date | string, locale: string = "en-US"): string {
|
|
85
|
-
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
86
|
-
return dateObj.toLocaleDateString(locale, {
|
|
87
|
-
year: "numeric",
|
|
88
|
-
month: "short",
|
|
89
|
-
day: "numeric",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Format date and time to locale string
|
|
95
|
-
*/
|
|
96
|
-
export function formatDateTime(date: Date | string, locale: string = "en-US"): string {
|
|
97
|
-
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
98
|
-
return dateObj.toLocaleString(locale, {
|
|
99
|
-
year: "numeric",
|
|
100
|
-
month: "short",
|
|
101
|
-
day: "numeric",
|
|
102
|
-
hour: "2-digit",
|
|
103
|
-
minute: "2-digit",
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Format relative time (e.g., "2 hours ago")
|
|
109
|
-
*/
|
|
110
|
-
export function formatRelativeTime(date: Date | string, locale: string = "en-US"): string {
|
|
111
|
-
const dateObj = typeof date === "string" ? new Date(date) : date;
|
|
112
|
-
const now = new Date();
|
|
113
|
-
const diffMs = now.getTime() - dateObj.getTime();
|
|
114
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
115
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
116
|
-
const diffHour = Math.floor(diffMin / 60);
|
|
117
|
-
const diffDay = Math.floor(diffHour / 24);
|
|
118
|
-
|
|
119
|
-
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
120
|
-
|
|
121
|
-
if (diffSec < 60) {
|
|
122
|
-
return rtf.format(-diffSec, "second");
|
|
123
|
-
}
|
|
124
|
-
if (diffMin < 60) {
|
|
125
|
-
return rtf.format(-diffMin, "minute");
|
|
126
|
-
}
|
|
127
|
-
if (diffHour < 24) {
|
|
128
|
-
return rtf.format(-diffHour, "hour");
|
|
129
|
-
}
|
|
130
|
-
if (diffDay < 30) {
|
|
131
|
-
return rtf.format(-diffDay, "day");
|
|
132
|
-
}
|
|
133
|
-
if (diffDay < 365) {
|
|
134
|
-
const months = Math.floor(diffDay / 30);
|
|
135
|
-
return rtf.format(-months, "month");
|
|
136
|
-
}
|
|
137
|
-
const years = Math.floor(diffDay / 365);
|
|
138
|
-
return rtf.format(-years, "year");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Truncate text with ellipsis
|
|
143
|
-
*/
|
|
144
|
-
export function truncateText(text: string, maxLength: number): string {
|
|
145
|
-
if (text.length <= maxLength) {
|
|
146
|
-
return text;
|
|
147
|
-
}
|
|
148
|
-
return text.slice(0, maxLength - 3) + "...";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Capitalize first letter of string
|
|
153
|
-
*/
|
|
154
|
-
export function capitalize(text: string): string {
|
|
155
|
-
if (!text) return text;
|
|
156
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Convert string to title case
|
|
161
|
-
*/
|
|
162
|
-
export function toTitleCase(text: string): string {
|
|
163
|
-
return text
|
|
164
|
-
.toLowerCase()
|
|
165
|
-
.split(" ")
|
|
166
|
-
.map((word) => capitalize(word))
|
|
167
|
-
.join(" ");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Convert string to slug
|
|
172
|
-
*/
|
|
173
|
-
export function toSlug(text: string): string {
|
|
174
|
-
return text
|
|
175
|
-
.toLowerCase()
|
|
176
|
-
.trim()
|
|
177
|
-
.replace(/[^\w\s-]/g, "")
|
|
178
|
-
.replace(/[\s_-]+/g, "-")
|
|
179
|
-
.replace(/^-+|-+$/g, "");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Format list of items with conjunction
|
|
184
|
-
*/
|
|
185
|
-
export function formatList(items: readonly string[], conjunction: string = "and"): string {
|
|
186
|
-
if (items.length === 0) return "";
|
|
187
|
-
if (items.length === 1) return items[0] ?? "";
|
|
188
|
-
if (items.length === 2) return items.join(` ${conjunction} `);
|
|
189
|
-
|
|
190
|
-
const allButLast = items.slice(0, -1);
|
|
191
|
-
const last = items[items.length - 1];
|
|
192
|
-
return `${allButLast.join(", ")}, ${conjunction} ${last}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Pluralize word based on count
|
|
197
|
-
*/
|
|
198
|
-
export function pluralize(word: string, count: number): string {
|
|
199
|
-
if (count === 1) return word;
|
|
200
|
-
return `${word}s`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Format count with plural word
|
|
205
|
-
*/
|
|
206
|
-
export function formatCount(word: string, count: number): string {
|
|
207
|
-
return `${count} ${pluralize(word, count)}`;
|
|
208
|
-
}
|
|
4
|
+
*
|
|
5
|
+
* This module re-exports formatting utilities from specialized modules
|
|
6
|
+
* for better organization and maintainability.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
formatDate,
|
|
11
|
+
formatDateTime,
|
|
12
|
+
formatRelativeTime,
|
|
13
|
+
} from "./date-format.util";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
formatNumber,
|
|
17
|
+
formatCurrency,
|
|
18
|
+
formatBytes,
|
|
19
|
+
formatDuration,
|
|
20
|
+
formatPercentage,
|
|
21
|
+
} from "./number-format.util";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
truncateText,
|
|
25
|
+
capitalize,
|
|
26
|
+
toTitleCase,
|
|
27
|
+
toSlug,
|
|
28
|
+
formatList,
|
|
29
|
+
pluralize,
|
|
30
|
+
formatCount,
|
|
31
|
+
} from "./string-format.util";
|
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
mapFalError,
|
|
40
40
|
parseFalError,
|
|
41
41
|
isFalErrorRetryable,
|
|
42
|
+
extractStatusCode,
|
|
42
43
|
} from "./error-mapper";
|
|
43
44
|
|
|
44
45
|
export {
|
|
@@ -151,3 +152,9 @@ export {
|
|
|
151
152
|
type ValidationError,
|
|
152
153
|
} from "./input-validator.util";
|
|
153
154
|
|
|
155
|
+
// FAL generation state manager
|
|
156
|
+
export {
|
|
157
|
+
FalGenerationStateManager,
|
|
158
|
+
type GenerationState,
|
|
159
|
+
type GenerationStateOptions,
|
|
160
|
+
} from "./fal-generation-state-manager.util";
|
|
@@ -29,7 +29,7 @@ export async function preprocessInput(
|
|
|
29
29
|
input: Record<string, unknown>,
|
|
30
30
|
): Promise<Record<string, unknown>> {
|
|
31
31
|
const result = { ...input };
|
|
32
|
-
const uploadPromises: Promise<
|
|
32
|
+
const uploadPromises: Promise<unknown>[] = [];
|
|
33
33
|
|
|
34
34
|
// Handle individual image URL keys
|
|
35
35
|
for (const key of IMAGE_URL_KEYS) {
|
|
@@ -50,7 +50,7 @@ export async function preprocessInput(
|
|
|
50
50
|
// Handle image_urls array (for multi-person generation)
|
|
51
51
|
if (Array.isArray(result.image_urls) && result.image_urls.length > 0) {
|
|
52
52
|
const imageUrls = result.image_urls as unknown[];
|
|
53
|
-
const
|
|
53
|
+
const uploadTasks: Array<{ index: number; url: string | Promise<string> }> = [];
|
|
54
54
|
const errors: string[] = [];
|
|
55
55
|
|
|
56
56
|
for (let i = 0; i < imageUrls.length; i++) {
|
|
@@ -62,19 +62,14 @@ export async function preprocessInput(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (isBase64DataUri(imageUrl)) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
errors.push(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
72
|
-
throw new Error(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
uploadPromises.push(uploadPromise);
|
|
65
|
+
const uploadPromise = uploadToFalStorage(imageUrl).catch((error) => {
|
|
66
|
+
const errorMessage = `Failed to upload image_urls[${i}]: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
67
|
+
errors.push(errorMessage);
|
|
68
|
+
throw new Error(errorMessage);
|
|
69
|
+
});
|
|
70
|
+
uploadTasks.push({ index: i, url: uploadPromise });
|
|
76
71
|
} else if (typeof imageUrl === "string") {
|
|
77
|
-
|
|
72
|
+
uploadTasks.push({ index: i, url: imageUrl });
|
|
78
73
|
} else {
|
|
79
74
|
errors.push(`image_urls[${i}] has invalid type: ${typeof imageUrl}`);
|
|
80
75
|
}
|
|
@@ -84,6 +79,13 @@ export async function preprocessInput(
|
|
|
84
79
|
throw new Error(`Image URL validation failed:\n${errors.join('\n')}`);
|
|
85
80
|
}
|
|
86
81
|
|
|
82
|
+
// Wait for all uploads and build the final array without sparse elements
|
|
83
|
+
const processedUrls = await Promise.all(
|
|
84
|
+
uploadTasks
|
|
85
|
+
.sort((a, b) => a.index - b.index)
|
|
86
|
+
.map((task) => Promise.resolve(task.url))
|
|
87
|
+
);
|
|
88
|
+
|
|
87
89
|
result.image_urls = processedUrls;
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number Formatting Utilities
|
|
3
|
+
* Functions for formatting numbers and quantities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format number with decimal places
|
|
8
|
+
*/
|
|
9
|
+
export function formatNumber(value: number, decimals: number = 2): string {
|
|
10
|
+
if (Number.isNaN(value) || !Number.isFinite(value)) {
|
|
11
|
+
return "0";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Return integer if no decimal part
|
|
15
|
+
if (value % 1 === 0) {
|
|
16
|
+
return value.toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value.toFixed(decimals);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format currency amount
|
|
24
|
+
*/
|
|
25
|
+
export function formatCurrency(amount: number, currency: string = "USD"): string {
|
|
26
|
+
const formatted = formatNumber(amount, 2);
|
|
27
|
+
return `${currency} ${formatted}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format bytes to human-readable size
|
|
32
|
+
*/
|
|
33
|
+
export function formatBytes(bytes: number, decimals: number = 2): string {
|
|
34
|
+
if (bytes === 0) return "0 Bytes";
|
|
35
|
+
|
|
36
|
+
const k = 1024;
|
|
37
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
38
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
|
39
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
40
|
+
|
|
41
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format duration in milliseconds to human-readable string
|
|
46
|
+
*/
|
|
47
|
+
export function formatDuration(milliseconds: number): string {
|
|
48
|
+
if (milliseconds < 1000) {
|
|
49
|
+
return `${milliseconds}ms`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
53
|
+
if (seconds < 60) {
|
|
54
|
+
return `${seconds}s`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const minutes = Math.floor(seconds / 60);
|
|
58
|
+
const remainingSeconds = seconds % 60;
|
|
59
|
+
if (minutes < 60) {
|
|
60
|
+
return remainingSeconds > 0
|
|
61
|
+
? `${minutes}m ${remainingSeconds}s`
|
|
62
|
+
: `${minutes}m`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hours = Math.floor(minutes / 60);
|
|
66
|
+
const remainingMinutes = minutes % 60;
|
|
67
|
+
return remainingMinutes > 0
|
|
68
|
+
? `${hours}h ${remainingMinutes}m`
|
|
69
|
+
: `${hours}h`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format percentage
|
|
74
|
+
*/
|
|
75
|
+
export function formatPercentage(value: number, decimals: number = 1): string {
|
|
76
|
+
if (value < 0) return "0%";
|
|
77
|
+
if (value > 100) return "100%";
|
|
78
|
+
return `${formatNumber(value, decimals)}%`;
|
|
79
|
+
}
|