@umituz/react-native-ai-generation-content 1.65.7 → 1.65.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/access-control/hooks/useAIFeatureGate.ts +1 -1
- package/src/domains/background/infrastructure/services/job-poller.service.ts +81 -3
- package/src/domains/background/infrastructure/utils/polling-interval.util.ts +1 -4
- package/src/domains/background/presentation/hooks/use-background-generation.ts +13 -3
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +1 -1
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +0 -1
- package/src/domains/creations/infrastructure/repositories/creation-delete.operations.ts +65 -6
- package/src/domains/creations/presentation/hooks/creation-validators.ts +0 -1
- package/src/domains/generation/infrastructure/flow/use-flow-store.types.ts +0 -1
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +5 -3
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +84 -0
- package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -7
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -2
- package/src/domains/image-to-video/domain/types/config.types.ts +3 -0
- package/src/domains/image-to-video/infrastructure/services/image-to-video-executor.ts +6 -3
- package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +10 -11
- package/src/domains/image-to-video/presentation/hooks/useFormState.ts +2 -2
- package/src/domains/image-to-video/presentation/hooks/useGeneration.ts +39 -6
- package/src/domains/image-to-video/presentation/hooks/useImageToVideoFeature.ts +19 -14
- package/src/domains/prompts/infrastructure/services/ImagePromptBuilder.ts +7 -1
- package/src/domains/text-to-image/presentation/hooks/useFormState.ts +1 -2
- package/src/domains/text-to-image/presentation/hooks/useGeneration.ts +2 -2
- package/src/domains/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +0 -1
- package/src/domains/text-to-video/infrastructure/services/text-to-video-executor.ts +2 -2
- package/src/infrastructure/executors/base-executor.ts +2 -2
- package/src/infrastructure/executors/index.ts +1 -1
- package/src/infrastructure/http/http-fetch-handler.ts +1 -1
- package/src/infrastructure/http/http-request-executor.ts +1 -1
- package/src/infrastructure/utils/domain-guards.ts +2 -1
- package/src/infrastructure/utils/index.ts +5 -1
- package/src/presentation/hooks/generation/orchestrator.ts +29 -2
- package/src/presentation/hooks/generation/useImageGeneration.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.65.
|
|
3
|
+
"version": "1.65.9",
|
|
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",
|
|
@@ -52,7 +52,7 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
|
|
|
52
52
|
hasSubscription: isPremium,
|
|
53
53
|
creditBalance,
|
|
54
54
|
requiredCredits: creditCost,
|
|
55
|
-
onShowPaywall: openPaywall,
|
|
55
|
+
onShowPaywall: () => openPaywall(),
|
|
56
56
|
isCreditsLoaded,
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -10,6 +10,56 @@ import { checkStatusForErrors, isJobComplete } from "../utils/status-checker.uti
|
|
|
10
10
|
import { validateResult } from "../utils/result-validator.util";
|
|
11
11
|
import type { PollJobOptions, PollJobResult } from "./job-poller.types";
|
|
12
12
|
|
|
13
|
+
declare const __DEV__: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a promise with abort signal support
|
|
17
|
+
* Rejects if signal is aborted before promise resolves
|
|
18
|
+
*/
|
|
19
|
+
function withAbortSignal<T>(
|
|
20
|
+
promise: Promise<T>,
|
|
21
|
+
signal: AbortSignal | undefined,
|
|
22
|
+
timeoutMs?: number,
|
|
23
|
+
): Promise<T> {
|
|
24
|
+
if (!signal && !timeoutMs) {
|
|
25
|
+
return promise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Promise<T>((resolve, reject) => {
|
|
29
|
+
// Handle abort signal
|
|
30
|
+
if (signal?.aborted) {
|
|
31
|
+
reject(new Error("Operation aborted"));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const abortHandler = () => {
|
|
36
|
+
reject(new Error("Operation aborted"));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
40
|
+
|
|
41
|
+
// Handle timeout
|
|
42
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
43
|
+
if (timeoutMs) {
|
|
44
|
+
timeoutId = setTimeout(() => {
|
|
45
|
+
reject(new Error(`Operation timeout after ${timeoutMs}ms`));
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
promise
|
|
50
|
+
.then((result) => {
|
|
51
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
52
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
53
|
+
resolve(result);
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
57
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
58
|
+
reject(error);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
13
63
|
/**
|
|
14
64
|
* Poll job until completion with exponential backoff
|
|
15
65
|
* Only reports 100% on actual completion
|
|
@@ -60,7 +110,12 @@ export async function pollJob<T = unknown>(
|
|
|
60
110
|
}
|
|
61
111
|
|
|
62
112
|
try {
|
|
63
|
-
|
|
113
|
+
// Wrap provider calls with abort signal support and timeout (30s default)
|
|
114
|
+
const status = await withAbortSignal(
|
|
115
|
+
provider.getJobStatus(model, requestId),
|
|
116
|
+
signal,
|
|
117
|
+
30000,
|
|
118
|
+
);
|
|
64
119
|
onStatusChange?.(status);
|
|
65
120
|
|
|
66
121
|
const statusCheck = checkStatusForErrors(status);
|
|
@@ -77,7 +132,12 @@ export async function pollJob<T = unknown>(
|
|
|
77
132
|
consecutiveTransientErrors = 0;
|
|
78
133
|
|
|
79
134
|
if (isJobComplete(status)) {
|
|
80
|
-
|
|
135
|
+
// Wrap result retrieval with abort signal support and timeout (60s for larger results)
|
|
136
|
+
const result = await withAbortSignal(
|
|
137
|
+
provider.getJobResult<T>(model, requestId),
|
|
138
|
+
signal,
|
|
139
|
+
60000,
|
|
140
|
+
);
|
|
81
141
|
|
|
82
142
|
const validation = validateResult(result);
|
|
83
143
|
if (!validation.isValid) {
|
|
@@ -98,11 +158,29 @@ export async function pollJob<T = unknown>(
|
|
|
98
158
|
elapsedMs: Date.now() - startTime,
|
|
99
159
|
};
|
|
100
160
|
}
|
|
101
|
-
} catch {
|
|
161
|
+
} catch (error) {
|
|
102
162
|
consecutiveTransientErrors++;
|
|
103
163
|
|
|
164
|
+
if (__DEV__) {
|
|
165
|
+
console.warn("[JobPoller] Transient error during polling", {
|
|
166
|
+
attempt: attempt + 1,
|
|
167
|
+
requestId,
|
|
168
|
+
model,
|
|
169
|
+
consecutiveErrors: consecutiveTransientErrors,
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
code: (error as { code?: string })?.code,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
104
175
|
// Check if we've hit max consecutive transient errors
|
|
105
176
|
if (maxConsecutiveErrors && consecutiveTransientErrors >= maxConsecutiveErrors) {
|
|
177
|
+
if (__DEV__) {
|
|
178
|
+
console.error("[JobPoller] Max consecutive errors reached", {
|
|
179
|
+
maxConsecutiveErrors,
|
|
180
|
+
requestId,
|
|
181
|
+
model,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
106
184
|
return {
|
|
107
185
|
success: false,
|
|
108
186
|
error: new Error(`Too many consecutive errors (${consecutiveTransientErrors})`),
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* Polling Interval Calculator
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_POLLING_CONFIG,
|
|
7
|
-
type PollingConfig,
|
|
8
|
-
} from "../../../../domain/entities/polling.types";
|
|
5
|
+
import type { PollingConfig } from "../../../../domain/entities/polling.types";
|
|
9
6
|
|
|
10
7
|
export interface IntervalOptions {
|
|
11
8
|
attempt: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useRef, useState, useMemo } from "react";
|
|
2
2
|
import { usePendingJobs } from "./use-pending-jobs";
|
|
3
3
|
import { executeDirectGeneration, executeQueuedJob } from "../../infrastructure/executors/backgroundJobExecutor";
|
|
4
4
|
import { DEFAULT_QUEUE_CONFIG } from "../../domain/entities/job.types";
|
|
@@ -85,13 +85,23 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
|
|
|
85
85
|
[removeJob],
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
+
// Calculate active jobs from TanStack Query state (not ref) for reactivity
|
|
89
|
+
// Active jobs are those currently processing or queued
|
|
90
|
+
const activeJobs = useMemo(
|
|
91
|
+
() => jobs.filter((job) => job.status === "processing" || job.status === "queued"),
|
|
92
|
+
[jobs]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const activeJobCount = activeJobs.length;
|
|
96
|
+
const hasActiveJobs = activeJobCount > 0;
|
|
97
|
+
|
|
88
98
|
return {
|
|
89
99
|
startJob,
|
|
90
100
|
executeDirectly,
|
|
91
101
|
cancelJob,
|
|
92
102
|
pendingJobs: jobs,
|
|
93
|
-
activeJobCount
|
|
94
|
-
hasActiveJobs
|
|
103
|
+
activeJobCount,
|
|
104
|
+
hasActiveJobs,
|
|
95
105
|
isProcessing,
|
|
96
106
|
progress,
|
|
97
107
|
};
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { updateDoc, deleteDoc } from "firebase/firestore";
|
|
6
6
|
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
7
7
|
|
|
8
|
+
declare const __DEV__: boolean;
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Soft deletes a creation
|
|
10
12
|
*/
|
|
@@ -14,12 +16,31 @@ export async function deleteCreation(
|
|
|
14
16
|
creationId: string
|
|
15
17
|
): Promise<boolean> {
|
|
16
18
|
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
17
|
-
if (!docRef)
|
|
19
|
+
if (!docRef) {
|
|
20
|
+
if (__DEV__) {
|
|
21
|
+
console.error("[CreationDelete] Cannot delete: Invalid document reference", {
|
|
22
|
+
userId,
|
|
23
|
+
creationId,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
try {
|
|
20
30
|
await updateDoc(docRef, { deletedAt: new Date() });
|
|
31
|
+
if (__DEV__) {
|
|
32
|
+
console.log("[CreationDelete] Soft deleted successfully", { userId, creationId });
|
|
33
|
+
}
|
|
21
34
|
return true;
|
|
22
|
-
} catch {
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (__DEV__) {
|
|
37
|
+
console.error("[CreationDelete] Soft delete failed", {
|
|
38
|
+
userId,
|
|
39
|
+
creationId,
|
|
40
|
+
error: error instanceof Error ? error.message : String(error),
|
|
41
|
+
code: (error as { code?: string })?.code,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
23
44
|
return false;
|
|
24
45
|
}
|
|
25
46
|
}
|
|
@@ -33,12 +54,31 @@ export async function hardDeleteCreation(
|
|
|
33
54
|
creationId: string
|
|
34
55
|
): Promise<boolean> {
|
|
35
56
|
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
36
|
-
if (!docRef)
|
|
57
|
+
if (!docRef) {
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.error("[CreationDelete] Cannot hard delete: Invalid document reference", {
|
|
60
|
+
userId,
|
|
61
|
+
creationId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
37
66
|
|
|
38
67
|
try {
|
|
39
68
|
await deleteDoc(docRef);
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.log("[CreationDelete] Hard deleted successfully", { userId, creationId });
|
|
71
|
+
}
|
|
40
72
|
return true;
|
|
41
|
-
} catch {
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (__DEV__) {
|
|
75
|
+
console.error("[CreationDelete] Hard delete failed", {
|
|
76
|
+
userId,
|
|
77
|
+
creationId,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
code: (error as { code?: string })?.code,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
42
82
|
return false;
|
|
43
83
|
}
|
|
44
84
|
}
|
|
@@ -52,12 +92,31 @@ export async function restoreCreation(
|
|
|
52
92
|
creationId: string
|
|
53
93
|
): Promise<boolean> {
|
|
54
94
|
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
55
|
-
if (!docRef)
|
|
95
|
+
if (!docRef) {
|
|
96
|
+
if (__DEV__) {
|
|
97
|
+
console.error("[CreationDelete] Cannot restore: Invalid document reference", {
|
|
98
|
+
userId,
|
|
99
|
+
creationId,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
56
104
|
|
|
57
105
|
try {
|
|
58
106
|
await updateDoc(docRef, { deletedAt: null });
|
|
107
|
+
if (__DEV__) {
|
|
108
|
+
console.log("[CreationDelete] Restored successfully", { userId, creationId });
|
|
109
|
+
}
|
|
59
110
|
return true;
|
|
60
|
-
} catch {
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (__DEV__) {
|
|
113
|
+
console.error("[CreationDelete] Restore failed", {
|
|
114
|
+
userId,
|
|
115
|
+
creationId,
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
code: (error as { code?: string })?.code,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
61
120
|
return false;
|
|
62
121
|
}
|
|
63
122
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { extractPrompt, extractDuration, extractAspectRatio, extractResolution }
|
|
|
15
15
|
import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
|
|
16
16
|
import { getVideoFeatureType } from "./video-generation.utils";
|
|
17
17
|
import type { WizardVideoInput, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
18
|
-
import { validatePhotoCount } from "./video-generation.types";
|
|
18
|
+
import { validatePhotoCount, validateWizardVideoInput } from "./video-generation.types";
|
|
19
19
|
|
|
20
20
|
declare const __DEV__: boolean;
|
|
21
21
|
|
|
@@ -76,7 +76,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
76
76
|
|
|
77
77
|
return {
|
|
78
78
|
execute: async (input: unknown) => {
|
|
79
|
-
|
|
79
|
+
// Runtime validation with descriptive errors
|
|
80
|
+
const videoInput = validateWizardVideoInput(input);
|
|
80
81
|
|
|
81
82
|
const result = await executeVideoFeature(videoFeatureType, {
|
|
82
83
|
sourceImageBase64: videoInput.sourceImageBase64,
|
|
@@ -97,7 +98,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
97
98
|
},
|
|
98
99
|
|
|
99
100
|
submitToQueue: async (input: unknown) => {
|
|
100
|
-
|
|
101
|
+
// Runtime validation with descriptive errors
|
|
102
|
+
const videoInput = validateWizardVideoInput(input);
|
|
101
103
|
|
|
102
104
|
const result = await submitVideoFeatureToQueue(videoFeatureType, {
|
|
103
105
|
sourceImageBase64: videoInput.sourceImageBase64,
|
|
@@ -59,3 +59,87 @@ export function validatePhotoCount(
|
|
|
59
59
|
|
|
60
60
|
return { isValid: true };
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Type guard for WizardVideoInput
|
|
65
|
+
* Validates runtime input and provides type safety
|
|
66
|
+
*/
|
|
67
|
+
export function isWizardVideoInput(input: unknown): input is WizardVideoInput {
|
|
68
|
+
if (!input || typeof input !== "object") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const obj = input as Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
// prompt is required
|
|
75
|
+
if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Optional fields validation
|
|
80
|
+
if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (obj.duration !== undefined && typeof obj.duration !== "number") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates and casts input to WizardVideoInput
|
|
105
|
+
* Throws descriptive error if validation fails
|
|
106
|
+
*/
|
|
107
|
+
export function validateWizardVideoInput(input: unknown): WizardVideoInput {
|
|
108
|
+
if (!isWizardVideoInput(input)) {
|
|
109
|
+
const errors: string[] = [];
|
|
110
|
+
|
|
111
|
+
if (!input || typeof input !== "object") {
|
|
112
|
+
throw new Error("Invalid input: expected object");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const obj = input as Record<string, unknown>;
|
|
116
|
+
|
|
117
|
+
if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
|
|
118
|
+
errors.push("prompt (string, required)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
|
|
122
|
+
errors.push("sourceImageBase64 (string, optional)");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
|
|
126
|
+
errors.push("targetImageBase64 (string, optional)");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (obj.duration !== undefined && typeof obj.duration !== "number") {
|
|
130
|
+
errors.push("duration (number, optional)");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
|
|
134
|
+
errors.push("aspectRatio (string, optional)");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
|
|
138
|
+
errors.push("resolution (string, optional)");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(`Invalid WizardVideoInput: ${errors.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return input;
|
|
145
|
+
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { buildWizardInput } from "../../infrastructure/strategies";
|
|
2
2
|
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
3
|
-
import type { Scenario } from "../../../../scenarios/domain/scenario.types";
|
|
4
3
|
import type { GenerationAction } from "./generationStateMachine";
|
|
5
4
|
|
|
6
5
|
declare const __DEV__: boolean;
|
|
7
6
|
|
|
8
7
|
interface ExecuteGenerationParams {
|
|
9
|
-
wizardData:
|
|
10
|
-
scenario:
|
|
8
|
+
wizardData: Record<string, unknown>;
|
|
9
|
+
scenario: WizardScenarioData;
|
|
11
10
|
isVideoMode: boolean;
|
|
12
11
|
isMountedRef: React.MutableRefObject<boolean>;
|
|
13
12
|
dispatch: React.Dispatch<GenerationAction>;
|
|
14
13
|
onError?: (error: string) => void;
|
|
15
|
-
videoGenerationFn: (input:
|
|
16
|
-
photoGenerationFn: (input:
|
|
14
|
+
videoGenerationFn: (input: unknown, prompt: string) => Promise<void>;
|
|
15
|
+
photoGenerationFn: (input: unknown, prompt: string) => Promise<void>;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export const executeWizardGeneration = async (params: ExecuteGenerationParams): Promise<void> => {
|
|
@@ -52,10 +51,10 @@ export const executeWizardGeneration = async (params: ExecuteGenerationParams):
|
|
|
52
51
|
if (isMountedRef.current) {
|
|
53
52
|
dispatch({ type: "COMPLETE" });
|
|
54
53
|
}
|
|
55
|
-
} catch (error:
|
|
54
|
+
} catch (error: unknown) {
|
|
56
55
|
if (!isMountedRef.current) return;
|
|
57
56
|
|
|
58
|
-
const errorMsg = error
|
|
57
|
+
const errorMsg = (error instanceof Error ? error.message : String(error)) || "error.generation.unknown";
|
|
59
58
|
if (__DEV__) {
|
|
60
59
|
console.error("[WizardGeneration] Error:", errorMsg, error);
|
|
61
60
|
}
|
|
@@ -46,7 +46,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
46
46
|
if (creationId && userId && (urls.videoUrl || urls.imageUrl)) {
|
|
47
47
|
try {
|
|
48
48
|
await persistence.updateToCompleted(userId, creationId, {
|
|
49
|
-
uri: urls.videoUrl || urls.imageUrl,
|
|
49
|
+
uri: (urls.videoUrl || urls.imageUrl) ?? "",
|
|
50
50
|
imageUrl: urls.imageUrl,
|
|
51
51
|
videoUrl: urls.videoUrl,
|
|
52
52
|
});
|
|
@@ -123,7 +123,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
123
123
|
requestIdRef.current = queueResult.requestId;
|
|
124
124
|
modelRef.current = queueResult.model;
|
|
125
125
|
|
|
126
|
-
if (creationId && userId) {
|
|
126
|
+
if (creationId && userId && queueResult.requestId && queueResult.model) {
|
|
127
127
|
try {
|
|
128
128
|
await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
|
|
129
129
|
} catch {}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ImageToVideoResult } from "./image-to-video.types";
|
|
7
7
|
import type { ImageToVideoFormState } from "./form.types";
|
|
8
|
+
import type { ImageToVideoGenerationStartData, ImageToVideoCreationData } from "./image-to-video-callbacks.types";
|
|
8
9
|
|
|
9
10
|
export interface ImageToVideoCallbacks {
|
|
10
11
|
onGenerate: (formState: ImageToVideoFormState) => Promise<void>;
|
|
@@ -13,6 +14,8 @@ export interface ImageToVideoCallbacks {
|
|
|
13
14
|
onShowPaywall?: (cost: number) => void;
|
|
14
15
|
onSuccess?: (result: ImageToVideoResult) => void;
|
|
15
16
|
onError?: (error: string) => void;
|
|
17
|
+
onGenerationStart?: (data: ImageToVideoGenerationStartData) => Promise<void>;
|
|
18
|
+
onCreationSave?: (data: ImageToVideoCreationData) => Promise<void>;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface ImageToVideoFormConfig {
|
|
@@ -14,6 +14,9 @@ import type { ImageToVideoRequest, ImageToVideoResult } from "../../domain/types
|
|
|
14
14
|
import { env } from "../../../../infrastructure/config/env.config";
|
|
15
15
|
import type { ExecuteImageToVideoOptions } from "./image-to-video-executor.types";
|
|
16
16
|
|
|
17
|
+
// Export types
|
|
18
|
+
export type { ExecuteImageToVideoOptions };
|
|
19
|
+
|
|
17
20
|
const STATUS_PROGRESS: Record<string, number> = {
|
|
18
21
|
queued: 10,
|
|
19
22
|
in_queue: 15,
|
|
@@ -41,17 +44,17 @@ class ImageToVideoExecutor extends BaseExecutor<
|
|
|
41
44
|
input: Record<string, unknown>,
|
|
42
45
|
onProgress?: (progress: number) => void,
|
|
43
46
|
): Promise<unknown> {
|
|
44
|
-
this.
|
|
47
|
+
this.log("info", "Starting provider.subscribe()...");
|
|
45
48
|
|
|
46
49
|
const result = await provider.subscribe(model, input, {
|
|
47
50
|
onQueueUpdate: (status: { status: string; queuePosition?: number }) => {
|
|
48
|
-
this.
|
|
51
|
+
this.log("info", `Queue: ${status.status}, pos: ${status.queuePosition}`);
|
|
49
52
|
onProgress?.(STATUS_PROGRESS[status.status.toLowerCase()] ?? 30);
|
|
50
53
|
},
|
|
51
54
|
timeoutMs: env.generationVideoTimeoutMs,
|
|
52
55
|
});
|
|
53
56
|
|
|
54
|
-
this.
|
|
57
|
+
this.log("info", `Complete, keys: ${result ? Object.keys(result as object) : "null"}`);
|
|
55
58
|
checkFalApiError(result);
|
|
56
59
|
return result;
|
|
57
60
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { executeImageToVideo } from "../../infrastructure/services";
|
|
2
2
|
import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
ImageToVideoFeatureConfig,
|
|
5
5
|
ImageToVideoCallbacks,
|
|
6
6
|
ImageToVideoResult,
|
|
7
7
|
ImageToVideoOptions,
|
|
@@ -17,8 +17,8 @@ interface VideoGenerationInput {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface CreateStrategyParams {
|
|
20
|
-
config:
|
|
21
|
-
callbacks
|
|
20
|
+
config: ImageToVideoFeatureConfig;
|
|
21
|
+
callbacks?: ImageToVideoCallbacks;
|
|
22
22
|
buildInput: ImageToVideoInputBuilder;
|
|
23
23
|
extractResult?: ImageToVideoResultExtractor;
|
|
24
24
|
userId: string;
|
|
@@ -30,22 +30,21 @@ interface CreateStrategyParams {
|
|
|
30
30
|
export const createImageToVideoStrategy = (
|
|
31
31
|
params: CreateStrategyParams,
|
|
32
32
|
): GenerationStrategy<VideoGenerationInput, ImageToVideoResult> => {
|
|
33
|
-
const { config, callbacks, buildInput, extractResult, userId,
|
|
33
|
+
const { config, callbacks, buildInput, extractResult, userId, creationIdRef, updateState } = params;
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
36
|
execute: async (input) => {
|
|
37
37
|
creationIdRef.current = input.creationId;
|
|
38
38
|
|
|
39
|
-
callbacks
|
|
39
|
+
callbacks?.onGenerationStart?.({
|
|
40
40
|
creationId: input.creationId,
|
|
41
41
|
type: "image-to-video",
|
|
42
|
-
|
|
43
|
-
prompt: input.prompt,
|
|
42
|
+
imageUri: input.imageUrl,
|
|
44
43
|
metadata: input.options as Record<string, unknown> | undefined,
|
|
45
44
|
}).catch(() => {});
|
|
46
45
|
|
|
47
46
|
const result = await executeImageToVideo(
|
|
48
|
-
{
|
|
47
|
+
{ imageUri: input.imageUrl, userId, motionPrompt: input.prompt, options: input.options },
|
|
49
48
|
{ model: config.model, buildInput, extractResult },
|
|
50
49
|
);
|
|
51
50
|
|
|
@@ -61,15 +60,15 @@ export const createImageToVideoStrategy = (
|
|
|
61
60
|
thumbnailUrl: result.thumbnailUrl,
|
|
62
61
|
};
|
|
63
62
|
},
|
|
64
|
-
getCreditCost: () => config.creditCost,
|
|
63
|
+
getCreditCost: () => config.creditCost ?? 0,
|
|
65
64
|
save: async (result) => {
|
|
66
65
|
if (result.success && result.videoUrl && creationIdRef.current) {
|
|
67
|
-
await callbacks
|
|
66
|
+
await callbacks?.onCreationSave?.({
|
|
68
67
|
creationId: creationIdRef.current,
|
|
69
68
|
type: "image-to-video",
|
|
70
69
|
videoUrl: result.videoUrl,
|
|
71
70
|
thumbnailUrl: result.thumbnailUrl,
|
|
72
|
-
|
|
71
|
+
imageUri: "",
|
|
73
72
|
});
|
|
74
73
|
}
|
|
75
74
|
},
|
|
@@ -24,8 +24,8 @@ export interface UseFormStateReturn {
|
|
|
24
24
|
function createInitialState(defaults: ImageToVideoFormDefaults): ImageToVideoFormState {
|
|
25
25
|
return {
|
|
26
26
|
selectedImages: [],
|
|
27
|
-
animationStyle: defaults.animationStyle,
|
|
28
|
-
duration: defaults.duration,
|
|
27
|
+
animationStyle: defaults.animationStyle ?? "none",
|
|
28
|
+
duration: defaults.duration ?? 3,
|
|
29
29
|
motionPrompt: "",
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generation Hook for Image-to-Video
|
|
3
|
-
* Manages generation state and execution
|
|
3
|
+
* Manages generation state and execution with abort support
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback, useMemo } from "react";
|
|
6
|
+
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
|
|
7
7
|
import type {
|
|
8
8
|
ImageToVideoFormState,
|
|
9
9
|
ImageToVideoGenerationState,
|
|
@@ -36,20 +36,46 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
|
|
|
36
36
|
INITIAL_GENERATION_STATE
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
40
|
+
const isMountedRef = useRef(true);
|
|
41
|
+
|
|
42
|
+
// Cleanup on unmount
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
isMountedRef.current = true;
|
|
45
|
+
return () => {
|
|
46
|
+
isMountedRef.current = false;
|
|
47
|
+
abortControllerRef.current?.abort();
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
// Stabilize callbacks to prevent unnecessary re-renders
|
|
52
|
+
const onErrorRef = useRef(callbacks.onError);
|
|
53
|
+
const onGenerateRef = useRef(callbacks.onGenerate);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
onErrorRef.current = callbacks.onError;
|
|
57
|
+
onGenerateRef.current = callbacks.onGenerate;
|
|
58
|
+
}, [callbacks.onError, callbacks.onGenerate]);
|
|
59
|
+
|
|
39
60
|
const setProgress = useCallback((progress: number) => {
|
|
61
|
+
if (!isMountedRef.current) return;
|
|
40
62
|
setGenerationState((prev) => ({ ...prev, progress }));
|
|
41
63
|
}, []);
|
|
42
64
|
|
|
43
65
|
const setError = useCallback((error: string | null) => {
|
|
66
|
+
if (!isMountedRef.current) return;
|
|
44
67
|
setGenerationState((prev) => ({ ...prev, error, isGenerating: false }));
|
|
45
68
|
}, []);
|
|
46
69
|
|
|
47
70
|
const handleGenerate = useCallback(async () => {
|
|
48
71
|
if (formState.selectedImages.length === 0) {
|
|
49
|
-
|
|
72
|
+
onErrorRef.current?.("No images selected");
|
|
50
73
|
return;
|
|
51
74
|
}
|
|
52
75
|
|
|
76
|
+
// Create new AbortController for this generation
|
|
77
|
+
abortControllerRef.current = new AbortController();
|
|
78
|
+
|
|
53
79
|
setGenerationState({
|
|
54
80
|
isGenerating: true,
|
|
55
81
|
progress: 0,
|
|
@@ -57,18 +83,25 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
|
|
|
57
83
|
});
|
|
58
84
|
|
|
59
85
|
try {
|
|
60
|
-
await
|
|
86
|
+
await onGenerateRef.current(formState);
|
|
87
|
+
|
|
88
|
+
if (!isMountedRef.current || abortControllerRef.current.signal.aborted) return;
|
|
89
|
+
|
|
61
90
|
setGenerationState((prev) => ({ ...prev, isGenerating: false, progress: 100 }));
|
|
62
91
|
} catch (error) {
|
|
92
|
+
if (!isMountedRef.current || abortControllerRef.current.signal.aborted) return;
|
|
93
|
+
|
|
63
94
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
95
|
setGenerationState({
|
|
65
96
|
isGenerating: false,
|
|
66
97
|
progress: 0,
|
|
67
98
|
error: errorMessage,
|
|
68
99
|
});
|
|
69
|
-
|
|
100
|
+
onErrorRef.current?.(errorMessage);
|
|
101
|
+
} finally {
|
|
102
|
+
abortControllerRef.current = null;
|
|
70
103
|
}
|
|
71
|
-
}, [formState
|
|
104
|
+
}, [formState]);
|
|
72
105
|
|
|
73
106
|
const isReady = useMemo(
|
|
74
107
|
() => formState.selectedImages.length > 0 && !generationState.isGenerating,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import { useGenerationOrchestrator } from "../../../../presentation/hooks/generation";
|
|
3
3
|
import { createImageToVideoStrategy } from "./imageToVideoStrategy";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
UseImageToVideoFeatureReturn,
|
|
4
|
+
import type { ImageToVideoCallbacks } from "../../domain/types";
|
|
5
|
+
import {
|
|
7
6
|
INITIAL_STATE,
|
|
8
7
|
DEFAULT_ALERT_MESSAGES,
|
|
8
|
+
type UseImageToVideoFeatureProps,
|
|
9
|
+
type UseImageToVideoFeatureReturn,
|
|
9
10
|
} from "./image-to-video-feature.types";
|
|
10
11
|
|
|
11
12
|
export type {
|
|
@@ -26,7 +27,7 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
|
|
|
26
27
|
() =>
|
|
27
28
|
createImageToVideoStrategy({
|
|
28
29
|
config,
|
|
29
|
-
callbacks,
|
|
30
|
+
callbacks: callbacks as ImageToVideoCallbacks | undefined,
|
|
30
31
|
buildInput: config.buildInput,
|
|
31
32
|
extractResult: config.extractResult,
|
|
32
33
|
userId,
|
|
@@ -40,14 +41,17 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
|
|
|
40
41
|
const orchestrator = useGenerationOrchestrator(strategy, {
|
|
41
42
|
userId,
|
|
42
43
|
alertMessages: DEFAULT_ALERT_MESSAGES,
|
|
43
|
-
onCreditsExhausted: () => callbacks
|
|
44
|
-
onSuccess: (result) => {
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
onCreditsExhausted: () => callbacks?.onShowPaywall?.(config.creditCost ?? 0),
|
|
45
|
+
onSuccess: (result: unknown) => {
|
|
46
|
+
const typedResult = result as { success: boolean; videoUrl?: string; thumbnailUrl?: string };
|
|
47
|
+
if (typedResult.success && typedResult.videoUrl) {
|
|
48
|
+
config.onProcessingComplete?.(typedResult);
|
|
49
|
+
callbacks?.onGenerate?.(typedResult);
|
|
50
|
+
}
|
|
47
51
|
},
|
|
48
52
|
onError: (err) => {
|
|
49
|
-
config.
|
|
50
|
-
callbacks
|
|
53
|
+
config.onError?.(err.message);
|
|
54
|
+
callbacks?.onError?.(err.message);
|
|
51
55
|
},
|
|
52
56
|
});
|
|
53
57
|
|
|
@@ -60,8 +64,8 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
|
|
|
60
64
|
}, []);
|
|
61
65
|
|
|
62
66
|
const generate = useCallback(
|
|
63
|
-
async (params?:
|
|
64
|
-
const imageUri = params
|
|
67
|
+
async (params?: unknown): Promise<{ success: boolean; videoUrl?: string; thumbnailUrl?: string; error?: string }> => {
|
|
68
|
+
const imageUri = (params && typeof params === "object" && "imageUri" in params ? (params as { imageUri?: string }).imageUri : undefined) || state.imageUri;
|
|
65
69
|
if (!imageUri) {
|
|
66
70
|
const error = "Image is required";
|
|
67
71
|
setState((prev) => ({ ...prev, error }));
|
|
@@ -74,11 +78,12 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
|
|
|
74
78
|
const result = await orchestrator.generate({
|
|
75
79
|
imageUrl: imageUri,
|
|
76
80
|
prompt: state.motionPrompt || "",
|
|
77
|
-
options: params,
|
|
81
|
+
options: params && typeof params === "object" ? params : undefined,
|
|
78
82
|
creationId: `image-to-video-${Date.now()}`,
|
|
79
83
|
});
|
|
80
84
|
setState((prev) => ({ ...prev, isProcessing: false }));
|
|
81
|
-
|
|
85
|
+
const typedResult = result as { success: boolean; videoUrl?: string; thumbnailUrl?: string; error?: string };
|
|
86
|
+
return typedResult;
|
|
82
87
|
} catch (error) {
|
|
83
88
|
const message = error instanceof Error ? error.message : "Generation failed";
|
|
84
89
|
setState((prev) => ({ ...prev, isProcessing: false, error: message }));
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
ANTI_REALISM_SEGMENTS,
|
|
12
12
|
ANATOMY_NEGATIVE_SEGMENTS,
|
|
13
13
|
} from "../../domain/entities/image-prompt-segments";
|
|
14
|
-
import type { ImagePromptResult, ImagePromptBuilderOptions } from "./image-prompt-builder.types";
|
|
14
|
+
import type { ImagePromptResult, ImagePromptBuilderOptions, AnimeSelfiePromptResult } from "./image-prompt-builder.types";
|
|
15
|
+
|
|
16
|
+
// Export types
|
|
17
|
+
export type { ImagePromptResult, ImagePromptBuilderOptions, AnimeSelfiePromptResult };
|
|
15
18
|
|
|
16
19
|
export class ImagePromptBuilder {
|
|
17
20
|
private positiveSegments: string[] = [];
|
|
@@ -96,3 +99,6 @@ export class ImagePromptBuilder {
|
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
|
|
103
|
+
// Export utility functions
|
|
104
|
+
export { createAnimeSelfiePrompt, createStyleTransferPrompt } from "../utils/prompt-creators.util";
|
|
@@ -13,10 +13,9 @@ import type {
|
|
|
13
13
|
TextToImageFormActions,
|
|
14
14
|
TextToImageFormDefaults,
|
|
15
15
|
} from "../../domain/types/form.types";
|
|
16
|
-
import { DEFAULT_FORM_VALUES } from "../../domain/constants/options.constants";
|
|
17
16
|
|
|
18
17
|
export interface UseFormStateOptions {
|
|
19
|
-
defaults:
|
|
18
|
+
defaults: TextToImageFormDefaults;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export interface UseFormStateReturn {
|
|
@@ -72,7 +72,7 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
|
|
|
72
72
|
|
|
73
73
|
// Use orchestrator
|
|
74
74
|
const { generate, isGenerating, error } = useGenerationOrchestrator(strategy, {
|
|
75
|
-
userId,
|
|
75
|
+
userId: userId ?? undefined,
|
|
76
76
|
alertMessages: DEFAULT_ALERT_MESSAGES,
|
|
77
77
|
onCreditsExhausted: () => callbacks.onCreditsRequired?.(totalCost),
|
|
78
78
|
onSuccess: (result) => {
|
|
@@ -132,7 +132,7 @@ export function useGeneration(options: UseGenerationOptions): UseGenerationRetur
|
|
|
132
132
|
return {
|
|
133
133
|
generationState: {
|
|
134
134
|
isGenerating,
|
|
135
|
-
error: error?.message
|
|
135
|
+
error: error?.message ?? null,
|
|
136
136
|
},
|
|
137
137
|
totalCost,
|
|
138
138
|
handleGenerate,
|
|
@@ -10,7 +10,6 @@ import { GenericWizardFlow } from "../../../../domains/generation/wizard/present
|
|
|
10
10
|
import { TEXT_TO_IMAGE_WIZARD_CONFIG } from "../../../../domains/generation/wizard/configs";
|
|
11
11
|
import { useAIFeatureGate } from "../../../../domains/access-control";
|
|
12
12
|
import {
|
|
13
|
-
createDefaultAlerts,
|
|
14
13
|
createScenarioData,
|
|
15
14
|
useWizardFlowHandlers,
|
|
16
15
|
AutoSkipPreview,
|
|
@@ -55,7 +55,7 @@ class TextToVideoExecutor extends BaseExecutor<
|
|
|
55
55
|
input: Record<string, unknown>,
|
|
56
56
|
onProgress?: (progress: number) => void,
|
|
57
57
|
): Promise<unknown> {
|
|
58
|
-
this.
|
|
58
|
+
this.log("info","Starting provider.run()...");
|
|
59
59
|
|
|
60
60
|
// Provider reports real progress via callback
|
|
61
61
|
const result = await provider.run(model, input, {
|
|
@@ -68,7 +68,7 @@ class TextToVideoExecutor extends BaseExecutor<
|
|
|
68
68
|
},
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
this.
|
|
71
|
+
this.log("info","provider.run() completed");
|
|
72
72
|
return result;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -21,7 +21,7 @@ export abstract class BaseExecutor<TRequest, TResult, TOutput> {
|
|
|
21
21
|
options: BaseExecutorOptions<TRequest, TOutput>,
|
|
22
22
|
): Promise<Result<TResult, string>> {
|
|
23
23
|
const providerResult = this.getProvider();
|
|
24
|
-
if (providerResult.error) return failure(providerResult.error);
|
|
24
|
+
if (providerResult.error || !providerResult.provider) return failure(providerResult.error);
|
|
25
25
|
|
|
26
26
|
const validationError = this.validateRequest(request);
|
|
27
27
|
if (validationError) {
|
|
@@ -88,7 +88,7 @@ export abstract class BaseExecutor<TRequest, TResult, TOutput> {
|
|
|
88
88
|
return success(this.transformResult(extracted as TOutput));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
protected log(level: "info" | "error", message: string): void {
|
|
92
92
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
93
|
const fn = level === "error" ? console.error : console.log;
|
|
94
94
|
fn(`[${this.logPrefix}] ${message}`);
|
|
@@ -19,7 +19,7 @@ export async function fetchWithTimeout<T>(
|
|
|
19
19
|
const { retries = 0 } = options;
|
|
20
20
|
|
|
21
21
|
const operation = async (): Promise<T> => {
|
|
22
|
-
const response = await executeRequest
|
|
22
|
+
const response = await executeRequest(url, options);
|
|
23
23
|
|
|
24
24
|
if (!isSuccessResponse(response)) {
|
|
25
25
|
const errorMessage = await extractErrorMessage(response);
|
|
@@ -11,7 +11,7 @@ import type { RequestOptions } from "./api-client.types";
|
|
|
11
11
|
/**
|
|
12
12
|
* Executes HTTP request with timeout
|
|
13
13
|
*/
|
|
14
|
-
export async function executeRequest
|
|
14
|
+
export async function executeRequest(
|
|
15
15
|
url: string,
|
|
16
16
|
options: RequestOptions = {}
|
|
17
17
|
): Promise<Response> {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Domain-Specific Type Guards
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { hasProperty
|
|
5
|
+
import { hasProperty } from "./structure-guards";
|
|
6
|
+
import { isObject } from "./primitive-guards";
|
|
6
7
|
import { isNonEmptyString } from "./primitive-guards";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -6,7 +6,6 @@ export * from "./error-classifier.util";
|
|
|
6
6
|
export * from "./error-message-extractor.util";
|
|
7
7
|
export * from "./error-handling.util";
|
|
8
8
|
export * from "./validation.util";
|
|
9
|
-
export * from "./type-guards.util";
|
|
10
9
|
export * from "./api-client.util";
|
|
11
10
|
export * from "../../domains/background/infrastructure/utils/polling-interval.util";
|
|
12
11
|
export * from "./progress-calculator.util";
|
|
@@ -21,3 +20,8 @@ export * from "./provider-validator.util";
|
|
|
21
20
|
export * from "./base64.util";
|
|
22
21
|
export * from "./video-result-extractor.util";
|
|
23
22
|
export * from "./id-generator.util";
|
|
23
|
+
|
|
24
|
+
// Export type guards (avoiding duplicate exports)
|
|
25
|
+
export { hasProperty, hasProperties } from "./structure-guards";
|
|
26
|
+
export { isObject, isNonEmptyString, isArray, isNumber, isFunction } from "./primitive-guards";
|
|
27
|
+
export { isCreationWithOutput, isWizardData } from "./domain-guards";
|
|
@@ -33,6 +33,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
33
33
|
const [state, setState] = useState<GenerationState<TResult>>(INITIAL_STATE);
|
|
34
34
|
const isGeneratingRef = useRef(false);
|
|
35
35
|
const isMountedRef = useRef(true);
|
|
36
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
36
37
|
|
|
37
38
|
const offlineStore = useOfflineStore();
|
|
38
39
|
const { showError, showSuccess } = useAlert();
|
|
@@ -40,7 +41,10 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
40
41
|
|
|
41
42
|
useEffect(() => {
|
|
42
43
|
isMountedRef.current = true;
|
|
43
|
-
return () => {
|
|
44
|
+
return () => {
|
|
45
|
+
isMountedRef.current = false;
|
|
46
|
+
abortControllerRef.current?.abort();
|
|
47
|
+
};
|
|
44
48
|
}, []);
|
|
45
49
|
|
|
46
50
|
const handleLifecycleComplete = useCallback(
|
|
@@ -91,6 +95,8 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
91
95
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] generate() called");
|
|
92
96
|
if (isGeneratingRef.current) return;
|
|
93
97
|
|
|
98
|
+
// Create new AbortController for this generation
|
|
99
|
+
abortControllerRef.current = new AbortController();
|
|
94
100
|
isGeneratingRef.current = true;
|
|
95
101
|
setState({ ...INITIAL_STATE, status: "checking", isGenerating: true });
|
|
96
102
|
|
|
@@ -100,6 +106,11 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
100
106
|
throw createGenerationError("network", alertMessages.networkError);
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
// Check if aborted
|
|
110
|
+
if (abortControllerRef.current.signal.aborted) {
|
|
111
|
+
throw new Error("Generation aborted");
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
// Pre-validate credits before generation to catch concurrent consumption
|
|
104
115
|
const creditCost = strategy.getCreditCost();
|
|
105
116
|
const hasEnoughCredits = await checkCredits(creditCost);
|
|
@@ -111,6 +122,11 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
111
122
|
throw createGenerationError("credits", alertMessages.creditFailed);
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
// Check if aborted before moderation
|
|
126
|
+
if (abortControllerRef.current.signal.aborted) {
|
|
127
|
+
throw new Error("Generation aborted");
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
return await handleModeration({
|
|
115
131
|
input,
|
|
116
132
|
moderation,
|
|
@@ -125,6 +141,14 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
125
141
|
handleLifecycleComplete,
|
|
126
142
|
});
|
|
127
143
|
} catch (err) {
|
|
144
|
+
// Don't show error if aborted
|
|
145
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
146
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
147
|
+
console.log("[Orchestrator] Generation aborted");
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
const error = parseError(err);
|
|
129
153
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[Orchestrator] Error:", error);
|
|
130
154
|
if (isMountedRef.current) setState({ status: "error", isGenerating: false, result: null, error });
|
|
@@ -134,12 +158,15 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
134
158
|
throw error;
|
|
135
159
|
} finally {
|
|
136
160
|
isGeneratingRef.current = false;
|
|
161
|
+
abortControllerRef.current = null;
|
|
137
162
|
}
|
|
138
163
|
},
|
|
139
|
-
[moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
|
|
164
|
+
[offlineStore.isOnline, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
|
|
140
165
|
);
|
|
141
166
|
|
|
142
167
|
const reset = useCallback(() => {
|
|
168
|
+
abortControllerRef.current?.abort();
|
|
169
|
+
abortControllerRef.current = null;
|
|
143
170
|
setState(INITIAL_STATE);
|
|
144
171
|
isGeneratingRef.current = false;
|
|
145
172
|
}, []);
|
|
@@ -15,6 +15,9 @@ import type {
|
|
|
15
15
|
DualImageInput,
|
|
16
16
|
} from "./use-image-generation.types";
|
|
17
17
|
|
|
18
|
+
// Export types
|
|
19
|
+
export type { ImageGenerationConfig, ImageGenerationInput, SingleImageInput, DualImageInput } from "./use-image-generation.types";
|
|
20
|
+
|
|
18
21
|
const isDualImageInput = (input: ImageGenerationInput): input is DualImageInput =>
|
|
19
22
|
"sourceImageBase64" in input && "targetImageBase64" in input;
|
|
20
23
|
|