@umituz/react-native-ai-generation-content 1.25.21 → 1.25.23
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/wizard/infrastructure/strategies/index.ts +2 -0
- package/src/domains/wizard/infrastructure/strategies/wizard-strategy.factory.ts +358 -0
- package/src/domains/wizard/presentation/components/GenericWizardFlow.tsx +26 -3
- package/src/domains/wizard/presentation/hooks/useWizardGeneration.ts +54 -301
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.23",
|
|
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",
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard Strategy Factory
|
|
3
|
+
* Creates generation strategies for wizard-based scenarios
|
|
4
|
+
* Centralized strategy creation for all wizard flows
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
8
|
+
import type { GenerationStrategy } from "../../../../presentation/hooks/generation/types";
|
|
9
|
+
import type { VideoFeatureType } from "../../../../domain/interfaces";
|
|
10
|
+
import { executeVideoFeature } from "../../../../infrastructure/services/video-feature-executor.service";
|
|
11
|
+
import { createCreationsRepository } from "../../../creations/infrastructure/adapters";
|
|
12
|
+
import { enhanceCouplePrompt } from "../../../../features/couple-future/infrastructure/couplePromptEnhancer";
|
|
13
|
+
import type { CoupleFeatureSelection } from "../../../../features/couple-future/domain/types";
|
|
14
|
+
import type { WizardOutputType, WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
interface ImageGenerationInput {
|
|
23
|
+
readonly partnerABase64: string;
|
|
24
|
+
readonly partnerBBase64: string;
|
|
25
|
+
readonly prompt: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface VideoGenerationInput {
|
|
29
|
+
readonly sourceImageBase64: string;
|
|
30
|
+
readonly targetImageBase64: string;
|
|
31
|
+
readonly prompt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type WizardGenerationInput = ImageGenerationInput | VideoGenerationInput;
|
|
35
|
+
type WizardGenerationResult = { imageUrl: string } | { videoUrl: string };
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Image Generation Executor
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
async function executeImageGeneration(
|
|
42
|
+
input: ImageGenerationInput,
|
|
43
|
+
onProgress?: (progress: number) => void,
|
|
44
|
+
): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
|
45
|
+
const { providerRegistry } = await import("../../../../infrastructure/services/provider-registry.service");
|
|
46
|
+
|
|
47
|
+
const provider = providerRegistry.getActiveProvider();
|
|
48
|
+
if (!provider || !provider.isInitialized()) {
|
|
49
|
+
return { success: false, error: "AI provider not initialized" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
onProgress?.(5);
|
|
54
|
+
|
|
55
|
+
const formatBase64 = (base64: string): string => {
|
|
56
|
+
return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const imageUrls = [input.partnerABase64, input.partnerBBase64]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map(formatBase64);
|
|
62
|
+
|
|
63
|
+
if (imageUrls.length < 2) {
|
|
64
|
+
return { success: false, error: "Two images required" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onProgress?.(10);
|
|
68
|
+
|
|
69
|
+
const enhancedPrompt = `Create a photorealistic image featuring the exact two people from the provided photos. Use the person from @image1 and the person from @image2 exactly as they appear in the reference images - maintain their facial features, expressions, and identity. ${input.prompt}. Professional photography, high quality, detailed, natural lighting, photorealistic rendering.`;
|
|
70
|
+
|
|
71
|
+
const modelInput = {
|
|
72
|
+
image_urls: imageUrls,
|
|
73
|
+
prompt: enhancedPrompt,
|
|
74
|
+
aspect_ratio: "1:1",
|
|
75
|
+
output_format: "jpeg",
|
|
76
|
+
num_images: 1,
|
|
77
|
+
enable_safety_checker: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
81
|
+
console.log("[WizardStrategy] Starting image generation");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let lastStatus = "";
|
|
85
|
+
const result = await provider.subscribe("fal-ai/nano-banana", modelInput, {
|
|
86
|
+
timeoutMs: 120000,
|
|
87
|
+
onQueueUpdate: (status) => {
|
|
88
|
+
if (status.status === lastStatus) return;
|
|
89
|
+
lastStatus = status.status;
|
|
90
|
+
if (status.status === "IN_QUEUE") onProgress?.(20);
|
|
91
|
+
else if (status.status === "IN_PROGRESS") onProgress?.(50);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
onProgress?.(90);
|
|
96
|
+
|
|
97
|
+
const rawResult = result as Record<string, unknown>;
|
|
98
|
+
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
99
|
+
const imageUrl = data?.images?.[0]?.url;
|
|
100
|
+
|
|
101
|
+
onProgress?.(100);
|
|
102
|
+
|
|
103
|
+
if (!imageUrl) {
|
|
104
|
+
return { success: false, error: "No image generated" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: true, imageUrl };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : "Generation failed";
|
|
110
|
+
return { success: false, error: message };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Input Builder
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
async function buildGenerationInput(
|
|
119
|
+
wizardData: Record<string, unknown>,
|
|
120
|
+
scenario: WizardScenarioData,
|
|
121
|
+
): Promise<WizardGenerationInput | null> {
|
|
122
|
+
const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
|
|
123
|
+
const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
|
|
124
|
+
|
|
125
|
+
if (!photo1Key || !photo2Key) {
|
|
126
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
127
|
+
console.error("[WizardStrategy] Missing photos in wizard data", {
|
|
128
|
+
keys: Object.keys(wizardData),
|
|
129
|
+
hasPhoto1: !!photo1Key,
|
|
130
|
+
hasPhoto2: !!photo2Key,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const photo1 = wizardData[photo1Key] as { uri?: string; base64?: string };
|
|
137
|
+
const photo2 = wizardData[photo2Key] as { uri?: string; base64?: string };
|
|
138
|
+
|
|
139
|
+
if (!photo1?.uri || !photo2?.uri) {
|
|
140
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
141
|
+
console.error("[WizardStrategy] Photos missing URI", {
|
|
142
|
+
photo1HasUri: !!photo1?.uri,
|
|
143
|
+
photo2HasUri: !!photo2?.uri,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Convert images to base64
|
|
150
|
+
const [photo1Base64, photo2Base64] = await Promise.all([
|
|
151
|
+
readFileAsBase64(photo1.uri),
|
|
152
|
+
readFileAsBase64(photo2.uri),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
if (!photo1Base64 || !photo2Base64) {
|
|
156
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
157
|
+
console.error("[WizardStrategy] Failed to convert images to base64");
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let prompt = scenario.aiPrompt || `Generate ${scenario.id} scene`;
|
|
163
|
+
const outputType = scenario.outputType || "video";
|
|
164
|
+
|
|
165
|
+
// For image generation, enhance prompt with style selections
|
|
166
|
+
if (outputType === "image") {
|
|
167
|
+
const selections: CoupleFeatureSelection = {};
|
|
168
|
+
|
|
169
|
+
// Romantic mood (multi-select array)
|
|
170
|
+
const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
|
|
171
|
+
if (romanticMoods && romanticMoods.length > 0) {
|
|
172
|
+
selections.romanticMoods = romanticMoods;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Art style (single select)
|
|
176
|
+
const artStyle = wizardData.selection_art_style as string | undefined;
|
|
177
|
+
if (artStyle && artStyle !== "original") {
|
|
178
|
+
selections.artStyle = artStyle;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Artist style (single select)
|
|
182
|
+
const artist = wizardData.selection_artist_style as string | undefined;
|
|
183
|
+
if (artist && artist !== "original") {
|
|
184
|
+
selections.artist = artist;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Enhance prompt with selected styles
|
|
188
|
+
if (Object.keys(selections).length > 0) {
|
|
189
|
+
prompt = enhanceCouplePrompt(prompt, selections);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
partnerABase64: photo1Base64,
|
|
194
|
+
partnerBBase64: photo2Base64,
|
|
195
|
+
prompt,
|
|
196
|
+
} as ImageGenerationInput;
|
|
197
|
+
} else {
|
|
198
|
+
return {
|
|
199
|
+
sourceImageBase64: photo1Base64,
|
|
200
|
+
targetImageBase64: photo2Base64,
|
|
201
|
+
prompt,
|
|
202
|
+
} as VideoGenerationInput;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Video Feature Type Detection
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
function getVideoFeatureType(scenarioId: string): VideoFeatureType {
|
|
211
|
+
const id = scenarioId.toLowerCase();
|
|
212
|
+
|
|
213
|
+
if (id.includes("kiss")) return "ai-kiss";
|
|
214
|
+
if (id.includes("hug")) return "ai-hug";
|
|
215
|
+
|
|
216
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
217
|
+
console.warn(`[WizardStrategy] Unknown scenario type "${scenarioId}", defaulting to ai-hug`);
|
|
218
|
+
}
|
|
219
|
+
return "ai-hug";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Strategy Factory
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
export interface CreateWizardStrategyOptions {
|
|
227
|
+
readonly scenario: WizardScenarioData;
|
|
228
|
+
readonly wizardData: Record<string, unknown>;
|
|
229
|
+
readonly collectionName?: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const createWizardStrategy = (
|
|
233
|
+
options: CreateWizardStrategyOptions,
|
|
234
|
+
): GenerationStrategy<WizardGenerationInput, WizardGenerationResult> => {
|
|
235
|
+
const { scenario, wizardData, collectionName = "creations" } = options;
|
|
236
|
+
const repository = createCreationsRepository(collectionName);
|
|
237
|
+
const outputType = scenario.outputType || "video";
|
|
238
|
+
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
239
|
+
|
|
240
|
+
let lastInputRef: WizardGenerationInput | null = null;
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
execute: async (input, onProgress) => {
|
|
244
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
245
|
+
console.log("[WizardStrategy] Executing generation", {
|
|
246
|
+
scenarioId: scenario.id,
|
|
247
|
+
outputType,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
lastInputRef = input;
|
|
252
|
+
|
|
253
|
+
// Execute based on output type
|
|
254
|
+
if (outputType === "image") {
|
|
255
|
+
const imageInput = input as ImageGenerationInput;
|
|
256
|
+
const result = await executeImageGeneration(imageInput, onProgress);
|
|
257
|
+
|
|
258
|
+
if (!result.success || !result.imageUrl) {
|
|
259
|
+
throw new Error(result.error || "Image generation failed");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { imageUrl: result.imageUrl };
|
|
263
|
+
} else {
|
|
264
|
+
const videoInput = input as VideoGenerationInput;
|
|
265
|
+
const result = await executeVideoFeature(
|
|
266
|
+
videoFeatureType,
|
|
267
|
+
{
|
|
268
|
+
sourceImageBase64: videoInput.sourceImageBase64,
|
|
269
|
+
targetImageBase64: videoInput.targetImageBase64,
|
|
270
|
+
prompt: videoInput.prompt,
|
|
271
|
+
},
|
|
272
|
+
{ onProgress },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (!result.success || !result.videoUrl) {
|
|
276
|
+
throw new Error(result.error || "Video generation failed");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { videoUrl: result.videoUrl };
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
getCreditCost: () => 1,
|
|
284
|
+
|
|
285
|
+
save: async (result, uid) => {
|
|
286
|
+
const input = lastInputRef;
|
|
287
|
+
if (!input) return;
|
|
288
|
+
|
|
289
|
+
// Validate scenario
|
|
290
|
+
if (!scenario || !scenario.id) {
|
|
291
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
292
|
+
console.error("[WizardStrategy] Cannot save: scenario.id is undefined");
|
|
293
|
+
}
|
|
294
|
+
throw new Error("Scenario ID is required for saving creation");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract prompt
|
|
298
|
+
const prompt = 'prompt' in input ? input.prompt : '';
|
|
299
|
+
|
|
300
|
+
// Extract URLs based on output type
|
|
301
|
+
let mainUri = '';
|
|
302
|
+
let imageUrl: string | undefined;
|
|
303
|
+
let videoUrl: string | undefined;
|
|
304
|
+
|
|
305
|
+
if (outputType === "image") {
|
|
306
|
+
const imageResult = result as { imageUrl?: string };
|
|
307
|
+
imageUrl = imageResult.imageUrl;
|
|
308
|
+
mainUri = imageUrl || '';
|
|
309
|
+
} else {
|
|
310
|
+
const videoResult = result as { videoUrl?: string };
|
|
311
|
+
videoUrl = videoResult.videoUrl;
|
|
312
|
+
mainUri = videoUrl || '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Create unique ID
|
|
316
|
+
const creationId = `${scenario.id}_${Date.now()}`;
|
|
317
|
+
|
|
318
|
+
// Build creation object
|
|
319
|
+
const creation = {
|
|
320
|
+
id: creationId,
|
|
321
|
+
uri: mainUri,
|
|
322
|
+
type: scenario.id,
|
|
323
|
+
prompt,
|
|
324
|
+
createdAt: new Date(),
|
|
325
|
+
isShared: false,
|
|
326
|
+
isFavorite: false,
|
|
327
|
+
metadata: {
|
|
328
|
+
scenarioId: scenario.id,
|
|
329
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
330
|
+
},
|
|
331
|
+
output: {
|
|
332
|
+
imageUrl,
|
|
333
|
+
videoUrl,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
338
|
+
console.log("[WizardStrategy] Saving creation", {
|
|
339
|
+
creationId,
|
|
340
|
+
scenarioId: scenario.id,
|
|
341
|
+
outputType,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await repository.create(uid, creation);
|
|
346
|
+
|
|
347
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
348
|
+
console.log("[WizardStrategy] Creation saved successfully");
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Input Builder Helper (Public API)
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
export const buildWizardInput = buildGenerationInput;
|
|
@@ -22,7 +22,7 @@ import { renderStep } from "../../infrastructure/renderers/step-renderer";
|
|
|
22
22
|
import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
|
|
23
23
|
import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
|
|
24
24
|
import { useWizardGeneration, type WizardScenarioData } from "../hooks/useWizardGeneration";
|
|
25
|
-
import type { AlertMessages } from "../../../../presentation/hooks/generation/
|
|
25
|
+
import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
|
|
26
26
|
|
|
27
27
|
export interface GenericWizardFlowProps {
|
|
28
28
|
readonly featureConfig: WizardFeatureConfig;
|
|
@@ -96,9 +96,32 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
96
96
|
[updateProgress],
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
+
// Ensure scenario has required fields - use feature config as fallback
|
|
100
|
+
const validatedScenario = useMemo(() => {
|
|
101
|
+
if (scenario && scenario.id && scenario.aiPrompt !== undefined) {
|
|
102
|
+
return scenario;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fallback to feature config
|
|
106
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
107
|
+
console.warn("[GenericWizardFlow] Scenario missing required fields, using fallback", {
|
|
108
|
+
hasScenario: !!scenario,
|
|
109
|
+
scenarioId: scenario?.id,
|
|
110
|
+
featureConfigId: featureConfig.id,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id: featureConfig.id,
|
|
116
|
+
aiPrompt: "",
|
|
117
|
+
outputType: "image" as const, // Default to image for safety
|
|
118
|
+
title: featureConfig.id,
|
|
119
|
+
};
|
|
120
|
+
}, [scenario, featureConfig.id]);
|
|
121
|
+
|
|
99
122
|
// Generation hook - handles AI generation automatically
|
|
100
123
|
const generationHook = useWizardGeneration({
|
|
101
|
-
scenario:
|
|
124
|
+
scenario: validatedScenario,
|
|
102
125
|
wizardData: customData,
|
|
103
126
|
userId,
|
|
104
127
|
isGeneratingStep: currentStep?.type === StepType.GENERATING,
|
|
@@ -110,7 +133,7 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
110
133
|
});
|
|
111
134
|
|
|
112
135
|
// Track previous step ID to prevent infinite loops
|
|
113
|
-
const prevStepIdRef = useRef<string>();
|
|
136
|
+
const prevStepIdRef = useRef<string | undefined>(undefined);
|
|
114
137
|
|
|
115
138
|
// DEBUG logging
|
|
116
139
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -1,49 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
|
-
*
|
|
4
|
-
* Handles video generation for couple features (ai-hug, ai-kiss, etc.)
|
|
3
|
+
* Wizard generation using orchestrator + strategy factory pattern
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import { useEffect, useRef, useMemo
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
type GenerationStrategy,
|
|
12
|
-
} from "../../../../presentation/hooks/generation";
|
|
13
|
-
import { executeVideoFeature } from "../../../../infrastructure/services/video-feature-executor.service";
|
|
14
|
-
import { createCreationsRepository } from "../../../../domains/creations/infrastructure/adapters";
|
|
15
|
-
import type { VideoFeatureType } from "../../../../domain/interfaces";
|
|
16
|
-
import type { AlertMessages } from "../../../../presentation/hooks/generation/orchestrator.types";
|
|
17
|
-
import { enhanceCouplePrompt } from "../../../../features/couple-future/infrastructure/couplePromptEnhancer";
|
|
18
|
-
import type { CoupleFeatureSelection } from "../../../../features/couple-future/domain/types";
|
|
6
|
+
import { useEffect, useRef, useMemo } from "react";
|
|
7
|
+
import { useGenerationOrchestrator } from "../../../../presentation/hooks/generation";
|
|
8
|
+
import type { AlertMessages } from "../../../../presentation/hooks/generation/types";
|
|
9
|
+
import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
|
|
19
10
|
|
|
20
11
|
declare const __DEV__: boolean;
|
|
21
12
|
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
22
17
|
export type WizardOutputType = "image" | "video";
|
|
23
18
|
|
|
24
19
|
export interface WizardScenarioData {
|
|
25
20
|
readonly id: string;
|
|
26
21
|
readonly aiPrompt: string;
|
|
27
|
-
readonly outputType?: WizardOutputType;
|
|
22
|
+
readonly outputType?: WizardOutputType;
|
|
28
23
|
readonly title?: string;
|
|
29
24
|
readonly description?: string;
|
|
30
25
|
[key: string]: unknown;
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
interface VideoGenerationInput {
|
|
34
|
-
readonly sourceImageBase64: string;
|
|
35
|
-
readonly targetImageBase64: string;
|
|
36
|
-
readonly prompt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ImageGenerationInput {
|
|
40
|
-
readonly partnerABase64: string;
|
|
41
|
-
readonly partnerBBase64: string;
|
|
42
|
-
readonly prompt: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type GenerationInput = VideoGenerationInput | ImageGenerationInput;
|
|
46
|
-
|
|
47
28
|
export interface UseWizardGenerationProps {
|
|
48
29
|
readonly scenario: WizardScenarioData;
|
|
49
30
|
readonly wizardData: Record<string, unknown>;
|
|
@@ -61,183 +42,9 @@ export interface UseWizardGenerationReturn {
|
|
|
61
42
|
readonly progress: number;
|
|
62
43
|
}
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (id.includes("kiss")) return "ai-kiss";
|
|
68
|
-
if (id.includes("hug")) return "ai-hug";
|
|
69
|
-
|
|
70
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
71
|
-
console.warn(`[useWizardGeneration] Unknown scenario type "${scenarioId}", defaulting to ai-hug`);
|
|
72
|
-
}
|
|
73
|
-
return "ai-hug";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function executeImageGeneration(
|
|
77
|
-
input: ImageGenerationInput,
|
|
78
|
-
onProgress?: (progress: number) => void,
|
|
79
|
-
): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
|
80
|
-
const { providerRegistry } = await import("../../../../infrastructure/services/provider-registry.service");
|
|
81
|
-
|
|
82
|
-
const provider = providerRegistry.getActiveProvider();
|
|
83
|
-
if (!provider || !provider.isInitialized()) {
|
|
84
|
-
return { success: false, error: "AI provider not initialized" };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
onProgress?.(5);
|
|
89
|
-
|
|
90
|
-
const formatBase64 = (base64: string): string => {
|
|
91
|
-
return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const imageUrls = [input.partnerABase64, input.partnerBBase64]
|
|
95
|
-
.filter(Boolean)
|
|
96
|
-
.map(formatBase64);
|
|
97
|
-
|
|
98
|
-
if (imageUrls.length < 2) {
|
|
99
|
-
return { success: false, error: "Two images required" };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
onProgress?.(10);
|
|
103
|
-
|
|
104
|
-
const enhancedPrompt = `A photorealistic image of a couple. The first person @image1 and the second person @image2. ${input.prompt}. High quality, detailed, professional photography.`;
|
|
105
|
-
|
|
106
|
-
const modelInput = {
|
|
107
|
-
image_urls: imageUrls,
|
|
108
|
-
prompt: enhancedPrompt,
|
|
109
|
-
aspect_ratio: "1:1",
|
|
110
|
-
output_format: "jpeg",
|
|
111
|
-
num_images: 1,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
-
console.log("[useWizardGeneration] Starting image generation");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let lastStatus = "";
|
|
119
|
-
const result = await provider.subscribe("fal-ai/nano-banana", modelInput, {
|
|
120
|
-
timeoutMs: 120000,
|
|
121
|
-
onQueueUpdate: (status) => {
|
|
122
|
-
if (status.status === lastStatus) return;
|
|
123
|
-
lastStatus = status.status;
|
|
124
|
-
if (status.status === "IN_QUEUE") onProgress?.(20);
|
|
125
|
-
else if (status.status === "IN_PROGRESS") onProgress?.(50);
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
onProgress?.(90);
|
|
130
|
-
|
|
131
|
-
const rawResult = result as Record<string, unknown>;
|
|
132
|
-
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
133
|
-
const imageUrl = data?.images?.[0]?.url;
|
|
134
|
-
|
|
135
|
-
onProgress?.(100);
|
|
136
|
-
|
|
137
|
-
if (!imageUrl) {
|
|
138
|
-
return { success: false, error: "No image generated" };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { success: true, imageUrl };
|
|
142
|
-
} catch (error) {
|
|
143
|
-
const message = error instanceof Error ? error.message : "Generation failed";
|
|
144
|
-
return { success: false, error: message };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
async function buildGenerationInput(
|
|
150
|
-
wizardData: Record<string, unknown>,
|
|
151
|
-
scenario: WizardScenarioData,
|
|
152
|
-
): Promise<GenerationInput | null> {
|
|
153
|
-
const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
|
|
154
|
-
const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
|
|
155
|
-
|
|
156
|
-
if (!photo1Key || !photo2Key) {
|
|
157
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
158
|
-
console.error("[useWizardGeneration] Missing photos in wizard data", {
|
|
159
|
-
keys: Object.keys(wizardData),
|
|
160
|
-
hasPhoto1: !!photo1Key,
|
|
161
|
-
hasPhoto2: !!photo2Key,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const photo1 = wizardData[photo1Key] as { uri?: string; base64?: string };
|
|
168
|
-
const photo2 = wizardData[photo2Key] as { uri?: string; base64?: string };
|
|
169
|
-
|
|
170
|
-
if (!photo1?.uri || !photo2?.uri) {
|
|
171
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
172
|
-
console.error("[useWizardGeneration] Photos missing URI", {
|
|
173
|
-
photo1HasUri: !!photo1?.uri,
|
|
174
|
-
photo2HasUri: !!photo2?.uri,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Convert images to base64 using design system utility
|
|
181
|
-
const [photo1Base64, photo2Base64] = await Promise.all([
|
|
182
|
-
readFileAsBase64(photo1.uri),
|
|
183
|
-
readFileAsBase64(photo2.uri),
|
|
184
|
-
]);
|
|
185
|
-
|
|
186
|
-
// Validate base64 conversion results
|
|
187
|
-
if (!photo1Base64 || !photo2Base64) {
|
|
188
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
189
|
-
console.error("[useWizardGeneration] Failed to convert images to base64", {
|
|
190
|
-
photo1Success: !!photo1Base64,
|
|
191
|
-
photo2Success: !!photo2Base64,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let prompt = scenario.aiPrompt || `Generate ${scenario.id} scene`;
|
|
198
|
-
const outputType = scenario.outputType || "video"; // Default to video for backward compatibility
|
|
199
|
-
|
|
200
|
-
// For image generation, enhance prompt with style selections
|
|
201
|
-
if (outputType === "image") {
|
|
202
|
-
// Extract style selections from wizard data
|
|
203
|
-
const selections: CoupleFeatureSelection = {};
|
|
204
|
-
|
|
205
|
-
// Romantic mood (multi-select array)
|
|
206
|
-
const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
|
|
207
|
-
if (romanticMoods && romanticMoods.length > 0) {
|
|
208
|
-
selections.romanticMoods = romanticMoods;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Art style (single select)
|
|
212
|
-
const artStyle = wizardData.selection_art_style as string | undefined;
|
|
213
|
-
if (artStyle && artStyle !== "original") {
|
|
214
|
-
selections.artStyle = artStyle;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Artist style (single select)
|
|
218
|
-
const artist = wizardData.selection_artist_style as string | undefined;
|
|
219
|
-
if (artist && artist !== "original") {
|
|
220
|
-
selections.artist = artist;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Enhance prompt with selected styles (only if not "original")
|
|
224
|
-
if (Object.keys(selections).length > 0) {
|
|
225
|
-
prompt = enhanceCouplePrompt(prompt, selections);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
partnerABase64: photo1Base64,
|
|
230
|
-
partnerBBase64: photo2Base64,
|
|
231
|
-
prompt,
|
|
232
|
-
} as ImageGenerationInput;
|
|
233
|
-
} else {
|
|
234
|
-
return {
|
|
235
|
-
sourceImageBase64: photo1Base64,
|
|
236
|
-
targetImageBase64: photo2Base64,
|
|
237
|
-
prompt,
|
|
238
|
-
} as VideoGenerationInput;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Hook
|
|
47
|
+
// ============================================================================
|
|
241
48
|
|
|
242
49
|
export const useWizardGeneration = (
|
|
243
50
|
props: UseWizardGenerationProps,
|
|
@@ -255,119 +62,63 @@ export const useWizardGeneration = (
|
|
|
255
62
|
} = props;
|
|
256
63
|
|
|
257
64
|
const hasStarted = useRef(false);
|
|
258
|
-
const lastInputRef = useRef<GenerationInput | null>(null);
|
|
259
|
-
const repository = useMemo(() => createCreationsRepository("creations"), []);
|
|
260
|
-
const videoFeatureType = useMemo(() => getVideoFeatureType(scenario.id), [scenario.id]);
|
|
261
|
-
const outputType = scenario.outputType || "video";
|
|
262
|
-
|
|
263
|
-
type GenerationResult = { videoUrl: string } | { imageUrl: string };
|
|
264
|
-
|
|
265
|
-
const strategy: GenerationStrategy<GenerationInput, GenerationResult> = useMemo(
|
|
266
|
-
() => ({
|
|
267
|
-
execute: async (input, onProgress) => {
|
|
268
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
269
|
-
console.log("[useWizardGeneration] Executing generation", {
|
|
270
|
-
scenarioId: scenario.id,
|
|
271
|
-
outputType,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
lastInputRef.current = input;
|
|
276
|
-
|
|
277
|
-
// Execute based on output type
|
|
278
|
-
if (outputType === "image") {
|
|
279
|
-
const imageInput = input as ImageGenerationInput;
|
|
280
|
-
const result = await executeImageGeneration(imageInput, onProgress);
|
|
281
|
-
|
|
282
|
-
if (!result.success || !result.imageUrl) {
|
|
283
|
-
throw new Error(result.error || "Image generation failed");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return { imageUrl: result.imageUrl };
|
|
287
|
-
} else {
|
|
288
|
-
const videoInput = input as VideoGenerationInput;
|
|
289
|
-
const result = await executeVideoFeature(
|
|
290
|
-
videoFeatureType,
|
|
291
|
-
{
|
|
292
|
-
sourceImageBase64: videoInput.sourceImageBase64,
|
|
293
|
-
targetImageBase64: videoInput.targetImageBase64,
|
|
294
|
-
prompt: videoInput.prompt,
|
|
295
|
-
},
|
|
296
|
-
{ onProgress },
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
if (!result.success || !result.videoUrl) {
|
|
300
|
-
throw new Error(result.error || "Video generation failed");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return { videoUrl: result.videoUrl };
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
getCreditCost: () => 1,
|
|
307
|
-
save: async (result, uid) => {
|
|
308
|
-
const input = lastInputRef.current;
|
|
309
|
-
if (!input) return;
|
|
310
|
-
|
|
311
|
-
// Both input types have prompt field
|
|
312
|
-
const prompt = 'prompt' in input ? input.prompt : '';
|
|
313
|
-
|
|
314
|
-
// Build creation object with only the relevant URL field
|
|
315
|
-
const creation: any = {
|
|
316
|
-
scenarioId: scenario.id,
|
|
317
|
-
scenarioTitle: scenario.title || scenario.id,
|
|
318
|
-
prompt,
|
|
319
|
-
createdAt: Date.now(),
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// Add only the relevant URL based on output type
|
|
323
|
-
if (outputType === "image") {
|
|
324
|
-
const imageResult = result as { imageUrl?: string };
|
|
325
|
-
creation.imageUrl = imageResult.imageUrl;
|
|
326
|
-
} else {
|
|
327
|
-
const videoResult = result as { videoUrl?: string };
|
|
328
|
-
creation.videoUrl = videoResult.videoUrl;
|
|
329
|
-
}
|
|
330
65
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
66
|
+
// Log output type on mount
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
|
+
console.log("[useWizardGeneration] Initialized", {
|
|
70
|
+
scenarioId: scenario.id,
|
|
71
|
+
outputType: scenario.outputType || "video",
|
|
72
|
+
hasOutputType: !!scenario.outputType,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}, [scenario.id, scenario.outputType]);
|
|
76
|
+
|
|
77
|
+
// Create strategy using factory
|
|
78
|
+
const strategy = useMemo(() => {
|
|
79
|
+
return createWizardStrategy({
|
|
80
|
+
scenario,
|
|
81
|
+
wizardData,
|
|
82
|
+
collectionName: "creations",
|
|
83
|
+
});
|
|
84
|
+
}, [scenario, wizardData]);
|
|
85
|
+
|
|
86
|
+
// Use orchestrator with strategy
|
|
87
|
+
const { generate, isGenerating, progress } = useGenerationOrchestrator(
|
|
88
|
+
strategy,
|
|
89
|
+
{
|
|
90
|
+
userId,
|
|
91
|
+
alertMessages: alertMessages || {
|
|
92
|
+
networkError: "No internet connection",
|
|
93
|
+
policyViolation: "Content policy violation",
|
|
94
|
+
saveFailed: "Failed to save",
|
|
95
|
+
creditFailed: "Failed to deduct credits",
|
|
96
|
+
unknown: "An error occurred",
|
|
336
97
|
},
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const { generate, isGenerating, progress } = useGenerationOrchestrator(strategy, {
|
|
342
|
-
userId,
|
|
343
|
-
alertMessages,
|
|
344
|
-
onCreditsExhausted,
|
|
345
|
-
onSuccess: useCallback(
|
|
346
|
-
(result) => {
|
|
98
|
+
onCreditsExhausted,
|
|
99
|
+
onSuccess: (result) => {
|
|
347
100
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
348
101
|
console.log("[useWizardGeneration] Success");
|
|
349
102
|
}
|
|
350
103
|
onSuccess?.(result);
|
|
351
104
|
},
|
|
352
|
-
|
|
353
|
-
),
|
|
354
|
-
onError: useCallback(
|
|
355
|
-
(err) => {
|
|
105
|
+
onError: (err) => {
|
|
356
106
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
357
107
|
console.log("[useWizardGeneration] Error:", err.message);
|
|
358
108
|
}
|
|
359
109
|
onError?.(err.message);
|
|
360
110
|
},
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
});
|
|
111
|
+
},
|
|
112
|
+
);
|
|
364
113
|
|
|
114
|
+
// Sync progress to parent
|
|
365
115
|
useEffect(() => {
|
|
366
116
|
if (onProgressChange) {
|
|
367
117
|
onProgressChange(progress);
|
|
368
118
|
}
|
|
369
119
|
}, [progress, onProgressChange]);
|
|
370
120
|
|
|
121
|
+
// Auto-start generation when entering generating step
|
|
371
122
|
useEffect(() => {
|
|
372
123
|
if (isGeneratingStep && !hasStarted.current && !isGenerating) {
|
|
373
124
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -377,7 +128,8 @@ export const useWizardGeneration = (
|
|
|
377
128
|
});
|
|
378
129
|
}
|
|
379
130
|
|
|
380
|
-
|
|
131
|
+
// Build input and start generation
|
|
132
|
+
buildWizardInput(wizardData, scenario)
|
|
381
133
|
.then((input) => {
|
|
382
134
|
if (!input) {
|
|
383
135
|
const error = "Failed to build generation input";
|
|
@@ -399,6 +151,7 @@ export const useWizardGeneration = (
|
|
|
399
151
|
});
|
|
400
152
|
}
|
|
401
153
|
|
|
154
|
+
// Reset hasStarted when leaving generating step
|
|
402
155
|
if (!isGeneratingStep && hasStarted.current) {
|
|
403
156
|
hasStarted.current = false;
|
|
404
157
|
}
|