@umituz/react-native-ai-fal-provider 2.0.16 → 2.0.18

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
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",
@@ -55,5 +55,5 @@ export interface TextToVideoOptions {
55
55
  }
56
56
 
57
57
  export interface FaceSwapOptions {
58
- // No additional options
58
+ readonly enhanceFaces?: boolean;
59
59
  }
@@ -93,6 +93,7 @@ export interface SubscribeOptions<T = unknown> {
93
93
 
94
94
  export interface RunOptions {
95
95
  onProgress?: (progress: ProviderProgressInfo) => void;
96
+ signal?: AbortSignal;
96
97
  }
97
98
 
98
99
  // =============================================================================
@@ -14,7 +14,6 @@ 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,
@@ -36,7 +36,10 @@ export function buildVideoFeatureInput(
36
36
  const effectivePrompt = prompt || DEFAULT_VIDEO_PROMPTS[feature] || "Generate video";
37
37
 
38
38
  if (isImageRequiredFeature(feature)) {
39
- return buildVideoFromImageInput(sourceImageBase64 || "", {
39
+ if (!sourceImageBase64 || sourceImageBase64.trim().length === 0) {
40
+ throw new Error(`${feature} requires a source image`);
41
+ }
42
+ return buildVideoFromImageInput(sourceImageBase64, {
40
43
  prompt: effectivePrompt,
41
44
  duration: options?.duration as number | undefined,
42
45
  resolution: options?.resolution as string | undefined,
@@ -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
- ...update as unknown as FalQueueStatus,
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
- // Clean up abort listener to prevent memory leak (only if it was added)
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
  }
@@ -21,8 +21,6 @@ import * as queueOps from "./fal-queue-operations";
21
21
  import * as featureModels from "./fal-feature-models";
22
22
  import { validateInput } from "../utils/input-validator.util";
23
23
 
24
- declare const __DEV__: boolean | undefined;
25
-
26
24
  export class FalProvider implements IAIProvider {
27
25
  readonly providerId = "fal";
28
26
  readonly providerName = "FAL AI";
@@ -46,9 +44,6 @@ export class FalProvider implements IAIProvider {
46
44
  },
47
45
  });
48
46
  this.initialized = true;
49
- if (typeof __DEV__ !== "undefined" && __DEV__) {
50
- console.log("[FalProvider] Initialized");
51
- }
52
47
  }
53
48
 
54
49
  enableCostTracking(config?: CostTrackerConfig): void {
@@ -116,9 +111,6 @@ export class FalProvider implements IAIProvider {
116
111
 
117
112
  const existing = getExistingRequest<T>(key);
118
113
  if (existing) {
119
- if (typeof __DEV__ !== "undefined" && __DEV__) {
120
- console.log(`[FalProvider] Dedup: returning existing promise for ${model}`);
121
- }
122
114
  return existing.promise;
123
115
  }
124
116
 
@@ -142,6 +134,11 @@ export class FalProvider implements IAIProvider {
142
134
  validateInput(model, input);
143
135
  const processedInput = await preprocessInput(input);
144
136
 
137
+ const signal = options?.signal;
138
+ if (signal?.aborted) {
139
+ throw new Error("Request cancelled by user");
140
+ }
141
+
145
142
  return executeWithCostTracking({
146
143
  tracker: this.costTracker,
147
144
  model,
@@ -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
- // Fallback to default status if validation fails
43
- return {
44
- status: "IN_PROGRESS",
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?.map((log: FalLogEntry) => ({
28
- message: log.message,
29
- level: log.level ?? "info",
30
- timestamp: log.timestamp ?? new Date().toISOString(),
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 counter (guarantees uniqueness)
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 (faster than crypto for dedup check)
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
- // Add counter to guarantee uniqueness even with hash collisions
41
- const uniqueId = `${requestCounter++}`;
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, key) => {
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 && typeof __DEV__ !== "undefined" && __DEV__) {
85
- console.warn(`[RequestStore] Large request store size: ${store.size}. Consider investigating potential leaks.`);
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
- if (typeof __DEV__ !== "undefined" && __DEV__) {
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
- if (typeof __DEV__ !== "undefined" && __DEV__) {
41
- console.warn("[CostTracking] Failed:", costError);
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
- // Silently ignore cost tracking errors on failure path
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
- const response = await fetch(tempUri);
36
- const blob = await response.blob();
37
- const url = await fal.storage.upload(blob);
38
-
39
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
40
- await deleteTempFile(tempUri);
41
-
42
- if (typeof __DEV__ !== "undefined" && __DEV__) {
43
- console.log("[FalStorage] Upload complete", { url: url.slice(0, 60) + "..." });
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
  /**
@@ -36,11 +36,12 @@ export function buildPhotoRestoreInput(
36
36
  export function buildFaceSwapInput(
37
37
  sourceBase64: string,
38
38
  targetBase64: string,
39
- _options?: FaceSwapOptions,
39
+ options?: FaceSwapOptions,
40
40
  ): Record<string, unknown> {
41
41
  return {
42
42
  base_image_url: formatImageDataUri(sourceBase64),
43
43
  swap_image_url: formatImageDataUri(targetBase64),
44
+ ...(options?.enhanceFaces !== undefined && { enhance_faces: options.enhanceFaces }),
44
45
  };
45
46
  }
46
47
 
@@ -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";
@@ -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
- // Pre-initialize array with correct length to avoid sparse array
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 && typeof value !== "string") {
79
- errors.push({
80
- field,
81
- message: `${field} must be a string`,
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 typically follow the pattern: "owner/model-name" or "owner/model/version"
96
- const modelIdPattern = /^[a-z0-9-]+\/[a-z0-9-]+(\/[a-z0-9.]+)?$/;
97
- return modelIdPattern.test(value);
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
  */
@@ -92,30 +90,18 @@ export function createAiProviderInitModule(
92
90
  const apiKey = getApiKey();
93
91
 
94
92
  if (!apiKey) {
95
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
96
- console.log('[createAiProviderInitModule] No API key - skipping');
97
- }
98
- return Promise.resolve(true); // Not an error, just skip
93
+ return Promise.resolve(false);
99
94
  }
100
95
 
101
- // Initialize FAL provider
102
96
  falProvider.initialize({
103
97
  apiKey,
104
98
  videoFeatureModels,
105
99
  imageFeatureModels,
106
100
  });
107
101
 
108
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
109
- console.log('[createAiProviderInitModule] FAL provider initialized');
110
- }
111
-
112
102
  return Promise.resolve(true);
113
103
  } catch (error) {
114
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
115
- console.error('[createAiProviderInitModule] Error:', error);
116
- }
117
- // Continue on error - AI provider is not critical
118
- return Promise.resolve(true);
104
+ return Promise.resolve(false);
119
105
  }
120
106
  },
121
107
  };
@@ -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
- declare const __DEV__: boolean;
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],