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

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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/src/domain/constants/default-models.constants.ts +8 -2
  3. package/src/domain/types/provider.types.ts +0 -2
  4. package/src/exports/infrastructure.ts +0 -9
  5. package/src/infrastructure/services/fal-provider-subscription.ts +6 -1
  6. package/src/infrastructure/services/fal-provider.constants.ts +2 -11
  7. package/src/infrastructure/services/fal-provider.ts +21 -27
  8. package/src/infrastructure/services/request-store.ts +5 -9
  9. package/src/infrastructure/utils/cost-tracker.ts +7 -6
  10. package/src/infrastructure/utils/fal-storage.util.ts +4 -2
  11. package/src/infrastructure/utils/image-helpers.util.ts +11 -1
  12. package/src/infrastructure/utils/index.ts +0 -9
  13. package/src/infrastructure/utils/input-builders.util.ts +0 -2
  14. package/src/infrastructure/utils/input-preprocessor.util.ts +13 -1
  15. package/src/infrastructure/utils/job-metadata/job-metadata-queries.util.ts +16 -2
  16. package/src/infrastructure/utils/prompt-helpers.util.ts +8 -2
  17. package/src/infrastructure/utils/type-guards.util.ts +5 -6
  18. package/src/init/createAiProviderInitModule.ts +0 -20
  19. package/src/presentation/hooks/use-fal-generation.ts +22 -3
  20. package/src/presentation/hooks/use-models.ts +3 -0
  21. package/src/infrastructure/builders/image-feature-builder.ts +0 -64
  22. package/src/infrastructure/builders/video-feature-builder.ts +0 -55
  23. package/src/infrastructure/services/fal-feature-models.ts +0 -48
  24. package/src/infrastructure/utils/image-feature-builders.util.ts +0 -104
  25. 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.18",
3
+ "version": "2.0.20",
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",
@@ -80,10 +80,16 @@ export function getDefaultModelsByType(type: FalModelType): FalModelConfig[] {
80
80
  return DEFAULT_IMAGE_TO_VIDEO_MODELS;
81
81
  case "text-to-text":
82
82
  return DEFAULT_TEXT_TO_TEXT_MODELS;
83
- case "image-to-image":
83
+ case "image-to-image": {
84
+ // eslint-disable-next-line no-console
85
+ console.warn('Model type "image-to-image" not supported yet');
84
86
  return [];
85
- default:
87
+ }
88
+ default: {
89
+ // eslint-disable-next-line no-console
90
+ console.warn('Unknown model type:', type);
86
91
  return [];
92
+ }
87
93
  }
88
94
  }
89
95
 
@@ -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
  // =============================================================================
@@ -18,15 +18,6 @@ export {
18
18
  isFalErrorRetryable,
19
19
  buildSingleImageInput,
20
20
  buildDualImageInput,
21
- buildUpscaleInput,
22
- buildPhotoRestoreInput,
23
- buildVideoFromImageInput,
24
- buildFaceSwapInput,
25
- buildImageToImageInput,
26
- buildRemoveBackgroundInput,
27
- buildRemoveObjectInput,
28
- buildReplaceBackgroundInput,
29
- buildHDTouchUpInput,
30
21
  } from "../infrastructure/utils";
31
22
 
32
23
  export { CostTracker } from "../infrastructure/utils/cost-tracker";
@@ -123,9 +123,14 @@ export async function handleFalSubscription<T = unknown>(
123
123
  }
124
124
 
125
125
  const userMessage = parseFalError(error);
126
+ if (!userMessage || userMessage.trim().length === 0) {
127
+ throw new Error("An unknown error occurred. Please try again.");
128
+ }
126
129
  throw new Error(userMessage);
