@umituz/react-native-ai-generation-content 1.82.5 → 1.82.7

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-generation-content",
3
- "version": "1.82.5",
3
+ "version": "1.82.7",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -5,7 +5,7 @@
5
5
 
6
6
  export const DEFAULT_MODELS = {
7
7
  TEXT_TO_IMAGE: "xai/grok-imagine-image",
8
- TEXT_TO_VIDEO: "xai/grok-imagine-video/text-to-video",
9
- IMAGE_TO_VIDEO: "xai/grok-imagine-video/image-to-video",
8
+ TEXT_TO_VIDEO: "fal-ai/ltx-video",
9
+ IMAGE_TO_VIDEO: "fal-ai/ltx-video",
10
10
  SCENARIO_VIDEO: "fal-ai/ltx-video",
11
11
  } as const;
@@ -41,6 +41,9 @@ export const CREATION_FIELDS = {
41
41
  IS_SHARED: "isShared" as const,
42
42
  RATING: "rating" as const,
43
43
 
44
+ // Completion timestamp
45
+ COMPLETED_AT: "completedAt" as const,
46
+
44
47
  // AI provider metadata
45
48
  REQUEST_ID: "requestId" as const,
46
49
  MODEL: "model" as const,
@@ -69,6 +72,7 @@ export const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
69
72
  CREATION_FIELDS.REQUEST_ID,
70
73
  CREATION_FIELDS.MODEL,
71
74
  CREATION_FIELDS.PROMPT,
75
+ CREATION_FIELDS.COMPLETED_AT,
72
76
  ] as const;
73
77
 
