@umituz/react-native-ai-generation-content 1.37.12 → 1.37.14
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/image-generation.executor.ts +5 -21
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +55 -17
- package/src/domains/generation/wizard/infrastructure/strategies/shared/unified-prompt-builder.ts +46 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +74 -83
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +30 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +4 -1
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +28 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.37.
|
|
3
|
+
"version": "1.37.14",
|
|
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/image-generation.executor.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* Handles the actual image generation execution
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { buildInteractionStylePrompt } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
|
|
6
|
+
import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
|
|
8
7
|
import type { WizardImageInput } from "./image-generation.types";
|
|
9
8
|
import {
|
|
10
9
|
GENERATION_TIMEOUT_MS,
|
|
@@ -21,31 +20,19 @@ interface ExecutionResult {
|
|
|
21
20
|
error?: string;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* Formats base64 string with proper data URI prefix
|
|
26
|
-
*/
|
|
27
23
|
function formatBase64(base64: string): string {
|
|
28
24
|
return base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
/**
|
|
32
|
-
* Builds the final prompt based on input type (photo-based or text-to-image)
|
|
33
|
-
*/
|
|
34
27
|
function buildFinalPrompt(input: WizardImageInput, imageUrls: string[]): string {
|
|
35
28
|
const hasPhotos = imageUrls.length > 0;
|
|
36
29
|
|
|
37
30
|
if (hasPhotos) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
return buildUnifiedPrompt({
|
|
32
|
+
basePrompt: input.prompt,
|
|
33
|
+
photoCount: imageUrls.length,
|
|
34
|
+
interactionStyle: input.interactionStyle,
|
|
41
35
|
});
|
|
42
|
-
|
|
43
|
-
const interactionPrompt = buildInteractionStylePrompt({
|
|
44
|
-
style: input.interactionStyle ?? "romantic",
|
|
45
|
-
personCount: imageUrls.length,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return interactionPrompt ? `${facePrompt}\n\n${interactionPrompt}` : facePrompt;
|
|
49
36
|
}
|
|
50
37
|
|
|
51
38
|
// Text-to-image with optional style
|
|
@@ -56,9 +43,6 @@ function buildFinalPrompt(input: WizardImageInput, imageUrls: string[]): string
|
|
|
56
43
|
return input.prompt;
|
|
57
44
|
}
|
|
58
45
|
|
|
59
|
-
/**
|
|
60
|
-
* Executes image generation using the AI provider
|
|
61
|
-
*/
|
|
62
46
|
export async function executeImageGeneration(
|
|
63
47
|
input: WizardImageInput,
|
|
64
48
|
model: string,
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts
CHANGED
|
@@ -80,11 +80,14 @@ function applyStyleEnhancements(prompt: string, wizardData: Record<string, unkno
|
|
|
80
80
|
// Strategy Factory
|
|
81
81
|
// ============================================================================
|
|
82
82
|
|
|
83
|
+
declare const __DEV__: boolean;
|
|
84
|
+
|
|
83
85
|
export function createImageStrategy(options: CreateImageStrategyOptions): WizardStrategy {
|
|
84
86
|
const { scenario, collectionName = "creations" } = options;
|
|
85
87
|
const repository = createCreationsRepository(collectionName);
|
|
86
88
|
|
|
87
89
|
let lastInputRef: WizardImageInput | null = null;
|
|
90
|
+
let processingCreationId: string | null = null;
|
|
88
91
|
|
|
89
92
|
return {
|
|
90
93
|
execute: async (input: unknown) => {
|
|
@@ -106,28 +109,63 @@ export function createImageStrategy(options: CreateImageStrategyOptions): Wizard
|
|
|
106
109
|
|
|
107
110
|
getCreditCost: () => 1,
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
112
|
+
saveAsProcessing: async (uid: string, input: unknown) => {
|
|
113
|
+
const imageInput = input as WizardImageInput;
|
|
114
|
+
const creationId = `${scenario.id}_${Date.now()}`;
|
|
115
|
+
processingCreationId = creationId;
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
id:
|
|
116
|
-
uri:
|
|
117
|
+
await repository.create(uid, {
|
|
118
|
+
id: creationId,
|
|
119
|
+
uri: "",
|
|
117
120
|
type: scenario.id,
|
|
118
|
-
prompt:
|
|
119
|
-
status: "
|
|
121
|
+
prompt: imageInput.prompt,
|
|
122
|
+
status: "processing" as const,
|
|
120
123
|
createdAt: new Date(),
|
|
121
124
|
isShared: false,
|
|
122
125
|
isFavorite: false,
|
|
123
|
-
metadata: {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
metadata: { scenarioId: scenario.id, scenarioTitle: scenario.title },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
130
|
+
console.log("[ImageStrategy] Saved as processing", { creationId });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return creationId;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
save: async (result: unknown, uid: string, creationId?: string) => {
|
|
137
|
+
const input = lastInputRef;
|
|
138
|
+
const imageResult = result as { imageUrl?: string };
|
|
139
|
+
if (!input || !scenario?.id || !imageResult.imageUrl) return;
|
|
140
|
+
|
|
141
|
+
const idToUpdate = creationId || processingCreationId;
|
|
142
|
+
|
|
143
|
+
if (idToUpdate) {
|
|
144
|
+
// Update existing processing creation to completed
|
|
145
|
+
await repository.update(uid, idToUpdate, {
|
|
146
|
+
uri: imageResult.imageUrl,
|
|
147
|
+
status: "completed" as const,
|
|
148
|
+
output: { imageUrl: imageResult.imageUrl },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
152
|
+
console.log("[ImageStrategy] Updated to completed", { creationId: idToUpdate });
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Fallback: create new (shouldn't happen normally)
|
|
156
|
+
await repository.create(uid, {
|
|
157
|
+
id: `${scenario.id}_${Date.now()}`,
|
|
158
|
+
uri: imageResult.imageUrl,
|
|
159
|
+
type: scenario.id,
|
|
160
|
+
prompt: input.prompt,
|
|
161
|
+
status: "completed" as const,
|
|
162
|
+
createdAt: new Date(),
|
|
163
|
+
isShared: false,
|
|
164
|
+
isFavorite: false,
|
|
165
|
+
metadata: { scenarioId: scenario.id, scenarioTitle: scenario.title },
|
|
166
|
+
output: { imageUrl: imageResult.imageUrl },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
131
169
|
},
|
|
132
170
|
};
|
|
133
171
|
}
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/unified-prompt-builder.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Prompt Builder
|
|
3
|
+
* Single prompt building logic for ALL generation types (image & video)
|
|
4
|
+
* Uses MultiPersonPromptStructure for photo-based scenarios
|
|
5
|
+
* Uses createEnhancedPrompt for text-only scenarios
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createMultiPersonPrompt } from "../../../../../prompts/domain/entities/MultiPersonPromptStructure";
|
|
9
|
+
import { createEnhancedPrompt } from "../../../../../prompts/domain/entities/BasePromptStructure";
|
|
10
|
+
|
|
11
|
+
export interface BuildPromptOptions {
|
|
12
|
+
/** Base scenario prompt (aiPrompt from scenario config) */
|
|
13
|
+
readonly basePrompt: string;
|
|
14
|
+
/** Number of photos/people in generation */
|
|
15
|
+
readonly photoCount: number;
|
|
16
|
+
/** Interaction style from scenario (optional - only if scenario specifies it) */
|
|
17
|
+
readonly interactionStyle?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build unified prompt for any generation type
|
|
22
|
+
* - Photo-based: Uses createMultiPersonPrompt with @image1, @image2 references
|
|
23
|
+
* - Text-only: Uses createEnhancedPrompt with identity preservation
|
|
24
|
+
*/
|
|
25
|
+
export function buildUnifiedPrompt(options: BuildPromptOptions): string {
|
|
26
|
+
const { basePrompt, photoCount, interactionStyle } = options;
|
|
27
|
+
|
|
28
|
+
// Text-only generation (no photos)
|
|
29
|
+
if (photoCount === 0) {
|
|
30
|
+
return createEnhancedPrompt(basePrompt, {
|
|
31
|
+
includeIdentityPreservation: false,
|
|
32
|
+
includePhotoRealism: true,
|
|
33
|
+
includePoseGuidelines: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Photo-based generation - use multi-person prompt with @imageN references
|
|
38
|
+
let finalPrompt = createMultiPersonPrompt(basePrompt, photoCount);
|
|
39
|
+
|
|
40
|
+
// Add interaction style if specified by scenario (no defaults)
|
|
41
|
+
if (interactionStyle) {
|
|
42
|
+
finalPrompt = `${finalPrompt}\n\nINTERACTION STYLE: ${interactionStyle}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return finalPrompt;
|
|
46
|
+
}
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -5,54 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
|
|
7
7
|
import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
|
|
8
|
+
import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
|
|
8
9
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
9
|
-
import type { ScenarioInputType } from "../../../../scenarios/domain/Scenario";
|
|
10
10
|
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
11
11
|
import { VIDEO_PROCESSING_PROMPTS } from "./wizard-strategy.constants";
|
|
12
12
|
import { extractPrompt, extractDuration, extractAspectRatio, extractResolution } from "../utils";
|
|
13
13
|
import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
|
|
14
14
|
import { getVideoFeatureType } from "./video-generation.utils";
|
|
15
15
|
import type { WizardVideoInput, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
16
|
+
import { validatePhotoCount } from "./video-generation.types";
|
|
16
17
|
|
|
17
18
|
declare const __DEV__: boolean;
|
|
18
19
|
|
|
19
20
|
export type { WizardVideoInput, WizardVideoResult, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
20
21
|
|
|
21
|
-
interface PhotoValidationResult {
|
|
22
|
-
isValid: boolean;
|
|
23
|
-
errorKey?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function validatePhotoCount(
|
|
27
|
-
photoCount: number,
|
|
28
|
-
inputType: ScenarioInputType | undefined,
|
|
29
|
-
): PhotoValidationResult {
|
|
30
|
-
const effectiveInputType = inputType ?? "single";
|
|
31
|
-
|
|
32
|
-
switch (effectiveInputType) {
|
|
33
|
-
case "dual":
|
|
34
|
-
if (photoCount < 2) {
|
|
35
|
-
return {
|
|
36
|
-
isValid: false,
|
|
37
|
-
errorKey: "error.generation.dualPhotosRequired",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
break;
|
|
41
|
-
case "single":
|
|
42
|
-
if (photoCount < 1) {
|
|
43
|
-
return {
|
|
44
|
-
isValid: false,
|
|
45
|
-
errorKey: "error.generation.photoRequired",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
break;
|
|
49
|
-
case "text":
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { isValid: true };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
22
|
export async function buildVideoInput(
|
|
57
23
|
wizardData: Record<string, unknown>,
|
|
58
24
|
scenario: WizardScenarioData,
|
|
@@ -65,62 +31,52 @@ export async function buildVideoInput(
|
|
|
65
31
|
|
|
66
32
|
const validation = validatePhotoCount(photos.length, scenario.inputType);
|
|
67
33
|
if (!validation.isValid) {
|
|
68
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
|
-
console.log("[VideoStrategy] Validation failed", {
|
|
70
|
-
scenarioId: scenario.id,
|
|
71
|
-
inputType: scenario.inputType,
|
|
72
|
-
photoCount: photos.length,
|
|
73
|
-
errorKey: validation.errorKey,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
34
|
throw new Error(validation.errorKey ?? "error.generation.invalidInput");
|
|
77
35
|
}
|
|
78
36
|
|
|
79
|
-
let
|
|
37
|
+
let basePrompt = extractPrompt(wizardData, scenario.aiPrompt);
|
|
80
38
|
|
|
81
|
-
if (!
|
|
39
|
+
if (!basePrompt) {
|
|
82
40
|
const defaultPrompt = VIDEO_PROCESSING_PROMPTS[scenario.id];
|
|
83
41
|
if (defaultPrompt) {
|
|
84
|
-
|
|
42
|
+
basePrompt = defaultPrompt;
|
|
85
43
|
} else {
|
|
86
44
|
throw new Error("error.generation.promptRequired");
|
|
87
45
|
}
|
|
88
46
|
}
|
|
89
47
|
|
|
90
|
-
|
|
48
|
+
// Build unified prompt with face preservation
|
|
49
|
+
const finalPrompt = buildUnifiedPrompt({
|
|
50
|
+
basePrompt,
|
|
51
|
+
photoCount: photos.length,
|
|
52
|
+
interactionStyle: scenario.interactionStyle as string | undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
+
console.log("[VideoStrategy] Prompt built", {
|
|
57
|
+
baseLength: basePrompt.length,
|
|
58
|
+
finalLength: finalPrompt.length,
|
|
59
|
+
photoCount: photos.length,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
91
64
|
sourceImageBase64: photos[0],
|
|
92
65
|
targetImageBase64: photos[1] || photos[0],
|
|
93
|
-
prompt,
|
|
66
|
+
prompt: finalPrompt,
|
|
94
67
|
duration: extractDuration(wizardData),
|
|
95
68
|
aspectRatio: extractAspectRatio(wizardData),
|
|
96
69
|
resolution: extractResolution(wizardData),
|
|
97
70
|
};
|
|
98
|
-
|
|
99
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
|
-
console.log("[VideoStrategy] Input built", {
|
|
101
|
-
hasSource: !!input.sourceImageBase64,
|
|
102
|
-
hasTarget: !!input.targetImageBase64,
|
|
103
|
-
duration: input.duration,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return input;
|
|
108
71
|
}
|
|
109
72
|
|
|
110
|
-
// ============================================================================
|
|
111
|
-
// Strategy Factory
|
|
112
|
-
// ============================================================================
|
|
113
|
-
|
|
114
73
|
export function createVideoStrategy(options: CreateVideoStrategyOptions): WizardStrategy {
|
|
115
74
|
const { scenario, collectionName = "creations" } = options;
|
|
116
75
|
const repository = createCreationsRepository(collectionName);
|
|
117
76
|
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
118
77
|
|
|
119
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
-
console.log("[VideoStrategy] Created", { scenarioId: scenario.id, videoFeatureType });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
78
|
let lastInputRef: WizardVideoInput | null = null;
|
|
79
|
+
let processingCreationId: string | null = null;
|
|
124
80
|
|
|
125
81
|
return {
|
|
126
82
|
execute: async (input: unknown) => {
|
|
@@ -147,28 +103,63 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
147
103
|
|
|
148
104
|
getCreditCost: () => 1,
|
|
149
105
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
106
|
+
saveAsProcessing: async (uid: string, input: unknown) => {
|
|
107
|
+
const videoInput = input as WizardVideoInput;
|
|
108
|
+
const creationId = `${scenario.id}_${Date.now()}`;
|
|
109
|
+
processingCreationId = creationId;
|
|
154
110
|
|
|
155
|
-
|
|
156
|
-
id:
|
|
157
|
-
uri:
|
|
111
|
+
await repository.create(uid, {
|
|
112
|
+
id: creationId,
|
|
113
|
+
uri: "",
|
|
158
114
|
type: scenario.id,
|
|
159
|
-
prompt:
|
|
160
|
-
status: "
|
|
115
|
+
prompt: videoInput.prompt,
|
|
116
|
+
status: "processing" as const,
|
|
161
117
|
createdAt: new Date(),
|
|
162
118
|
isShared: false,
|
|
163
119
|
isFavorite: false,
|
|
164
|
-
metadata: {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
120
|
+
metadata: { scenarioId: scenario.id, scenarioTitle: scenario.title },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
124
|
+
console.log("[VideoStrategy] Saved as processing", { creationId });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return creationId;
|
|
128
|
+
},
|
|
170
129
|
|
|
171
|
-
|
|
130
|
+
save: async (result: unknown, uid: string, creationId?: string) => {
|
|
131
|
+
const input = lastInputRef;
|
|
132
|
+
const videoResult = result as { videoUrl?: string };
|
|
133
|
+
if (!input || !scenario?.id || !videoResult.videoUrl) return;
|
|
134
|
+
|
|
135
|
+
const idToUpdate = creationId || processingCreationId;
|
|
136
|
+
|
|
137
|
+
if (idToUpdate) {
|
|
138
|
+
// Update existing processing creation to completed
|
|
139
|
+
await repository.update(uid, idToUpdate, {
|
|
140
|
+
uri: videoResult.videoUrl,
|
|
141
|
+
status: "completed" as const,
|
|
142
|
+
output: { videoUrl: videoResult.videoUrl },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
146
|
+
console.log("[VideoStrategy] Updated to completed", { creationId: idToUpdate });
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// Fallback: create new (shouldn't happen normally)
|
|
150
|
+
await repository.create(uid, {
|
|
151
|
+
id: `${scenario.id}_${Date.now()}`,
|
|
152
|
+
uri: videoResult.videoUrl,
|
|
153
|
+
type: scenario.id,
|
|
154
|
+
prompt: input.prompt,
|
|
155
|
+
status: "completed" as const,
|
|
156
|
+
createdAt: new Date(),
|
|
157
|
+
isShared: false,
|
|
158
|
+
isFavorite: false,
|
|
159
|
+
metadata: { scenarioId: scenario.id, scenarioTitle: scenario.title },
|
|
160
|
+
output: { videoUrl: videoResult.videoUrl },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
172
163
|
},
|
|
173
164
|
};
|
|
174
165
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
7
|
+
import type { ScenarioInputType } from "../../../../scenarios/domain/Scenario";
|
|
7
8
|
|
|
8
9
|
export interface WizardVideoInput {
|
|
9
10
|
/** Source image (optional for text-to-video) */
|
|
@@ -27,3 +28,32 @@ export interface CreateVideoStrategyOptions {
|
|
|
27
28
|
readonly scenario: WizardScenarioData;
|
|
28
29
|
readonly collectionName?: string;
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
export interface PhotoValidationResult {
|
|
33
|
+
isValid: boolean;
|
|
34
|
+
errorKey?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validatePhotoCount(
|
|
38
|
+
photoCount: number,
|
|
39
|
+
inputType: ScenarioInputType | undefined,
|
|
40
|
+
): PhotoValidationResult {
|
|
41
|
+
const effectiveInputType = inputType ?? "single";
|
|
42
|
+
|
|
43
|
+
switch (effectiveInputType) {
|
|
44
|
+
case "dual":
|
|
45
|
+
if (photoCount < 2) {
|
|
46
|
+
return { isValid: false, errorKey: "error.generation.dualPhotosRequired" };
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case "single":
|
|
50
|
+
if (photoCount < 1) {
|
|
51
|
+
return { isValid: false, errorKey: "error.generation.photoRequired" };
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "text":
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { isValid: true };
|
|
59
|
+
}
|
|
@@ -6,5 +6,8 @@
|
|
|
6
6
|
export interface WizardStrategy {
|
|
7
7
|
execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string }>;
|
|
8
8
|
getCreditCost: () => number;
|
|
9
|
-
|
|
9
|
+
/** Save as processing when generation starts - returns creation ID */
|
|
10
|
+
saveAsProcessing?: (userId: string, input: unknown) => Promise<string>;
|
|
11
|
+
/** Update to completed when generation finishes */
|
|
12
|
+
save?: (result: unknown, userId: string, creationId?: string) => Promise<void>;
|
|
10
13
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useWizardGeneration Hook
|
|
3
3
|
* Wizard generation using orchestrator + strategy factory pattern
|
|
4
|
-
*
|
|
4
|
+
* Saves to Firestore with status="processing" at start for gallery display
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useEffect, useRef, useMemo, useCallback } from "react";
|
|
8
8
|
import { useGenerationOrchestrator } from "../../../../../presentation/hooks/generation";
|
|
9
|
-
import { usePendingJobs } from "../../../../../presentation/hooks/use-pending-jobs";
|
|
10
9
|
import { createWizardStrategy, buildWizardInput } from "../../infrastructure/strategies";
|
|
11
10
|
import type {
|
|
12
11
|
UseWizardGenerationProps,
|
|
@@ -34,23 +33,19 @@ export const useWizardGeneration = (
|
|
|
34
33
|
onSuccess,
|
|
35
34
|
onError,
|
|
36
35
|
onCreditsExhausted,
|
|
37
|
-
trackAsBackgroundJob = true,
|
|
38
36
|
} = props;
|
|
39
37
|
|
|
40
38
|
const hasStarted = useRef(false);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const { addJob, updateJob, removeJob } = usePendingJobs();
|
|
39
|
+
const currentCreationIdRef = useRef<string | null>(null);
|
|
44
40
|
|
|
45
41
|
useEffect(() => {
|
|
46
42
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
43
|
console.log("[useWizardGeneration] Initialized", {
|
|
48
44
|
scenarioId: scenario.id,
|
|
49
45
|
outputType: scenario.outputType,
|
|
50
|
-
trackAsBackgroundJob,
|
|
51
46
|
});
|
|
52
47
|
}
|
|
53
|
-
}, [scenario.id, scenario.outputType
|
|
48
|
+
}, [scenario.id, scenario.outputType]);
|
|
54
49
|
|
|
55
50
|
const strategy = useMemo(() => {
|
|
56
51
|
return createWizardStrategy({
|
|
@@ -62,17 +57,14 @@ export const useWizardGeneration = (
|
|
|
62
57
|
const handleSuccess = useCallback(
|
|
63
58
|
(result: unknown) => {
|
|
64
59
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
65
|
-
console.log("[useWizardGeneration] Success"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (trackAsBackgroundJob && currentJobIdRef.current) {
|
|
69
|
-
removeJob(currentJobIdRef.current);
|
|
70
|
-
currentJobIdRef.current = null;
|
|
60
|
+
console.log("[useWizardGeneration] Success", {
|
|
61
|
+
creationId: currentCreationIdRef.current,
|
|
62
|
+
});
|
|
71
63
|
}
|
|
72
|
-
|
|
64
|
+
currentCreationIdRef.current = null;
|
|
73
65
|
onSuccess?.(result);
|
|
74
66
|
},
|
|
75
|
-
[
|
|
67
|
+
[onSuccess],
|
|
76
68
|
);
|
|
77
69
|
|
|
78
70
|
const handleError = useCallback(
|
|
@@ -80,17 +72,11 @@ export const useWizardGeneration = (
|
|
|
80
72
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
81
73
|
console.log("[useWizardGeneration] Error:", err.message);
|
|
82
74
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
updateJob({
|
|
86
|
-
id: currentJobIdRef.current,
|
|
87
|
-
updates: { status: "failed", error: err.message, progress: 0 },
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
75
|
+
// Note: Failed status update is handled by orchestrator via strategy
|
|
76
|
+
currentCreationIdRef.current = null;
|
|
91
77
|
onError?.(err.message);
|
|
92
78
|
},
|
|
93
|
-
[
|
|
79
|
+
[onError],
|
|
94
80
|
);
|
|
95
81
|
|
|
96
82
|
const { generate, isGenerating } = useGenerationOrchestrator(strategy, {
|
|
@@ -113,31 +99,27 @@ export const useWizardGeneration = (
|
|
|
113
99
|
hasStarted.current = true;
|
|
114
100
|
|
|
115
101
|
buildWizardInput(wizardData, scenario)
|
|
116
|
-
.then((input) => {
|
|
102
|
+
.then(async (input) => {
|
|
117
103
|
if (!input) {
|
|
118
104
|
hasStarted.current = false;
|
|
119
105
|
onError?.("Failed to build generation input");
|
|
120
106
|
return;
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
140
|
-
console.log("[useWizardGeneration] Created background job:", jobId);
|
|
109
|
+
// Save to Firestore with status="processing" BEFORE starting generation
|
|
110
|
+
if (strategy.saveAsProcessing && userId) {
|
|
111
|
+
try {
|
|
112
|
+
const creationId = await strategy.saveAsProcessing(userId, input);
|
|
113
|
+
currentCreationIdRef.current = creationId;
|
|
114
|
+
|
|
115
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
+
console.log("[useWizardGeneration] Saved as processing:", creationId);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
+
console.error("[useWizardGeneration] saveAsProcessing error:", err);
|
|
121
|
+
}
|
|
122
|
+
// Continue with generation even if save fails
|
|
141
123
|
}
|
|
142
124
|
}
|
|
143
125
|
|
|
@@ -162,8 +144,8 @@ export const useWizardGeneration = (
|
|
|
162
144
|
isGenerating,
|
|
163
145
|
generate,
|
|
164
146
|
onError,
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
strategy,
|
|
148
|
+
userId,
|
|
167
149
|
]);
|
|
168
150
|
|
|
169
151
|
return { isGenerating };
|