127
130
  } finally {
128
- if (timeoutId) clearTimeout(timeoutId);
131
+ if (timeoutId) {
132
+ clearTimeout(timeoutId);
133
+ }
129
134
  if (listenerAdded && abortHandler && signal) {
130
135
  signal.removeEventListener("abort", abortHandler);
131
136
  }
@@ -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
- "upscale",
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, ImageFeatureInputData,
10
- VideoFeatureInputData, ProviderCapabilities,
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,7 +18,6 @@ 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
23
  export class FalProvider implements IAIProvider {
@@ -28,13 +27,9 @@ export class FalProvider implements IAIProvider {
28
27
  private apiKey: string | null = null;
29
28
  private initialized = false;
30
29
  private costTracker: CostTracker | null = null;
31
- private videoFeatureModels: Record<string, string> = {};
32
- private imageFeatureModels: Record<string, string> = {};
33
30
 
34
31
  initialize(config: AIProviderConfig): void {
35
32
  this.apiKey = config.apiKey;
36
- this.videoFeatureModels = config.videoFeatureModels ?? {};
37
- this.imageFeatureModels = config.imageFeatureModels ?? {};
38
33
  fal.config({
39
34
  credentials: config.apiKey,
40
35
  retry: {
@@ -70,10 +65,24 @@ export class FalProvider implements IAIProvider {
70
65
  return FAL_CAPABILITIES;
71
66
  }
72
67
 
73
- isFeatureSupported(feature: ImageFeatureType | VideoFeatureType): boolean {
74
- const caps = this.getCapabilities();
75
- return caps.imageFeatures.includes(feature as ImageFeatureType) ||
76
- caps.videoFeatures.includes(feature as VideoFeatureType);
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.");
77
86
  }
78
87
 
79
88
  private validateInit(): void {
@@ -125,6 +134,7 @@ export class FalProvider implements IAIProvider {
125
134
  getRequestId: (res) => res.requestId ?? undefined,
126
135
  }).then((res) => res.result).finally(() => removeRequest(key));
127
136
 
137
+ // Store promise immediately to prevent race condition
128
138
  storeRequest(key, { promise, abortController });
129
139
  return promise;
130
140
  }
@@ -160,22 +170,6 @@ export class FalProvider implements IAIProvider {
160
170
  hasRunningRequest(): boolean {
161
171
  return hasActiveRequests();
162
172
  }
163
-
164
- getImageFeatureModel(feature: ImageFeatureType): string {
165
- return featureModels.getImageFeatureModel(this.imageFeatureModels, feature);
166
- }
167
-
168
- buildImageFeatureInput(feature: ImageFeatureType, data: ImageFeatureInputData): Record<string, unknown> {
169
- return featureModels.buildImageFeatureInput(feature, data);
170
- }
171
-
172
- getVideoFeatureModel(feature: VideoFeatureType): string {
173
- return featureModels.getVideoFeatureModel(this.videoFeatureModels, feature);
174
- }
175
-
176
- buildVideoFeatureInput(feature: VideoFeatureType, data: VideoFeatureInputData): Record<string, unknown> {
177
- return featureModels.buildVideoFeatureInput(feature, data);
178
- }
179
173
  }
180
174
 
181
175
  export const falProvider = new FalProvider();
@@ -19,10 +19,8 @@ export function getRequestStore(): RequestStore {
19
19
  }
20
20
 
21
21
  /**
22
- * Create a collision-resistant request key using combination of:
23
- * - Model name
24
- * - Input hash (for quick comparison)
25
- * - Unique ID (guarantees uniqueness)
22
+ * Create a deterministic request key using model and input hash
23
+ * Same model + input will always produce the same key for deduplication
26
24
  */
27
25
  export function createRequestKey(model: string, input: Record<string, unknown>): string {
28
26
  const inputStr = JSON.stringify(input, Object.keys(input).sort());
@@ -32,11 +30,9 @@ export function createRequestKey(model: string, input: Record<string, unknown>):
32
30
  const char = inputStr.charCodeAt(i);
33
31
  hash = ((hash << 5) - hash + char) | 0;
34
32
  }
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)}`;
39
- return `${model}:${hash.toString(36)}:${uniqueId}`;
33
+ // Return deterministic key without unique ID
34
+ // This allows proper deduplication: same model + input = same key
35
+ return `${model}:${hash.toString(36)}`;
40
36
  }
41
37
 
42
38
  export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined {
@@ -45,7 +45,6 @@ export class CostTracker {
45
45
  private config: Required<CostTrackerConfig>;
46
46
  private costHistory: GenerationCost[] = [];
47
47
  private currentOperationCosts: Map<string, number> = new Map();
48
- private operationCounter = 0;
49
48
 
50
49
  constructor(config?: CostTrackerConfig) {
51
50
  this.config = {
@@ -84,11 +83,14 @@ export class CostTracker {
84
83
  }
85
84
 
86
85
  startOperation(modelId: string, operation: string): string {
87
- // Use counter + timestamp for guaranteed unique operation IDs
88
- const operationId = `${Date.now()}-${this.operationCounter++}-${operation}`;
86
+ // Use crypto.randomUUID() for guaranteed uniqueness without overflow
87
+ const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
88
+ ? crypto.randomUUID()
89
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}-${operation}`;
90
+
89
91
  const estimatedCost = this.calculateEstimatedCost(modelId);
90
92
 
91
- this.currentOperationCosts.set(operationId, estimatedCost);
93
+ this.currentOperationCosts.set(uniqueId, estimatedCost);
92
94
 
93
95
  if (this.config.trackEstimatedCost) {
94
96
  const cost: GenerationCost = {
@@ -104,7 +106,7 @@ export class CostTracker {
104
106
  this.config.onCostUpdate(cost);
105
107
  }
106
108
 
107
- return operationId;
109
+ return uniqueId;
108
110
  }
109
111
 
110
112
  completeOperation(
@@ -156,7 +158,6 @@ export class CostTracker {
156
158
  clearHistory(): void {
157
159
  this.costHistory = [];
158
160
  this.currentOperationCosts.clear();
159
- this.operationCounter = 0;
160
161
  }
161
162
 
162
163
  getCostsByModel(modelId: string): GenerationCost[] {
@@ -27,8 +27,10 @@ export async function uploadToFalStorage(base64: string): Promise<string> {
27
27
  try {
28
28
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
29
29
  await deleteTempFile(tempUri);
30
- } catch {
31
- // Silently ignore cleanup failures
30
+ } catch (cleanupError) {
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);
32
34
  }
33
35
  }
34
36
  }
@@ -20,8 +20,18 @@ export function extractBase64(dataUri: string): string {
20
20
  if (!dataUri.startsWith("data:")) {
21
21
  return dataUri;
22
22
  }
23
+
23
24
  const parts = dataUri.split(",");
24
- return parts.length > 1 ? parts[1] : dataUri;
25
+ if (parts.length < 2) {
26
+ throw new Error(`Invalid data URI format: ${dataUri}`);
27
+ }
28
+
29
+ const base64Part = parts[1];
30
+ if (!base64Part || base64Part.length === 0) {
31
+ throw new Error(`Empty base64 data in URI: ${dataUri}`);
32
+ }
33
+
34
+ return base64Part;
25
35
  }
26
36
 
27
37
  /**
@@ -11,15 +11,6 @@ export {
11
11
  export {
12
12
  buildSingleImageInput,
13
13
  buildDualImageInput,
14
- buildUpscaleInput,
15
- buildPhotoRestoreInput,
16
- buildVideoFromImageInput,
17
- buildFaceSwapInput,
18
- buildImageToImageInput,
19
- buildRemoveBackgroundInput,
20
- buildRemoveObjectInput,
21
- buildReplaceBackgroundInput,
22
- buildHDTouchUpInput,
23
14
  } from "./input-builders.util";
24
15
 
25
16
  export {
@@ -4,5 +4,3 @@
4
4
  */
5
5
 
6
6
  export * from "./base-builders.util";
7
- export * from "./image-feature-builders.util";
8
- export * from "./video-feature-builders.util";
@@ -51,9 +51,16 @@ export async function preprocessInput(
51
51
  if (Array.isArray(result.image_urls) && result.image_urls.length > 0) {
52
52
  const imageUrls = result.image_urls as unknown[];
53
53
  const processedUrls: string[] = [];
54
+ const errors: string[] = [];
54
55
 
55
56
  for (let i = 0; i < imageUrls.length; i++) {
56
57
  const imageUrl = imageUrls[i];
58
+
59
+ if (!imageUrl) {
60
+ errors.push(`image_urls[${i}] is null or undefined`);
61
+ continue;
62
+ }
63
+
57
64
  if (isBase64DataUri(imageUrl)) {
58
65
  const index = i;
59
66
  const uploadPromise = uploadToFalStorage(imageUrl)
@@ -61,6 +68,7 @@ export async function preprocessInput(
61
68
  processedUrls[index] = url;
62
69
  })
63
70
  .catch((error) => {
71
+ errors.push(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
64
72
  throw new Error(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
65
73
  });
66
74
 
@@ -68,10 +76,14 @@ export async function preprocessInput(
68
76
  } else if (typeof imageUrl === "string") {
69
77
  processedUrls[i] = imageUrl;
70
78
  } else {
71
- processedUrls[i] = "";
79
+ errors.push(`image_urls[${i}] has invalid type: ${typeof imageUrl}`);
72
80
  }
73
81
  }
74
82
 
83
+ if (errors.length > 0) {
84
+ throw new Error(`Image URL validation failed:\n${errors.join('\n')}`);
85
+ }
86
+
75
87
  result.image_urls = processedUrls;
76
88
  }
77
89
 
@@ -18,8 +18,22 @@ export function serializeJobMetadata(metadata: FalJobMetadata): string {
18
18
  */
19
19
  export function deserializeJobMetadata(data: string): FalJobMetadata | null {
20
20
  try {
21
- return JSON.parse(data) as FalJobMetadata;
22
- } catch {
21
+ const parsed = JSON.parse(data) as Record<string, unknown>;
22
+ // Validate structure
23
+ if (!parsed || typeof parsed !== 'object') {
24
+ // eslint-disable-next-line no-console
25
+ console.warn('Invalid job metadata: not an object', data);
26
+ return null;
27
+ }
28
+ if (!parsed.requestId || !parsed.model || !parsed.status) {
29
+ // eslint-disable-next-line no-console
30
+ console.warn('Invalid job metadata: missing required fields', data);
31
+ return null;
32
+ }
33
+ return parsed as unknown as FalJobMetadata;
34
+ } catch (error) {
35
+ // eslint-disable-next-line no-console
36
+ console.error('Failed to deserialize job metadata:', error, 'Data:', data);
23
37
  return null;
24
38
  }
25
39
  }
@@ -14,8 +14,14 @@ export function truncatePrompt(prompt: string, maxLength: number = 5000): string
14
14
  }
15
15
 
16
16
  /**
17
- * Sanitize prompt by removing excessive whitespace
17
+ * Sanitize prompt by removing excessive whitespace and control characters
18
18
  */
19
19
  export function sanitizePrompt(prompt: string): string {
20
- return prompt.trim().replace(/\s+/g, " ");
20
+ return prompt
21
+ .trim()
22
+ .replace(/\s+/g, " ")
23
+ // Remove control characters except tab, newline, carriage return
24
+ // eslint-disable-next-line no-control-regex
25
+ .replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '')
26
+ .slice(0, 5000);
21
27
  }
@@ -87,15 +87,14 @@ export function isValidApiKey(value: unknown): boolean {
87
87
  /**
88
88
  * Validate model ID format
89
89
  */
90
+ const MODEL_ID_PATTERN = /^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+(\/[a-zA-Z0-9-_.]+)?$/;
91
+
90
92
  export function isValidModelId(value: unknown): boolean {
91
93
  if (typeof value !== "string") {
92
94
  return false;
93
95
  }
94
96
 
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;
97
+ return MODEL_ID_PATTERN.test(value) && value.length >= 3;
99
98
  }
100
99
 
101
100
  /**
@@ -109,12 +108,12 @@ export function isValidPrompt(value: unknown): boolean {
109
108
  * Validate timeout value
110
109
  */
111
110
  export function isValidTimeout(value: unknown): boolean {
112
- return typeof value === "number" && value > 0 && value <= 600000; // Max 10 minutes
111
+ return typeof value === "number" && !isNaN(value) && isFinite(value) && value > 0 && value <= 600000; // Max 10 minutes
113
112
  }
114
113
 
115
114
  /**
116
115
  * Validate retry count
117
116
  */
118
117
  export function isValidRetryCount(value: unknown): boolean {
119
- return typeof value === "number" && value >= 0 && value <= 10;
118
+ return typeof value === "number" && !isNaN(value) && isFinite(value) && Number.isInteger(value) && value >= 0 && value <= 10;
120
119
  }
@@ -22,18 +22,6 @@ export interface AiProviderInitModuleConfig {
22
22
  */
23
23
  getApiKey: () => string | undefined;
24
24
 
25
- /**
26
- * Video feature models mapping
27
- * Maps feature types to FAL model IDs
28
- */
29
- videoFeatureModels?: Record<string, string>;
30
-
31
- /**
32
- * Image feature models mapping
33
- * Maps feature types to FAL model IDs
34
- */
35
- imageFeatureModels?: Record<string, string>;
36
-
37
25
  /**
38
26
  * Whether this module is critical for app startup
39
27
  * @default false
@@ -61,10 +49,6 @@ export interface AiProviderInitModuleConfig {
61
49
  * createFirebaseInitModule(),
62
50
  * createAiProviderInitModule({
63
51
  * getApiKey: () => getFalApiKey(),
64
- * videoFeatureModels: {
65
- * "image-to-video": "fal-ai/wan-25-preview/image-to-video",
66
- * "text-to-video": "fal-ai/wan-25-preview/text-to-video",
67
- * },
68
52
  * }),
69
53
  * ],
70
54
  * });
@@ -75,8 +59,6 @@ export function createAiProviderInitModule(
75
59
  ): InitModule {
76
60
  const {
77
61
  getApiKey,
78
- videoFeatureModels,
79
- imageFeatureModels,
80
62
  critical = false,
81
63
  dependsOn = ['firebase'],
82
64
  } = config;
@@ -95,8 +77,6 @@ export function createAiProviderInitModule(
95
77
 
96
78
  falProvider.initialize({
97
79
  apiKey,
98
- videoFeatureModels,
99
- imageFeatureModels,
100
80
  });
101
81
 
102
82
  return Promise.resolve(true);
@@ -3,7 +3,7 @@
3
3
  * React hook for FAL AI generation operations
4
4
  */
5
5
 
6
- import { useState, useCallback, useRef } from "react";
6
+ import { useState, useCallback, useRef, useEffect } from "react";
7
7
  import { falProvider } from "../../infrastructure/services/fal-provider";
8
8
  import { mapFalError } from "../../infrastructure/utils/error-mapper";
9
9
  import type { FalJobInput, FalQueueStatus, FalLogEntry } from "../../domain/entities/fal.types";
@@ -38,9 +38,23 @@ export function useFalGeneration<T = unknown>(
38
38
 
39
39
  const lastRequestRef = useRef<{ endpoint: string; input: FalJobInput } | null>(null);
40
40
  const currentRequestIdRef = useRef<string | null>(null);
41
+ const isMountedRef = useRef(true);
42
+
43
+ // Cleanup on unmount
44
+ useEffect(() => {
45
+ isMountedRef.current = true;
46
+ return () => {
47
+ isMountedRef.current = false;
48
+ if (falProvider.hasRunningRequest()) {
49
+ falProvider.cancelCurrentRequest();
50
+ }
51
+ };
52
+ }, []);
41
53
 
42
54
  const generate = useCallback(
43
55
  async (modelEndpoint: string, input: FalJobInput): Promise<T | null> => {
56
+ if (!isMountedRef.current) return null;
57
+
44
58
  lastRequestRef.current = { endpoint: modelEndpoint, input };
45
59
  setIsLoading(true);
46
60
  setError(null);
@@ -52,6 +66,7 @@ export function useFalGeneration<T = unknown>(
52
66
  const result = await falProvider.subscribe<T>(modelEndpoint, input, {
53
67
  timeoutMs: options?.timeoutMs,
54
68
  onQueueUpdate: (status) => {
69
+ if (!isMountedRef.current) return;
55
70
  if (status.requestId) {
56
71
  currentRequestIdRef.current = status.requestId;
57
72
  }
@@ -68,16 +83,20 @@ export function useFalGeneration<T = unknown>(
68
83
  },
69
84
  });
70
85
 
86
+ if (!isMountedRef.current) return null;
71
87
  setData(result);
72
88
  return result;
73
89
  } catch (err) {
90
+ if (!isMountedRef.current) return null;
74
91
  const errorInfo = mapFalError(err);
75
92
  setError(errorInfo);
76
93
  options?.onError?.(errorInfo);
77
94
  return null;
78
95
  } finally {
79
- setIsLoading(false);
80
- setIsCancelling(false);
96
+ if (isMountedRef.current) {
97
+ setIsLoading(false);
98
+ setIsCancelling(false);
99
+ }
81
100
  }
82
101
  },
83
102
  [options]
@@ -68,6 +68,9 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
68
68
  const model = models.find((m) => m.id === modelId);
69
69
  if (model) {
70
70
  setSelectedModel(model);
71
+ } else {
72
+ // eslint-disable-next-line no-console
73
+ console.warn(`Model not found: ${modelId}. Available models:`, models.map(m => m.id));
71
74
  }
72
75
  },
73
76
  [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,55 +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
- if (!sourceImageBase64 || sourceImageBase64.trim().length === 0) {
40
- throw new Error(`${feature} requires a source image`);
41
- }
42
- return buildVideoFromImageInput(sourceImageBase64, {
43
- prompt: effectivePrompt,
44
- duration: options?.duration as number | undefined,
45
- resolution: options?.resolution as string | undefined,
46
- });
47
- }
48
-
49
- return buildTextToVideoInput({
50
- prompt: effectivePrompt,
51
- duration: options?.duration as number | undefined,
52
- aspectRatio: options?.aspect_ratio as string | undefined,
53
- resolution: options?.resolution as string | undefined,
54
- });
55
- }
@@ -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,104 +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
- ...(options?.enhanceFaces !== undefined && { enhance_faces: options.enhanceFaces }),
45
- };
46
- }
47
-
48
- export function buildRemoveBackgroundInput(
49
- base64: string,
50
- options?: RemoveBackgroundOptions & {
51
- model?: string;
52
- operating_resolution?: string;
53
- output_format?: string;
54
- refine_foreground?: boolean;
55
- },
56
- ): Record<string, unknown> {
57
- return buildSingleImageInput(base64, {
58
- model: options?.model ?? "General Use (Light)",
59
- operating_resolution: options?.operating_resolution ?? "1024x1024",
60
- output_format: options?.output_format ?? "png",
61
- refine_foreground: options?.refine_foreground ?? true,
62
- });
63
- }
64
-
65
- export function buildRemoveObjectInput(
66
- base64: string,
67
- options?: RemoveObjectOptions,
68
- ): Record<string, unknown> {
69
- return buildSingleImageInput(base64, {
70
- mask_url: options?.mask,
71
- prompt: options?.prompt || "Remove the object and fill with background",
72
- });
73
- }
74
-
75
- export function buildReplaceBackgroundInput(
76
- base64: string,
77
- options: ReplaceBackgroundOptions,
78
- ): Record<string, unknown> {
79
- return buildSingleImageInput(base64, {
80
- prompt: options.prompt,
81
- });
82
- }
83
-
84
- export function buildHDTouchUpInput(
85
- base64: string,
86
- options?: UpscaleOptions,
87
- ): Record<string, unknown> {
88
- return buildUpscaleInput(base64, options);
89
- }
90
-
91
- export interface KontextStyleTransferOptions {
92
- prompt: string;
93
- guidance_scale?: number;
94
- }
95
-
96
- export function buildKontextStyleTransferInput(
97
- base64: string,
98
- options: KontextStyleTransferOptions,
99
- ): Record<string, unknown> {
100
- return buildSingleImageInput(base64, {
101
- prompt: options.prompt,
102
- guidance_scale: options.guidance_scale ?? 3.5,
103
- });
104
- }
@@ -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
- }