74
78
  /**
@@ -38,6 +38,8 @@ export interface Creation {
38
38
  // Background job tracking
39
39
  readonly requestId?: string;
40
40
  readonly model?: string;
41
+ // Timestamps
42
+ readonly completedAt?: Date;
41
43
  // Soft delete - if set, the creation is considered deleted
42
44
  readonly deletedAt?: Date;
43
45
  }
@@ -61,6 +63,7 @@ export interface CreationDocument {
61
63
  readonly createdAt: FirebaseTimestamp | Date;
62
64
  readonly completedAt?: FirebaseTimestamp | Date | null;
63
65
  readonly deletedAt?: FirebaseTimestamp | Date | null;
66
+ readonly updatedAt?: FirebaseTimestamp | Date | null;
64
67
  // Background job tracking
65
68
  readonly requestId?: string;
66
69
  readonly model?: string;
@@ -105,6 +108,13 @@ export function mapDocumentToCreation(
105
108
  deletedAtDate = data.deletedAt.toDate();
106
109
  }
107
110
 
111
+ let completedAtDate: Date | undefined;
112
+ if (data.completedAt instanceof Date) {
113
+ completedAtDate = data.completedAt;
114
+ } else if (data.completedAt && typeof data.completedAt === "object" && "toDate" in data.completedAt && typeof data.completedAt.toDate === "function") {
115
+ completedAtDate = data.completedAt.toDate();
116
+ }
117
+
108
118
  return {
109
119
  id,
110
120
  uri,
@@ -121,6 +131,7 @@ export function mapDocumentToCreation(
121
131
  output: data.output ?? undefined,
122
132
  requestId: data.requestId,
123
133
  model: data.model,
134
+ completedAt: completedAtDate,
124
135
  deletedAt: deletedAtDate,
125
136
  };
126
137
  }
@@ -85,7 +85,8 @@ export function useProcessingJobsPoller(
85
85
  try {
86
86
  await repository.update(userId, creation.id, {
87
87
  status: CREATION_STATUS.FAILED,
88
- metadata: { error: "Generation timed out" },
88
+ metadata: { ...creation.metadata, error: "Generation timed out" },
89
+ completedAt: new Date(),
89
90
  });
90
91
  } catch (e) {
91
92
  if (typeof __DEV__ !== "undefined" && __DEV__) {
@@ -129,7 +130,8 @@ export function useProcessingJobsPoller(
129
130
  }
130
131
  await repository.update(userId, creation.id, {
131
132
  status: CREATION_STATUS.FAILED,
132
- metadata: { error: "No valid result URL received" },
133
+ metadata: { ...creation.metadata, error: "No valid result URL received" },
134
+ completedAt: new Date(),
133
135
  });
134
136
  return;
135
137
  }
@@ -143,6 +145,7 @@ export function useProcessingJobsPoller(
143
145
  status: CREATION_STATUS.COMPLETED,
144
146
  uri,
145
147
  output,
148
+ completedAt: new Date(),
146
149
  });
147
150
  } else if (status.status === QUEUE_STATUS.FAILED) {
148
151
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Failed:", creation.id);
@@ -151,7 +154,8 @@ export function useProcessingJobsPoller(
151
154
 
152
155
  await repository.update(userId, creation.id, {
153
156
  status: CREATION_STATUS.FAILED,
154
- metadata: { error: "Generation failed" },
157
+ metadata: { ...creation.metadata, error: "Generation failed" },
158
+ completedAt: new Date(),
155
159
  });
156
160
  }
157
161
  } catch (error) {
@@ -16,6 +16,9 @@ export interface ProcessingCreationData {
16
16
  readonly duration?: number;
17
17
  readonly resolution?: string;
18
18
  readonly creditCost?: number;
19
+ readonly aspectRatio?: string;
20
+ readonly provider?: string;
21
+ readonly outputType?: string;
19
22
  }
20
23
 
21
24
  export interface CompletedCreationData {
@@ -36,6 +36,10 @@ export async function saveAsProcessing(
36
36
  ...(data.duration && { duration: data.duration }),
37
37
  ...(data.resolution && { resolution: data.resolution }),
38
38
  ...(data.creditCost && { creditCost: data.creditCost }),
39
+ ...(data.aspectRatio && { aspectRatio: data.aspectRatio }),
40
+ ...(data.provider && { provider: data.provider }),
41
+ ...(data.outputType && { outputType: data.outputType }),
42
+ startedAt: new Date().toISOString(),
39
43
  },
40
44
  });
41
45
 
@@ -26,7 +26,8 @@ export async function updateToCompleted(
26
26
  uri: data.uri,
27
27
  status: "completed" as const,
28
28
  output,
29
- });
29
+ completedAt: new Date(),
30
+ } as Partial<import("../../../../creations/domain/entities/Creation").Creation>);
30
31
 
31
32
  if (typeof __DEV__ !== "undefined" && __DEV__) {
32
33
  console.log("[CreationPersistence] Updated to completed", { creationId });
@@ -45,7 +46,8 @@ export async function updateToFailed(
45
46
  await repository.update(userId, creationId, {
46
47
  status: "failed" as const,
47
48
  metadata: { error },
48
- });
49
+ completedAt: new Date(),
50
+ } as Partial<import("../../../../creations/domain/entities/Creation").Creation>);
49
51
 
50
52
  if (typeof __DEV__ !== "undefined" && __DEV__) {
51
53
  console.log("[CreationPersistence] Updated to failed", { creationId, error });
@@ -25,13 +25,16 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
25
25
  const consecutiveErrorsRef = useRef(0);
26
26
  const [isGenerating, setIsGenerating] = useState(false);
27
27
 
28
+ const clearPolling = useCallback(() => {
29
+ if (pollingRef.current) {
30
+ clearInterval(pollingRef.current);
31
+ pollingRef.current = null;
32
+ }
33
+ }, []);
34
+
28
35
  useEffect(() => {
29
36
  return () => {
30
- if (pollingRef.current) {
31
- clearInterval(pollingRef.current);
32
- pollingRef.current = null;
33
- }
34
- // Reset all refs on unmount
37
+ clearPolling();
35
38
  isGeneratingRef.current = false;
36
39
  isPollingRef.current = false;
37
40
  consecutiveErrorsRef.current = 0;
@@ -40,9 +43,10 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
40
43
  modelRef.current = null;
41
44
  setIsGenerating(false);
42
45
  };
43
- }, []);
46
+ }, [clearPolling]);
44
47
 
45
48
  const resetRefs = useCallback(() => {
49
+ clearPolling();
46
50
  creationIdRef.current = null;
47
51
  requestIdRef.current = null;
48
52
  modelRef.current = null;
@@ -50,10 +54,13 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
50
54
  isPollingRef.current = false;
51
55
  consecutiveErrorsRef.current = 0;
52
56
  setIsGenerating(false);
53
- }, []);
57
+ }, [clearPolling]);
54
58
 
55
59
  const handleComplete = useCallback(
56
60
  async (urls: GenerationUrls) => {
61
+ // Stop polling immediately on completion
62
+ clearPolling();
63
+
57
64
  const creationId = creationIdRef.current;
58
65
  const uri = (urls.videoUrl || urls.imageUrl) ?? "";
59
66
 
@@ -72,9 +79,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
72
79
  if (typeof __DEV__ !== "undefined" && __DEV__) {
73
80
  console.error("[VideoQueue] ❌ Invalid completion data:", { creationId, userId, uri });
74
81
  }
82
+ resetRefs();
83
+ onError?.("Invalid completion data - no valid URL received");
75
84
  return;
76
85
  }
77
86
 
87
+ let persistenceSucceeded = true;
78
88
  if (creationId && userId) {
79
89
  try {
80
90
  await persistence.updateToCompleted(userId, creationId, {
@@ -87,27 +97,32 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
87
97
  console.log("[VideoQueue] ✅ Updated completion status in Firestore");
88
98
  }
89
99
  } catch (error) {
100
+ persistenceSucceeded = false;
90
101
  if (typeof __DEV__ !== "undefined" && __DEV__) {
91
102
  console.error("[VideoQueue] ❌ Failed to update completion status:", error);
92
103
  }
93
104
  }
94
105
  }
95
106
 
107
+ resetRefs();
108
+
109
+ // Still call onSuccess even if persistence failed - the generation itself succeeded
110
+ // The video/image URL is valid, user should still see the result
96
111
  if (typeof __DEV__ !== "undefined" && __DEV__) {
97
- console.log("[VideoQueue] 🎯 Calling onSuccess callback now...");
112
+ console.log("[VideoQueue] 🎯 Calling onSuccess callback now...", { persistenceSucceeded });
98
113
  }
99
- resetRefs();
100
114
  onSuccess?.(urls);
101
115
 
102
116
  if (typeof __DEV__ !== "undefined" && __DEV__) {
103
117
  console.log("[VideoQueue] ✅ onSuccess callback completed");
104
118
  }
105
119
  },
106
- [userId, persistence, onSuccess, resetRefs],
120
+ [userId, persistence, onSuccess, onError, resetRefs, clearPolling],
107
121
  );
108
122
 
109
123
  const handleError = useCallback(
110
124
  async (errorMsg: string) => {
125
+ clearPolling();
111
126
  const creationId = creationIdRef.current;
112
127
  if (creationId && userId) {
113
128
  try {
@@ -121,7 +136,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
121
136
  resetRefs();
122
137
  onError?.(errorMsg);
123
138
  },
124
- [userId, persistence, onError, resetRefs],
139
+ [userId, persistence, onError, resetRefs, clearPolling],
125
140
  );
126
141
 
127
142
  const pollStatus = useCallback(async () => {
@@ -129,15 +144,21 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
129
144
  const model = modelRef.current;
130
145
  if (!requestId || !model) return;
131
146
 
132
- await pollQueueStatus({
133
- requestId,
134
- model,
135
- isPollingRef,
136
- pollingRef,
137
- consecutiveErrorsRef,
138
- onComplete: handleComplete,
139
- onError: handleError,
140
- });
147
+ try {
148
+ await pollQueueStatus({
149
+ requestId,
150
+ model,
151
+ isPollingRef,
152
+ pollingRef,
153
+ consecutiveErrorsRef,
154
+ onComplete: handleComplete,
155
+ onError: handleError,
156
+ });
157
+ } catch (error) {
158
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
159
+ console.error("[VideoQueue] Unexpected poll error:", error);
160
+ }
161
+ }
141
162
  }, [handleComplete, handleError]);
142
163
 
143
164
  const startGeneration = useCallback(
@@ -158,6 +179,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
158
179
  const inputData = input as Record<string, unknown>;
159
180
  const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
160
181
  const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
182
+ const aspectRatio = typeof inputData?.aspectRatio === "string" ? inputData.aspectRatio : undefined;
161
183
 
162
184
  creationId = await persistence.saveAsProcessing(userId, {
163
185
  scenarioId: scenario.id,
@@ -166,20 +188,43 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
166
188
  duration,
167
189
  resolution,
168
190
  creditCost,
191
+ aspectRatio,
192
+ provider: "fal",
193
+ outputType: scenario.outputType,
169
194
  });
170
195
  creationIdRef.current = creationId;
171
196
  } catch (error) {
172
197
  if (typeof __DEV__ !== "undefined" && __DEV__) {
173
198
  console.error("[VideoQueue] Failed to save processing creation:", error);
174
199
  }
200
+ // Continue without creation tracking - generation can still proceed
201
+ // The video will be generated but won't appear in gallery history
202
+ }
203
+ }
204
+
205
+ let queueResult;
206
+ try {
207
+ queueResult = await strategy.submitToQueue(input);
208
+ } catch (error) {
209
+ // Queue submission threw - reset state and report error
210
+ if (creationId && userId) {
211
+ try {
212
+ await persistence.updateToFailed(userId, creationId, error instanceof Error ? error.message : "Queue submission failed");
213
+ } catch { /* best effort */ }
175
214
  }
215
+ isGeneratingRef.current = false;
216
+ setIsGenerating(false);
217
+ onError?.(error instanceof Error ? error.message : "Queue submission failed");
218
+ return;
176
219
  }
177
220
 
178
- const queueResult = await strategy.submitToQueue(input);
179
221
  if (!queueResult.success || !queueResult.requestId || !queueResult.model) {
180
222
  if (creationId && userId) {
181
- await persistence.updateToFailed(userId, creationId, queueResult.error || "Queue submission failed");
223
+ try {
224
+ await persistence.updateToFailed(userId, creationId, queueResult.error || "Queue submission failed");
225
+ } catch { /* best effort */ }
182
226
  }
227
+ isGeneratingRef.current = false;
183
228
  setIsGenerating(false);
184
229
  onError?.(queueResult.error || "Queue submission failed");
185
230
  return;
@@ -201,7 +246,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
201
246
  pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
202
247
  void pollStatus();
203
248
  },
204
- [userId, scenario, persistence, strategy, pollStatus, onError],
249
+ [userId, scenario, persistence, strategy, creditCost, pollStatus, onError],
205
250
  );
206
251
 
207
252
  return { isGenerating, startGeneration };