@umituz/react-native-ai-generation-content 1.49.0 → 1.50.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/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +4 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts +3 -0
- package/src/domains/generation/wizard/presentation/hooks/generation-result.utils.ts +47 -0
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +132 -0
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +169 -0
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +40 -264
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.50.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",
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -43,6 +43,10 @@ export async function buildVideoInput(
|
|
|
43
43
|
if (defaultPrompt) {
|
|
44
44
|
basePrompt = defaultPrompt;
|
|
45
45
|
} else {
|
|
46
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
+
console.error("[VideoStrategy] No prompt found for scenario:", scenario.id);
|
|
48
|
+
console.error("[VideoStrategy] Available defaults:", Object.keys(VIDEO_PROCESSING_PROMPTS));
|
|
49
|
+
}
|
|
46
50
|
throw new Error("error.generation.promptRequired");
|
|
47
51
|
}
|
|
48
52
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts
CHANGED
|
@@ -50,4 +50,7 @@ export const VIDEO_PROCESSING_PROMPTS: Record<string, string> = {
|
|
|
50
50
|
"ai-kiss": "Create a romantic video where these two people share a gentle, loving kiss",
|
|
51
51
|
"ai-hug": "Create a heartwarming video where these two people share a warm, affectionate hug",
|
|
52
52
|
"image-to-video": "Animate this image with natural, smooth motion while preserving the original style",
|
|
53
|
+
"solo_renaissance_portrait": "Transform this person into an elegant Renaissance-style animated portrait with classical artistic movements and period-appropriate lighting",
|
|
54
|
+
"renaissance_portrait": "Transform this portrait into a majestic Renaissance-style animated painting with subtle classical movements",
|
|
55
|
+
"historical_portrait": "Animate this portrait in a historical style with period-appropriate subtle movements",
|
|
53
56
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation Result Utilities
|
|
3
|
+
* Shared utilities for extracting and processing generation results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FalResult {
|
|
7
|
+
video?: { url?: string };
|
|
8
|
+
output?: string;
|
|
9
|
+
images?: Array<{ url?: string }>;
|
|
10
|
+
image?: { url?: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GenerationUrls {
|
|
14
|
+
imageUrl?: string;
|
|
15
|
+
videoUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extracts image/video URL from FAL result
|
|
20
|
+
* Handles various result formats from different FAL models
|
|
21
|
+
*/
|
|
22
|
+
export function extractResultUrl(result: FalResult): GenerationUrls {
|
|
23
|
+
// Video result
|
|
24
|
+
if (result.video?.url) {
|
|
25
|
+
return { videoUrl: result.video.url };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Output URL (some models return direct URL)
|
|
29
|
+
if (typeof result.output === "string" && result.output.startsWith("http")) {
|
|
30
|
+
if (result.output.includes(".mp4") || result.output.includes("video")) {
|
|
31
|
+
return { videoUrl: result.output };
|
|
32
|
+
}
|
|
33
|
+
return { imageUrl: result.output };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Images array (most image models)
|
|
37
|
+
if (result.images?.[0]?.url) {
|
|
38
|
+
return { imageUrl: result.images[0].url };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Single image
|
|
42
|
+
if (result.image?.url) {
|
|
43
|
+
return { imageUrl: result.image.url };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePhotoBlockingGeneration Hook
|
|
3
|
+
* Handles photo generation via blocking execution
|
|
4
|
+
* - Uses orchestrator for synchronous generation
|
|
5
|
+
* - Waits for result before returning
|
|
6
|
+
* - Best for quick image operations (10-30 seconds)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRef, useCallback } from "react";
|
|
10
|
+
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
11
|
+
import type { CreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
12
|
+
import type { WizardStrategy } from "../../infrastructure/strategies/wizard-strategy.types";
|
|
13
|
+
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
14
|
+
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
export interface UsePhotoBlockingGenerationProps {
|
|
19
|
+
readonly userId?: string;
|
|
20
|
+
readonly scenario: WizardScenarioData;
|
|
21
|
+
readonly persistence: CreationPersistence;
|
|
22
|
+
readonly strategy: WizardStrategy;
|
|
23
|
+
readonly alertMessages: AlertMessages;
|
|
24
|
+
readonly onSuccess?: (result: unknown) => void;
|
|
25
|
+
readonly onError?: (error: string) => void;
|
|
26
|
+
readonly onCreditsExhausted?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UsePhotoBlockingGenerationReturn {
|
|
30
|
+
readonly isGenerating: boolean;
|
|
31
|
+
readonly startGeneration: (input: unknown, prompt: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function usePhotoBlockingGeneration(
|
|
35
|
+
props: UsePhotoBlockingGenerationProps,
|
|
36
|
+
): UsePhotoBlockingGenerationReturn {
|
|
37
|
+
const {
|
|
38
|
+
userId,
|
|
39
|
+
scenario,
|
|
40
|
+
persistence,
|
|
41
|
+
strategy,
|
|
42
|
+
alertMessages,
|
|
43
|
+
onSuccess,
|
|
44
|
+
onError,
|
|
45
|
+
onCreditsExhausted,
|
|
46
|
+
} = props;
|
|
47
|
+
|
|
48
|
+
const creationIdRef = useRef<string | null>(null);
|
|
49
|
+
|
|
50
|
+
const handleSuccess = useCallback(
|
|
51
|
+
async (result: unknown) => {
|
|
52
|
+
const typedResult = result as { imageUrl?: string; videoUrl?: string };
|
|
53
|
+
const creationId = creationIdRef.current;
|
|
54
|
+
|
|
55
|
+
if (creationId && userId) {
|
|
56
|
+
try {
|
|
57
|
+
await persistence.updateToCompleted(userId, creationId, {
|
|
58
|
+
uri: typedResult.imageUrl || typedResult.videoUrl || "",
|
|
59
|
+
imageUrl: typedResult.imageUrl,
|
|
60
|
+
videoUrl: typedResult.videoUrl,
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
+
console.error("[PhotoBlockingGeneration] updateToCompleted error:", err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
creationIdRef.current = null;
|
|
70
|
+
onSuccess?.(result);
|
|
71
|
+
},
|
|
72
|
+
[userId, persistence, onSuccess],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const handleError = useCallback(
|
|
76
|
+
async (err: { message: string }) => {
|
|
77
|
+
const creationId = creationIdRef.current;
|
|
78
|
+
|
|
79
|
+
if (creationId && userId) {
|
|
80
|
+
try {
|
|
81
|
+
await persistence.updateToFailed(userId, creationId, err.message);
|
|
82
|
+
} catch (updateErr) {
|
|
83
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
84
|
+
console.error("[PhotoBlockingGeneration] updateToFailed error:", updateErr);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
creationIdRef.current = null;
|
|
90
|
+
onError?.(err.message);
|
|
91
|
+
},
|
|
92
|
+
[userId, persistence, onError],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const { generate, isGenerating } = useGenerationOrchestrator(strategy, {
|
|
96
|
+
userId,
|
|
97
|
+
alertMessages,
|
|
98
|
+
onCreditsExhausted,
|
|
99
|
+
onSuccess: handleSuccess,
|
|
100
|
+
onError: handleError,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const startGeneration = useCallback(
|
|
104
|
+
async (input: unknown, prompt: string) => {
|
|
105
|
+
// Save to Firestore first
|
|
106
|
+
if (userId && prompt) {
|
|
107
|
+
try {
|
|
108
|
+
const creationId = await persistence.saveAsProcessing(userId, {
|
|
109
|
+
scenarioId: scenario.id,
|
|
110
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
111
|
+
prompt,
|
|
112
|
+
});
|
|
113
|
+
creationIdRef.current = creationId;
|
|
114
|
+
|
|
115
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
+
console.log("[PhotoBlockingGeneration] Saved as processing:", creationId);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
+
console.error("[PhotoBlockingGeneration] saveAsProcessing error:", err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Start blocking generation
|
|
126
|
+
generate(input);
|
|
127
|
+
},
|
|
128
|
+
[userId, scenario, persistence, generate],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return { isGenerating, startGeneration };
|
|
132
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVideoQueueGeneration Hook
|
|
3
|
+
* Handles video generation via FAL queue with background support
|
|
4
|
+
* - Submits to queue for non-blocking generation
|
|
5
|
+
* - Polls for completion status
|
|
6
|
+
* - Supports background generation (user can dismiss wizard)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef, useCallback, useState } from "react";
|
|
10
|
+
import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
|
|
11
|
+
import { extractResultUrl, type FalResult, type GenerationUrls } from "./generation-result.utils";
|
|
12
|
+
import type { CreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
13
|
+
import type { WizardStrategy } from "../../infrastructure/strategies/wizard-strategy.types";
|
|
14
|
+
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
const POLL_INTERVAL_MS = 3000;
|
|
19
|
+
|
|
20
|
+
export interface UseVideoQueueGenerationProps {
|
|
21
|
+
readonly userId?: string;
|
|
22
|
+
readonly scenario: WizardScenarioData;
|
|
23
|
+
readonly persistence: CreationPersistence;
|
|
24
|
+
readonly strategy: WizardStrategy;
|
|
25
|
+
readonly onSuccess?: (result: unknown) => void;
|
|
26
|
+
readonly onError?: (error: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseVideoQueueGenerationReturn {
|
|
30
|
+
readonly isGenerating: boolean;
|
|
31
|
+
readonly startGeneration: (input: unknown, prompt: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useVideoQueueGeneration(
|
|
35
|
+
props: UseVideoQueueGenerationProps,
|
|
36
|
+
): UseVideoQueueGenerationReturn {
|
|
37
|
+
const { userId, scenario, persistence, strategy, onSuccess, onError } = props;
|
|
38
|
+
|
|
39
|
+
const creationIdRef = useRef<string | null>(null);
|
|
40
|
+
const requestIdRef = useRef<string | null>(null);
|
|
41
|
+
const modelRef = useRef<string | null>(null);
|
|
42
|
+
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
43
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
44
|
+
|
|
45
|
+
// Cleanup polling on unmount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
return () => {
|
|
48
|
+
if (pollingRef.current) {
|
|
49
|
+
clearInterval(pollingRef.current);
|
|
50
|
+
pollingRef.current = null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const resetRefs = useCallback(() => {
|
|
56
|
+
creationIdRef.current = null;
|
|
57
|
+
requestIdRef.current = null;
|
|
58
|
+
modelRef.current = null;
|
|
59
|
+
setIsGenerating(false);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const handleComplete = useCallback(
|
|
63
|
+
async (urls: GenerationUrls) => {
|
|
64
|
+
const creationId = creationIdRef.current;
|
|
65
|
+
if (creationId && userId) {
|
|
66
|
+
try {
|
|
67
|
+
await persistence.updateToCompleted(userId, creationId, {
|
|
68
|
+
uri: urls.videoUrl || urls.imageUrl || "",
|
|
69
|
+
imageUrl: urls.imageUrl,
|
|
70
|
+
videoUrl: urls.videoUrl,
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] updateToCompleted error:", err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
resetRefs();
|
|
77
|
+
onSuccess?.(urls);
|
|
78
|
+
},
|
|
79
|
+
[userId, persistence, onSuccess, resetRefs],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const handleError = useCallback(
|
|
83
|
+
async (errorMsg: string) => {
|
|
84
|
+
const creationId = creationIdRef.current;
|
|
85
|
+
if (creationId && userId) {
|
|
86
|
+
try {
|
|
87
|
+
await persistence.updateToFailed(userId, creationId, errorMsg);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] updateToFailed error:", err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
resetRefs();
|
|
93
|
+
onError?.(errorMsg);
|
|
94
|
+
},
|
|
95
|
+
[userId, persistence, onError, resetRefs],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const pollQueueStatus = useCallback(async () => {
|
|
99
|
+
const requestId = requestIdRef.current;
|
|
100
|
+
const model = modelRef.current;
|
|
101
|
+
const provider = providerRegistry.getActiveProvider();
|
|
102
|
+
if (!requestId || !model || !provider) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const status = await provider.getJobStatus(model, requestId);
|
|
106
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[VideoQueueGeneration] Poll:", status.status);
|
|
107
|
+
|
|
108
|
+
if (status.status === "COMPLETED" || status.status === "FAILED") {
|
|
109
|
+
if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; }
|
|
110
|
+
if (status.status === "COMPLETED") {
|
|
111
|
+
const result = await provider.getJobResult<FalResult>(model, requestId);
|
|
112
|
+
await handleComplete(extractResultUrl(result));
|
|
113
|
+
} else {
|
|
114
|
+
await handleError("Generation failed");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] Poll error:", err);
|
|
119
|
+
}
|
|
120
|
+
}, [handleComplete, handleError]);
|
|
121
|
+
|
|
122
|
+
const startGeneration = useCallback(
|
|
123
|
+
async (input: unknown, prompt: string) => {
|
|
124
|
+
if (!strategy.submitToQueue) { onError?.("Queue submission not available"); return; }
|
|
125
|
+
setIsGenerating(true);
|
|
126
|
+
|
|
127
|
+
// Save to Firestore FIRST (enables background visibility)
|
|
128
|
+
let creationId: string | null = null;
|
|
129
|
+
if (userId && prompt) {
|
|
130
|
+
try {
|
|
131
|
+
creationId = await persistence.saveAsProcessing(userId, {
|
|
132
|
+
scenarioId: scenario.id, scenarioTitle: scenario.title || scenario.id, prompt,
|
|
133
|
+
});
|
|
134
|
+
creationIdRef.current = creationId;
|
|
135
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[VideoQueueGeneration] Saved:", creationId);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] save error:", err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const queueResult = await strategy.submitToQueue(input);
|
|
142
|
+
if (!queueResult.success || !queueResult.requestId || !queueResult.model) {
|
|
143
|
+
if (creationId && userId) await persistence.updateToFailed(userId, creationId, queueResult.error || "Queue submission failed");
|
|
144
|
+
setIsGenerating(false);
|
|
145
|
+
onError?.(queueResult.error || "Queue submission failed");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
requestIdRef.current = queueResult.requestId;
|
|
150
|
+
modelRef.current = queueResult.model;
|
|
151
|
+
|
|
152
|
+
// Update with requestId for background polling
|
|
153
|
+
if (creationId && userId) {
|
|
154
|
+
try {
|
|
155
|
+
await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
|
|
156
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[VideoQueueGeneration] Updated requestId:", queueResult.requestId);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[VideoQueueGeneration] updateRequestId error:", err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pollingRef.current = setInterval(() => void pollQueueStatus(), POLL_INTERVAL_MS);
|
|
163
|
+
void pollQueueStatus();
|
|
164
|
+
},
|
|
165
|
+
[userId, scenario, persistence, strategy, pollQueueStatus, onError],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return { isGenerating, startGeneration };
|
|
169
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
|
-
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* - Saves requestId/model to Firestore for background polling
|
|
3
|
+
* Orchestrates wizard-based generation by delegating to appropriate mode:
|
|
4
|
+
* - Video: Queue-based generation with background support
|
|
5
|
+
* - Photo: Blocking execution for quick results
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import { useEffect, useRef, useMemo
|
|
10
|
-
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
11
|
-
import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
|
|
8
|
+
import { useEffect, useRef, useMemo } from "react";
|
|
12
9
|
import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
|
|
13
10
|
import { createCreationPersistence } from "../../infrastructure/utils/creation-persistence.util";
|
|
11
|
+
import { useVideoQueueGeneration } from "./useVideoQueueGeneration";
|
|
12
|
+
import { usePhotoBlockingGeneration } from "./usePhotoBlockingGeneration";
|
|
14
13
|
import type {
|
|
15
14
|
UseWizardGenerationProps,
|
|
16
15
|
UseWizardGenerationReturn,
|
|
@@ -18,8 +17,6 @@ import type {
|
|
|
18
17
|
|
|
19
18
|
declare const __DEV__: boolean;
|
|
20
19
|
|
|
21
|
-
const POLL_INTERVAL_MS = 3000;
|
|
22
|
-
|
|
23
20
|
export type {
|
|
24
21
|
WizardOutputType,
|
|
25
22
|
WizardScenarioData,
|
|
@@ -27,32 +24,6 @@ export type {
|
|
|
27
24
|
UseWizardGenerationReturn,
|
|
28
25
|
} from "./wizard-generation.types";
|
|
29
26
|
|
|
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
|
-
|
|
56
27
|
export const useWizardGeneration = (
|
|
57
28
|
props: UseWizardGenerationProps,
|
|
58
29
|
): UseWizardGenerationReturn => {
|
|
@@ -69,11 +40,6 @@ export const useWizardGeneration = (
|
|
|
69
40
|
} = props;
|
|
70
41
|
|
|
71
42
|
const hasStarted = useRef(false);
|
|
72
|
-
const creationIdRef = useRef<string | null>(null);
|
|
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);
|
|
77
43
|
|
|
78
44
|
const persistence = useMemo(() => createCreationPersistence(), []);
|
|
79
45
|
const strategy = useMemo(
|
|
@@ -81,158 +47,35 @@ export const useWizardGeneration = (
|
|
|
81
47
|
[scenario, creditCost],
|
|
82
48
|
);
|
|
83
49
|
|
|
84
|
-
|
|
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;
|
|
50
|
+
const isVideoMode = scenario.outputType === "video" && !!strategy.submitToQueue;
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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(
|
|
182
|
-
async (result: unknown) => {
|
|
183
|
-
const typedResult = result as { imageUrl?: string; videoUrl?: string };
|
|
184
|
-
const creationId = creationIdRef.current;
|
|
185
|
-
|
|
186
|
-
if (creationId && userId) {
|
|
187
|
-
try {
|
|
188
|
-
await persistence.updateToCompleted(userId, creationId, {
|
|
189
|
-
uri: typedResult.imageUrl || typedResult.videoUrl || "",
|
|
190
|
-
imageUrl: typedResult.imageUrl,
|
|
191
|
-
videoUrl: typedResult.videoUrl,
|
|
192
|
-
});
|
|
193
|
-
} catch (err) {
|
|
194
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
195
|
-
console.error("[useWizardGeneration] updateToCompleted error:", err);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
creationIdRef.current = null;
|
|
201
|
-
onSuccess?.(result);
|
|
202
|
-
},
|
|
203
|
-
[userId, persistence, onSuccess],
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const handleBlockingError = useCallback(
|
|
207
|
-
async (err: { message: string }) => {
|
|
208
|
-
const creationId = creationIdRef.current;
|
|
209
|
-
|
|
210
|
-
if (creationId && userId) {
|
|
211
|
-
try {
|
|
212
|
-
await persistence.updateToFailed(userId, creationId, err.message);
|
|
213
|
-
} catch (updateErr) {
|
|
214
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
215
|
-
console.error("[useWizardGeneration] updateToFailed error:", updateErr);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
creationIdRef.current = null;
|
|
221
|
-
onError?.(err.message);
|
|
222
|
-
},
|
|
223
|
-
[userId, persistence, onError],
|
|
224
|
-
);
|
|
52
|
+
// Video generation hook (queue-based)
|
|
53
|
+
const videoGeneration = useVideoQueueGeneration({
|
|
54
|
+
userId,
|
|
55
|
+
scenario,
|
|
56
|
+
persistence,
|
|
57
|
+
strategy,
|
|
58
|
+
onSuccess,
|
|
59
|
+
onError,
|
|
60
|
+
});
|
|
225
61
|
|
|
226
|
-
|
|
62
|
+
// Photo generation hook (blocking)
|
|
63
|
+
const photoGeneration = usePhotoBlockingGeneration({
|
|
227
64
|
userId,
|
|
65
|
+
scenario,
|
|
66
|
+
persistence,
|
|
67
|
+
strategy,
|
|
228
68
|
alertMessages,
|
|
69
|
+
onSuccess,
|
|
70
|
+
onError,
|
|
229
71
|
onCreditsExhausted,
|
|
230
|
-
onSuccess: handleBlockingSuccess,
|
|
231
|
-
onError: handleBlockingError,
|
|
232
72
|
});
|
|
233
73
|
|
|
74
|
+
// Main effect: trigger generation when step becomes active
|
|
234
75
|
useEffect(() => {
|
|
235
|
-
|
|
76
|
+
const isAlreadyGenerating = videoGeneration.isGenerating || photoGeneration.isGenerating;
|
|
77
|
+
|
|
78
|
+
if (isGeneratingStep && !hasStarted.current && !isAlreadyGenerating) {
|
|
236
79
|
hasStarted.current = true;
|
|
237
80
|
|
|
238
81
|
buildWizardInput(wizardData, scenario)
|
|
@@ -244,86 +87,21 @@ export const useWizardGeneration = (
|
|
|
244
87
|
}
|
|
245
88
|
|
|
246
89
|
const typedInput = input as { prompt?: string };
|
|
247
|
-
const useQueueMode = scenario.outputType === "video" && !!strategy.submitToQueue;
|
|
248
90
|
|
|
249
91
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
250
|
-
console.log("[
|
|
92
|
+
console.log("[WizardGeneration] Mode:", isVideoMode ? "VIDEO_QUEUE" : "PHOTO_BLOCKING");
|
|
251
93
|
}
|
|
252
94
|
|
|
253
|
-
if (
|
|
254
|
-
|
|
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
|
-
}
|
|
292
|
-
}
|
|
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();
|
|
95
|
+
if (isVideoMode) {
|
|
96
|
+
await videoGeneration.startGeneration(input, typedInput.prompt || "");
|
|
302
97
|
} else {
|
|
303
|
-
|
|
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
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
generate(input);
|
|
98
|
+
await photoGeneration.startGeneration(input, typedInput.prompt || "");
|
|
324
99
|
}
|
|
325
100
|
})
|
|
326
101
|
.catch((error) => {
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
+
console.error("[WizardGeneration] Build input error:", error.message);
|
|
104
|
+
}
|
|
327
105
|
hasStarted.current = false;
|
|
328
106
|
onError?.(error.message);
|
|
329
107
|
});
|
|
@@ -336,15 +114,13 @@ export const useWizardGeneration = (
|
|
|
336
114
|
isGeneratingStep,
|
|
337
115
|
scenario,
|
|
338
116
|
wizardData,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
117
|
+
isVideoMode,
|
|
118
|
+
videoGeneration,
|
|
119
|
+
photoGeneration,
|
|
342
120
|
onError,
|
|
343
|
-
userId,
|
|
344
|
-
persistence,
|
|
345
|
-
strategy,
|
|
346
|
-
pollQueueStatus,
|
|
347
121
|
]);
|
|
348
122
|
|
|
349
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
isGenerating: videoGeneration.isGenerating || photoGeneration.isGenerating,
|
|
125
|
+
};
|
|
350
126
|
};
|