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