@umituz/react-native-ai-generation-content 1.48.2 → 1.49.0
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/creations/domain/entities/Creation.ts +8 -0
- package/src/domains/creations/index.ts +5 -0
- package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +3 -0
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +163 -0
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +9 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +26 -1
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +10 -1
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.util.ts +30 -1
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +229 -40
- package/src/index.ts +1 -0
- package/src/infrastructure/services/index.ts +1 -0
- package/src/infrastructure/services/video-feature-executor.service.ts +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
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",
|
|
@@ -35,6 +35,9 @@ export interface Creation {
|
|
|
35
35
|
// Extended fields for job-based creations
|
|
36
36
|
readonly status?: CreationStatus;
|
|
37
37
|
readonly output?: CreationOutput;
|
|
38
|
+
// Background job tracking - FAL queue requestId and model
|
|
39
|
+
readonly requestId?: string;
|
|
40
|
+
readonly model?: string;
|
|
38
41
|
// Soft delete - if set, the creation is considered deleted
|
|
39
42
|
readonly deletedAt?: Date;
|
|
40
43
|
}
|
|
@@ -58,6 +61,9 @@ export interface CreationDocument {
|
|
|
58
61
|
readonly createdAt: FirebaseTimestamp | Date;
|
|
59
62
|
readonly completedAt?: FirebaseTimestamp | Date;
|
|
60
63
|
readonly deletedAt?: FirebaseTimestamp | Date;
|
|
64
|
+
// Background job tracking - FAL queue requestId and model
|
|
65
|
+
readonly requestId?: string;
|
|
66
|
+
readonly model?: string;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
interface FirebaseTimestamp {
|
|
@@ -113,6 +119,8 @@ export function mapDocumentToCreation(
|
|
|
113
119
|
ratedAt: ratedAtDate,
|
|
114
120
|
status: data.status as CreationStatus | undefined,
|
|
115
121
|
output: data.output ?? undefined,
|
|
122
|
+
requestId: data.requestId,
|
|
123
|
+
model: data.model,
|
|
116
124
|
deletedAt: deletedAtDate,
|
|
117
125
|
};
|
|
118
126
|
}
|
|
@@ -125,6 +125,11 @@ export type {
|
|
|
125
125
|
BaseProcessingStartData,
|
|
126
126
|
BaseProcessingResult,
|
|
127
127
|
} from "./presentation/hooks/useCreationPersistence";
|
|
128
|
+
export { useProcessingJobsPoller } from "./presentation/hooks/useProcessingJobsPoller";
|
|
129
|
+
export type {
|
|
130
|
+
UseProcessingJobsPollerConfig,
|
|
131
|
+
UseProcessingJobsPollerReturn,
|
|
132
|
+
} from "./presentation/hooks/useProcessingJobsPoller";
|
|
128
133
|
|
|
129
134
|
// =============================================================================
|
|
130
135
|
// PRESENTATION LAYER - Components
|
|
@@ -8,6 +8,7 @@ declare const __DEV__: boolean;
|
|
|
8
8
|
const UPDATABLE_FIELDS = [
|
|
9
9
|
"metadata", "isShared", "uri", "type", "prompt", "status",
|
|
10
10
|
"output", "rating", "ratedAt", "isFavorite", "deletedAt",
|
|
11
|
+
"requestId", "model",
|
|
11
12
|
] as const;
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -34,6 +35,8 @@ export class CreationsWriter {
|
|
|
34
35
|
...(creation.status !== undefined && { status: creation.status }),
|
|
35
36
|
...(creation.output !== undefined && { output: creation.output }),
|
|
36
37
|
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
38
|
+
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
39
|
+
...(creation.model !== undefined && { model: creation.model }),
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
try {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useProcessingJobsPoller Hook
|
|
3
|
+
* Polls FAL queue status for "processing" creations and updates Firestore when complete
|
|
4
|
+
* Enables true background generation - works even after wizard is dismissed
|
|
5
|
+
* Uses provider registry internally - no need to pass FAL functions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
9
|
+
import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
|
|
10
|
+
import type { Creation } from "../../domain/entities/Creation";
|
|
11
|
+
import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
|
|
12
|
+
|
|
13
|
+
declare const __DEV__: boolean;
|
|
14
|
+
|
|
15
|
+
const POLL_INTERVAL_MS = 5000; // Poll every 5 seconds
|
|
16
|
+
|
|
17
|
+
export interface UseProcessingJobsPollerConfig {
|
|
18
|
+
readonly userId?: string | null;
|
|
19
|
+
readonly creations: Creation[];
|
|
20
|
+
readonly repository: ICreationsRepository;
|
|
21
|
+
readonly enabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseProcessingJobsPollerReturn {
|
|
25
|
+
readonly processingCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface FalResult {
|
|
29
|
+
video?: { url?: string };
|
|
30
|
+
output?: string;
|
|
31
|
+
images?: Array<{ url?: string }>;
|
|
32
|
+
image?: { url?: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractResultUrl(result: FalResult): { imageUrl?: string; videoUrl?: string } {
|
|
36
|
+
// Video result
|
|
37
|
+
if (result.video?.url) {
|
|
38
|
+
return { videoUrl: result.video.url };
|
|
39
|
+
}
|
|
40
|
+
// Output URL (some models)
|
|
41
|
+
if (typeof result.output === "string" && result.output.startsWith("http")) {
|
|
42
|
+
if (result.output.includes(".mp4") || result.output.includes("video")) {
|
|
43
|
+
return { videoUrl: result.output };
|
|
44
|
+
}
|
|
45
|
+
return { imageUrl: result.output };
|
|
46
|
+
}
|
|
47
|
+
// Images array
|
|
48
|
+
if (result.images?.[0]?.url) {
|
|
49
|
+
return { imageUrl: result.images[0].url };
|
|
50
|
+
}
|
|
51
|
+
// Single image
|
|
52
|
+
if (result.image?.url) {
|
|
53
|
+
return { imageUrl: result.image.url };
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useProcessingJobsPoller(
|
|
59
|
+
config: UseProcessingJobsPollerConfig,
|
|
60
|
+
): UseProcessingJobsPollerReturn {
|
|
61
|
+
const {
|
|
62
|
+
userId,
|
|
63
|
+
creations,
|
|
64
|
+
repository,
|
|
65
|
+
enabled = true,
|
|
66
|
+
} = config;
|
|
67
|
+
|
|
68
|
+
const pollingRef = useRef<Set<string>>(new Set());
|
|
69
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
70
|
+
|
|
71
|
+
// Find creations that need polling
|
|
72
|
+
const processingJobs = creations.filter(
|
|
73
|
+
(c) => c.status === "processing" && c.requestId && c.model,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const pollJob = useCallback(
|
|
77
|
+
async (creation: Creation) => {
|
|
78
|
+
if (!userId || !creation.requestId || !creation.model) return;
|
|
79
|
+
if (pollingRef.current.has(creation.id)) return;
|
|
80
|
+
|
|
81
|
+
const provider = providerRegistry.getActiveProvider();
|
|
82
|
+
if (!provider || !provider.isInitialized()) return;
|
|
83
|
+
|
|
84
|
+
pollingRef.current.add(creation.id);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
88
|
+
console.log("[ProcessingJobsPoller] Checking status:", creation.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const status = await provider.getJobStatus(creation.model, creation.requestId);
|
|
92
|
+
|
|
93
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
+
console.log("[ProcessingJobsPoller] Status:", creation.id, status.status);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (status.status === "COMPLETED") {
|
|
98
|
+
// Fetch the result
|
|
99
|
+
const result = await provider.getJobResult<FalResult>(creation.model, creation.requestId);
|
|
100
|
+
const urls = extractResultUrl(result);
|
|
101
|
+
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
+
console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update Firestore
|
|
107
|
+
const uri = urls.videoUrl || urls.imageUrl || "";
|
|
108
|
+
await repository.update(userId, creation.id, {
|
|
109
|
+
status: "completed",
|
|
110
|
+
uri,
|
|
111
|
+
output: urls,
|
|
112
|
+
});
|
|
113
|
+
} else if (status.status === "FAILED") {
|
|
114
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
+
console.log("[ProcessingJobsPoller] Failed:", creation.id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await repository.update(userId, creation.id, {
|
|
119
|
+
status: "failed",
|
|
120
|
+
metadata: { error: "Generation failed" },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// If still IN_PROGRESS or IN_QUEUE, we'll check again next interval
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
+
console.error("[ProcessingJobsPoller] Poll error:", creation.id, error);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
pollingRef.current.delete(creation.id);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[userId, repository],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!enabled || !userId || processingJobs.length === 0) {
|
|
137
|
+
if (intervalRef.current) {
|
|
138
|
+
clearInterval(intervalRef.current);
|
|
139
|
+
intervalRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Initial poll
|
|
145
|
+
processingJobs.forEach((job) => void pollJob(job));
|
|
146
|
+
|
|
147
|
+
// Set up interval polling
|
|
148
|
+
intervalRef.current = setInterval(() => {
|
|
149
|
+
processingJobs.forEach((job) => void pollJob(job));
|
|
150
|
+
}, POLL_INTERVAL_MS);
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
if (intervalRef.current) {
|
|
154
|
+
clearInterval(intervalRef.current);
|
|
155
|
+
intervalRef.current = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}, [enabled, userId, processingJobs, pollJob]);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
processingCount: processingJobs.length,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@umituz/react-native-design-system";
|
|
9
9
|
import { useCreations } from "../hooks/useCreations";
|
|
10
10
|
import { useDeleteCreation } from "../hooks/useDeleteCreation";
|
|
11
|
+
import { useProcessingJobsPoller } from "../hooks/useProcessingJobsPoller";
|
|
11
12
|
import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
12
13
|
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
13
14
|
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
@@ -38,6 +39,14 @@ export function CreationsGalleryScreen({
|
|
|
38
39
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
39
40
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
40
41
|
|
|
42
|
+
// Poll FAL queue for "processing" creations (enables true background generation)
|
|
43
|
+
useProcessingJobsPoller({
|
|
44
|
+
userId,
|
|
45
|
+
creations: creations ?? [],
|
|
46
|
+
repository,
|
|
47
|
+
enabled: !!userId && (creations?.length ?? 0) > 0,
|
|
48
|
+
});
|
|
49
|
+
|
|
41
50
|
useEffect(() => {
|
|
42
51
|
if (initialCreationId && creations && creations.length > 0 && !hasAutoSelectedRef.current) {
|
|
43
52
|
const creation = creations.find((c) => c.id === initialCreationId);
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* Handles video-specific generation logic (execution only)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
executeVideoFeature,
|
|
8
|
+
submitVideoFeatureToQueue,
|
|
9
|
+
} from "../../../../../infrastructure/services/video-feature-executor.service";
|
|
7
10
|
import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
|
|
8
11
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
9
12
|
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
@@ -95,6 +98,28 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
95
98
|
return { videoUrl: result.videoUrl };
|
|
96
99
|
},
|
|
97
100
|
|
|
101
|
+
submitToQueue: async (input: unknown) => {
|
|
102
|
+
const videoInput = input as WizardVideoInput;
|
|
103
|
+
|
|
104
|
+
const result = await submitVideoFeatureToQueue(videoFeatureType, {
|
|
105
|
+
sourceImageBase64: videoInput.sourceImageBase64,
|
|
106
|
+
targetImageBase64: videoInput.targetImageBase64,
|
|
107
|
+
prompt: videoInput.prompt,
|
|
108
|
+
options: {
|
|
109
|
+
duration: videoInput.duration,
|
|
110
|
+
aspect_ratio: videoInput.aspectRatio,
|
|
111
|
+
resolution: videoInput.resolution,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
success: result.success,
|
|
117
|
+
requestId: result.requestId,
|
|
118
|
+
model: result.model,
|
|
119
|
+
error: result.error,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
98
123
|
getCreditCost: () => creditCost,
|
|
99
124
|
};
|
|
100
125
|
}
|
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
* Shared types for all wizard strategies
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export interface QueueSubmissionResult {
|
|
7
|
+
readonly success: boolean;
|
|
8
|
+
readonly requestId?: string;
|
|
9
|
+
readonly model?: string;
|
|
10
|
+
readonly error?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export interface WizardStrategy {
|
|
7
|
-
/** Execute the generation - returns result with URLs */
|
|
14
|
+
/** Execute the generation - returns result with URLs (blocking) */
|
|
8
15
|
execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string }>;
|
|
16
|
+
/** Submit to queue for background processing - returns immediately with requestId */
|
|
17
|
+
submitToQueue?: (input: unknown) => Promise<QueueSubmissionResult>;
|
|
9
18
|
/** Get credit cost for this generation */
|
|
10
19
|
getCreditCost: () => number;
|
|
11
20
|
}
|
|
@@ -16,6 +16,9 @@ export interface ProcessingCreationData {
|
|
|
16
16
|
readonly scenarioId: string;
|
|
17
17
|
readonly scenarioTitle: string;
|
|
18
18
|
readonly prompt: string;
|
|
19
|
+
// Background job tracking - FAL queue
|
|
20
|
+
readonly requestId?: string;
|
|
21
|
+
readonly model?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface CompletedCreationData {
|
|
@@ -51,6 +54,8 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
|
|
|
51
54
|
createdAt: new Date(),
|
|
52
55
|
isShared: false,
|
|
53
56
|
isFavorite: false,
|
|
57
|
+
requestId: data.requestId,
|
|
58
|
+
model: data.model,
|
|
54
59
|
metadata: {
|
|
55
60
|
scenarioId: data.scenarioId,
|
|
56
61
|
scenarioTitle: data.scenarioTitle,
|
|
@@ -58,7 +63,11 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
|
|
|
58
63
|
});
|
|
59
64
|
|
|
60
65
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
console.log("[CreationPersistence] Saved as processing", {
|
|
66
|
+
console.log("[CreationPersistence] Saved as processing", {
|
|
67
|
+
creationId,
|
|
68
|
+
requestId: data.requestId,
|
|
69
|
+
model: data.model,
|
|
70
|
+
});
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
return creationId;
|
|
@@ -104,6 +113,26 @@ export function createCreationPersistence(config: CreationPersistenceConfig = {}
|
|
|
104
113
|
console.log("[CreationPersistence] Updated to failed", { creationId, error });
|
|
105
114
|
}
|
|
106
115
|
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update creation with FAL queue requestId and model after job submission
|
|
119
|
+
* Call this after fal.queue.submit() returns
|
|
120
|
+
*/
|
|
121
|
+
updateRequestId: async (
|
|
122
|
+
userId: string,
|
|
123
|
+
creationId: string,
|
|
124
|
+
requestId: string,
|
|
125
|
+
model: string,
|
|
126
|
+
): Promise<void> => {
|
|
127
|
+
await repository.update(userId, creationId, {
|
|
128
|
+
requestId,
|
|
129
|
+
model,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
+
console.log("[CreationPersistence] Updated requestId", { creationId, requestId, model });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
107
136
|
};
|
|
108
137
|
}
|
|
109
138
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
|
-
* Wizard generation with Firestore persistence
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
3
|
+
* Wizard generation with Firestore persistence and background support
|
|
4
|
+
* - Uses queue submission for videos (enables true background generation)
|
|
5
|
+
* - Uses blocking execution for images
|
|
6
|
+
* - Saves requestId/model to Firestore for background polling
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useEffect, useRef, useMemo, useCallback } from "react";
|
|
9
|
+
import { useEffect, useRef, useMemo, useCallback, useState } from "react";
|
|
10
10
|
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
11
|
+
import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
|
|
11
12
|
import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
|
|
12
13
|
import { createCreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
13
14
|
import type {
|
|
@@ -17,6 +18,8 @@ import type {
|
|
|
17
18
|
|
|
18
19
|
declare const __DEV__: boolean;
|
|
19
20
|
|
|
21
|
+
const POLL_INTERVAL_MS = 3000;
|
|
22
|
+
|
|
20
23
|
export type {
|
|
21
24
|
WizardOutputType,
|
|
22
25
|
WizardScenarioData,
|
|
@@ -24,6 +27,32 @@ export type {
|
|
|
24
27
|
UseWizardGenerationReturn,
|
|
25
28
|
} from "./wizard-generation.types";
|
|
26
29
|
|
|
30
|
+
interface FalResult {
|
|
31
|
+
video?: { url?: string };
|
|
32
|
+
output?: string;
|
|
33
|
+
images?: Array<{ url?: string }>;
|
|
34
|
+
image?: { url?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractResultUrl(result: FalResult): { imageUrl?: string; videoUrl?: string } {
|
|
38
|
+
if (result.video?.url) {
|
|
39
|
+
return { videoUrl: result.video.url };
|
|
40
|
+
}
|
|
41
|
+
if (typeof result.output === "string" && result.output.startsWith("http")) {
|
|
42
|
+
if (result.output.includes(".mp4") || result.output.includes("video")) {
|
|
43
|
+
return { videoUrl: result.output };
|
|
44
|
+
}
|
|
45
|
+
return { imageUrl: result.output };
|
|
46
|
+
}
|
|
47
|
+
if (result.images?.[0]?.url) {
|
|
48
|
+
return { imageUrl: result.images[0].url };
|
|
49
|
+
}
|
|
50
|
+
if (result.image?.url) {
|
|
51
|
+
return { imageUrl: result.image.url };
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
export const useWizardGeneration = (
|
|
28
57
|
props: UseWizardGenerationProps,
|
|
29
58
|
): UseWizardGenerationReturn => {
|
|
@@ -41,23 +70,119 @@ export const useWizardGeneration = (
|
|
|
41
70
|
|
|
42
71
|
const hasStarted = useRef(false);
|
|
43
72
|
const creationIdRef = useRef<string | null>(null);
|
|
44
|
-
const
|
|
73
|
+
const requestIdRef = useRef<string | null>(null);
|
|
74
|
+
const modelRef = useRef<string | null>(null);
|
|
75
|
+
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
76
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
45
77
|
|
|
46
|
-
// Persistence utility - separate from strategy
|
|
47
78
|
const persistence = useMemo(() => createCreationPersistence(), []);
|
|
48
|
-
|
|
49
|
-
// Strategy - only handles execution, creditCost is passed from app
|
|
50
79
|
const strategy = useMemo(
|
|
51
80
|
() => createWizardStrategy({ scenario, creditCost }),
|
|
52
81
|
[scenario, creditCost],
|
|
53
82
|
);
|
|
54
83
|
|
|
55
|
-
|
|
84
|
+
// Cleanup polling on unmount
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
return () => {
|
|
87
|
+
if (pollingRef.current) {
|
|
88
|
+
clearInterval(pollingRef.current);
|
|
89
|
+
pollingRef.current = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const handleQueueComplete = useCallback(
|
|
95
|
+
async (urls: { imageUrl?: string; videoUrl?: string }) => {
|
|
96
|
+
const creationId = creationIdRef.current;
|
|
97
|
+
|
|
98
|
+
if (creationId && userId) {
|
|
99
|
+
try {
|
|
100
|
+
await persistence.updateToCompleted(userId, creationId, {
|
|
101
|
+
uri: urls.videoUrl || urls.imageUrl || "",
|
|
102
|
+
imageUrl: urls.imageUrl,
|
|
103
|
+
videoUrl: urls.videoUrl,
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
107
|
+
console.error("[useWizardGeneration] updateToCompleted error:", err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
creationIdRef.current = null;
|
|
113
|
+
requestIdRef.current = null;
|
|
114
|
+
modelRef.current = null;
|
|
115
|
+
setIsGenerating(false);
|
|
116
|
+
onSuccess?.(urls);
|
|
117
|
+
},
|
|
118
|
+
[userId, persistence, onSuccess],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const handleQueueError = useCallback(
|
|
122
|
+
async (errorMsg: string) => {
|
|
123
|
+
const creationId = creationIdRef.current;
|
|
124
|
+
|
|
125
|
+
if (creationId && userId) {
|
|
126
|
+
try {
|
|
127
|
+
await persistence.updateToFailed(userId, creationId, errorMsg);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
130
|
+
console.error("[useWizardGeneration] updateToFailed error:", err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
creationIdRef.current = null;
|
|
136
|
+
requestIdRef.current = null;
|
|
137
|
+
modelRef.current = null;
|
|
138
|
+
setIsGenerating(false);
|
|
139
|
+
onError?.(errorMsg);
|
|
140
|
+
},
|
|
141
|
+
[userId, persistence, onError],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const pollQueueStatus = useCallback(async () => {
|
|
145
|
+
const requestId = requestIdRef.current;
|
|
146
|
+
const model = modelRef.current;
|
|
147
|
+
const provider = providerRegistry.getActiveProvider();
|
|
148
|
+
|
|
149
|
+
if (!requestId || !model || !provider) return;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const status = await provider.getJobStatus(model, requestId);
|
|
153
|
+
|
|
154
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
155
|
+
console.log("[useWizardGeneration] Poll status:", status.status);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (status.status === "COMPLETED") {
|
|
159
|
+
if (pollingRef.current) {
|
|
160
|
+
clearInterval(pollingRef.current);
|
|
161
|
+
pollingRef.current = null;
|
|
162
|
+
}
|
|
163
|
+
const result = await provider.getJobResult<FalResult>(model, requestId);
|
|
164
|
+
const urls = extractResultUrl(result);
|
|
165
|
+
await handleQueueComplete(urls);
|
|
166
|
+
} else if (status.status === "FAILED") {
|
|
167
|
+
if (pollingRef.current) {
|
|
168
|
+
clearInterval(pollingRef.current);
|
|
169
|
+
pollingRef.current = null;
|
|
170
|
+
}
|
|
171
|
+
await handleQueueError("Generation failed");
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
175
|
+
console.error("[useWizardGeneration] Poll error:", err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, [handleQueueComplete, handleQueueError]);
|
|
179
|
+
|
|
180
|
+
// For images: use blocking execution with orchestrator
|
|
181
|
+
const handleBlockingSuccess = useCallback(
|
|
56
182
|
async (result: unknown) => {
|
|
57
183
|
const typedResult = result as { imageUrl?: string; videoUrl?: string };
|
|
58
184
|
const creationId = creationIdRef.current;
|
|
59
185
|
|
|
60
|
-
// Update to completed in Firestore
|
|
61
186
|
if (creationId && userId) {
|
|
62
187
|
try {
|
|
63
188
|
await persistence.updateToCompleted(userId, creationId, {
|
|
@@ -73,17 +198,15 @@ export const useWizardGeneration = (
|
|
|
73
198
|
}
|
|
74
199
|
|
|
75
200
|
creationIdRef.current = null;
|
|
76
|
-
inputRef.current = null;
|
|
77
201
|
onSuccess?.(result);
|
|
78
202
|
},
|
|
79
203
|
[userId, persistence, onSuccess],
|
|
80
204
|
);
|
|
81
205
|
|
|
82
|
-
const
|
|
206
|
+
const handleBlockingError = useCallback(
|
|
83
207
|
async (err: { message: string }) => {
|
|
84
208
|
const creationId = creationIdRef.current;
|
|
85
209
|
|
|
86
|
-
// Update to failed in Firestore
|
|
87
210
|
if (creationId && userId) {
|
|
88
211
|
try {
|
|
89
212
|
await persistence.updateToFailed(userId, creationId, err.message);
|
|
@@ -95,22 +218,21 @@ export const useWizardGeneration = (
|
|
|
95
218
|
}
|
|
96
219
|
|
|
97
220
|
creationIdRef.current = null;
|
|
98
|
-
inputRef.current = null;
|
|
99
221
|
onError?.(err.message);
|
|
100
222
|
},
|
|
101
223
|
[userId, persistence, onError],
|
|
102
224
|
);
|
|
103
225
|
|
|
104
|
-
const { generate, isGenerating } = useGenerationOrchestrator(strategy, {
|
|
226
|
+
const { generate, isGenerating: isBlockingGenerating } = useGenerationOrchestrator(strategy, {
|
|
105
227
|
userId,
|
|
106
228
|
alertMessages,
|
|
107
229
|
onCreditsExhausted,
|
|
108
|
-
onSuccess:
|
|
109
|
-
onError:
|
|
230
|
+
onSuccess: handleBlockingSuccess,
|
|
231
|
+
onError: handleBlockingError,
|
|
110
232
|
});
|
|
111
233
|
|
|
112
234
|
useEffect(() => {
|
|
113
|
-
if (isGeneratingStep && !hasStarted.current && !isGenerating) {
|
|
235
|
+
if (isGeneratingStep && !hasStarted.current && !isGenerating && !isBlockingGenerating) {
|
|
114
236
|
hasStarted.current = true;
|
|
115
237
|
|
|
116
238
|
buildWizardInput(wizardData, scenario)
|
|
@@ -121,30 +243,85 @@ export const useWizardGeneration = (
|
|
|
121
243
|
return;
|
|
122
244
|
}
|
|
123
245
|
|
|
124
|
-
inputRef.current = input as { prompt: string };
|
|
125
|
-
|
|
126
|
-
// Save to Firestore with status="processing"
|
|
127
246
|
const typedInput = input as { prompt?: string };
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
247
|
+
const useQueueMode = scenario.outputType === "video" && !!strategy.submitToQueue;
|
|
248
|
+
|
|
249
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
250
|
+
console.log("[useWizardGeneration] Mode:", useQueueMode ? "QUEUE" : "BLOCKING");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (useQueueMode && strategy.submitToQueue) {
|
|
254
|
+
// Queue mode for videos
|
|
255
|
+
setIsGenerating(true);
|
|
256
|
+
|
|
257
|
+
// Submit to queue first to get requestId
|
|
258
|
+
const queueResult = await strategy.submitToQueue(input);
|
|
259
|
+
|
|
260
|
+
if (!queueResult.success || !queueResult.requestId || !queueResult.model) {
|
|
261
|
+
hasStarted.current = false;
|
|
262
|
+
setIsGenerating(false);
|
|
263
|
+
onError?.(queueResult.error || "Queue submission failed");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
requestIdRef.current = queueResult.requestId;
|
|
268
|
+
modelRef.current = queueResult.model;
|
|
269
|
+
|
|
270
|
+
// Save to Firestore with requestId and model
|
|
271
|
+
if (userId && typedInput.prompt) {
|
|
272
|
+
try {
|
|
273
|
+
const creationId = await persistence.saveAsProcessing(userId, {
|
|
274
|
+
scenarioId: scenario.id,
|
|
275
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
276
|
+
prompt: typedInput.prompt,
|
|
277
|
+
requestId: queueResult.requestId,
|
|
278
|
+
model: queueResult.model,
|
|
279
|
+
});
|
|
280
|
+
creationIdRef.current = creationId;
|
|
281
|
+
|
|
282
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
283
|
+
console.log("[useWizardGeneration] Saved with requestId:", {
|
|
284
|
+
creationId,
|
|
285
|
+
requestId: queueResult.requestId,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
290
|
+
console.error("[useWizardGeneration] saveAsProcessing error:", err);
|
|
291
|
+
}
|
|
139
292
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Start polling for completion
|
|
296
|
+
pollingRef.current = setInterval(() => {
|
|
297
|
+
void pollQueueStatus();
|
|
298
|
+
}, POLL_INTERVAL_MS);
|
|
299
|
+
|
|
300
|
+
// Initial poll
|
|
301
|
+
void pollQueueStatus();
|
|
302
|
+
} else {
|
|
303
|
+
// Blocking mode for images
|
|
304
|
+
if (userId && typedInput.prompt) {
|
|
305
|
+
try {
|
|
306
|
+
const creationId = await persistence.saveAsProcessing(userId, {
|
|
307
|
+
scenarioId: scenario.id,
|
|
308
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
309
|
+
prompt: typedInput.prompt,
|
|
310
|
+
});
|
|
311
|
+
creationIdRef.current = creationId;
|
|
312
|
+
|
|
313
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
314
|
+
console.log("[useWizardGeneration] Saved as processing:", creationId);
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
318
|
+
console.error("[useWizardGeneration] saveAsProcessing error:", err);
|
|
319
|
+
}
|
|
143
320
|
}
|
|
144
321
|
}
|
|
145
|
-
}
|
|
146
322
|
|
|
147
|
-
|
|
323
|
+
generate(input);
|
|
324
|
+
}
|
|
148
325
|
})
|
|
149
326
|
.catch((error) => {
|
|
150
327
|
hasStarted.current = false;
|
|
@@ -155,7 +332,19 @@ export const useWizardGeneration = (
|
|
|
155
332
|
if (!isGeneratingStep && hasStarted.current) {
|
|
156
333
|
hasStarted.current = false;
|
|
157
334
|
}
|
|
158
|
-
}, [
|
|
335
|
+
}, [
|
|
336
|
+
isGeneratingStep,
|
|
337
|
+
scenario,
|
|
338
|
+
wizardData,
|
|
339
|
+
isGenerating,
|
|
340
|
+
isBlockingGenerating,
|
|
341
|
+
generate,
|
|
342
|
+
onError,
|
|
343
|
+
userId,
|
|
344
|
+
persistence,
|
|
345
|
+
strategy,
|
|
346
|
+
pollQueueStatus,
|
|
347
|
+
]);
|
|
159
348
|
|
|
160
|
-
return { isGenerating };
|
|
349
|
+
return { isGenerating: isGenerating || isBlockingGenerating };
|
|
161
350
|
};
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
export {
|
|
48
48
|
providerRegistry, generationOrchestrator, pollJob, createJobPoller,
|
|
49
49
|
executeImageFeature, hasImageFeatureSupport, executeVideoFeature, hasVideoFeatureSupport,
|
|
50
|
+
submitVideoFeatureToQueue,
|
|
50
51
|
} from "./infrastructure/services";
|
|
51
52
|
|
|
52
53
|
export type {
|
|
@@ -160,6 +160,67 @@ export function hasVideoFeatureSupport(): boolean {
|
|
|
160
160
|
return provider !== null && provider.isInitialized();
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Submit a video feature to the queue for background processing
|
|
165
|
+
* Returns immediately with requestId and model for later status polling
|
|
166
|
+
*/
|
|
167
|
+
export async function submitVideoFeatureToQueue(
|
|
168
|
+
featureType: VideoFeatureType,
|
|
169
|
+
request: VideoFeatureRequest,
|
|
170
|
+
): Promise<{ success: boolean; requestId?: string; model?: string; error?: string }> {
|
|
171
|
+
if (__DEV__) {
|
|
172
|
+
console.log(`[VideoExecutor:${featureType}] QUEUE SUBMIT`, {
|
|
173
|
+
hasSource: !!request.sourceImageBase64,
|
|
174
|
+
hasTarget: !!request.targetImageBase64,
|
|
175
|
+
promptLength: request.prompt?.length ?? 0,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const provider = providerRegistry.getActiveProvider();
|
|
180
|
+
|
|
181
|
+
if (!provider) {
|
|
182
|
+
return { success: false, error: "No AI provider configured" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!provider.isInitialized()) {
|
|
186
|
+
return { success: false, error: "AI provider not initialized" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const model = provider.getVideoFeatureModel(featureType);
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const inputData: VideoFeatureInputData = {
|
|
193
|
+
sourceImageBase64: cleanBase64(request.sourceImageBase64),
|
|
194
|
+
targetImageBase64: cleanBase64(request.targetImageBase64),
|
|
195
|
+
prompt: request.prompt,
|
|
196
|
+
options: request.options,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const input = provider.buildVideoFeatureInput(featureType, inputData);
|
|
200
|
+
|
|
201
|
+
const submission = await provider.submitJob(model, input);
|
|
202
|
+
|
|
203
|
+
if (__DEV__) {
|
|
204
|
+
console.log(`[VideoExecutor:${featureType}] QUEUE SUBMITTED`, {
|
|
205
|
+
requestId: submission.requestId,
|
|
206
|
+
model,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
requestId: submission.requestId,
|
|
213
|
+
model,
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = extractErrorMessage(error, "Queue submission failed", `Video:${featureType}`);
|
|
217
|
+
if (__DEV__) {
|
|
218
|
+
console.error(`[VideoExecutor:${featureType}] QUEUE EXCEPTION`, { error: message });
|
|
219
|
+
}
|
|
220
|
+
return { success: false, error: message };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
163
224
|
export type {
|
|
164
225
|
ExecuteVideoFeatureOptions,
|
|
165
226
|
VideoFeatureResult,
|