@umituz/react-native-ai-generation-content 1.26.36 → 1.26.39
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.strategy.ts +219 -0
- package/src/domains/generation/wizard/infrastructure/strategies/index.ts +1 -1
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +162 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.factory.ts +22 -437
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.39",
|
|
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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generation Strategy
|
|
3
|
+
* Handles image-specific generation logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
7
|
+
import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
|
|
8
|
+
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
9
|
+
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
10
|
+
import {
|
|
11
|
+
GENERATION_TIMEOUT_MS,
|
|
12
|
+
BASE64_IMAGE_PREFIX,
|
|
13
|
+
PHOTO_KEY_PREFIX,
|
|
14
|
+
DEFAULT_STYLE_VALUE,
|
|
15
|
+
MODEL_INPUT_DEFAULTS,
|
|
16
|
+
} from "./wizard-strategy.constants";
|
|
17
|
+
|
|
18
|
+
declare const __DEV__: boolean;
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
export interface ImageGenerationInput {
|
|
25
|
+
readonly photos: readonly string[];
|
|
26
|
+
readonly prompt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ImageGenerationResult {
|
|
30
|
+
readonly imageUrl: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Photo Extraction
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
async function extractPhotosFromWizardData(
|
|
38
|
+
wizardData: Record<string, unknown>,
|
|
39
|
+
): Promise<string[] | null> {
|
|
40
|
+
const photoKeys = Object.keys(wizardData)
|
|
41
|
+
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
42
|
+
.sort();
|
|
43
|
+
|
|
44
|
+
if (photoKeys.length === 0) {
|
|
45
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
46
|
+
console.error("[ImageStrategy] No photos found", { keys: Object.keys(wizardData) });
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const photoUris: string[] = [];
|
|
52
|
+
for (const key of photoKeys) {
|
|
53
|
+
const photo = wizardData[key] as { uri?: string };
|
|
54
|
+
if (!photo?.uri) return null;
|
|
55
|
+
photoUris.push(photo.uri);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
|
|
59
|
+
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
60
|
+
|
|
61
|
+
return validPhotos.length > 0 ? validPhotos : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Image Generation Executor
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
async function executeImageGeneration(
|
|
69
|
+
input: ImageGenerationInput,
|
|
70
|
+
model: string,
|
|
71
|
+
onProgress?: (progress: number) => void,
|
|
72
|
+
): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
|
73
|
+
const { providerRegistry } = await import("../../../../../infrastructure/services/provider-registry.service");
|
|
74
|
+
|
|
75
|
+
const provider = providerRegistry.getActiveProvider();
|
|
76
|
+
if (!provider?.isInitialized()) {
|
|
77
|
+
return { success: false, error: "AI provider not initialized" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const formatBase64 = (base64: string): string =>
|
|
82
|
+
base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
|
|
83
|
+
|
|
84
|
+
const imageUrls = input.photos.map(formatBase64);
|
|
85
|
+
if (imageUrls.length === 0) {
|
|
86
|
+
return { success: false, error: "At least one image required" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
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.`;
|
|
90
|
+
|
|
91
|
+
const modelInput = {
|
|
92
|
+
image_urls: imageUrls,
|
|
93
|
+
prompt: enhancedPrompt,
|
|
94
|
+
aspect_ratio: MODEL_INPUT_DEFAULTS.aspectRatio,
|
|
95
|
+
output_format: MODEL_INPUT_DEFAULTS.outputFormat,
|
|
96
|
+
num_images: MODEL_INPUT_DEFAULTS.numImages,
|
|
97
|
+
enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let lastStatus = "";
|
|
101
|
+
const result = await provider.subscribe(model, modelInput, {
|
|
102
|
+
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
103
|
+
onQueueUpdate: (status) => {
|
|
104
|
+
if (status.status !== lastStatus) lastStatus = status.status;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const rawResult = result as Record<string, unknown>;
|
|
109
|
+
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
110
|
+
const imageUrl = data?.images?.[0]?.url;
|
|
111
|
+
|
|
112
|
+
onProgress?.(100);
|
|
113
|
+
|
|
114
|
+
return imageUrl ? { success: true, imageUrl } : { success: false, error: "No image generated" };
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return { success: false, error: error instanceof Error ? error.message : "Generation failed" };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Input Builder
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
export async function buildImageInput(
|
|
125
|
+
wizardData: Record<string, unknown>,
|
|
126
|
+
scenario: WizardScenarioData,
|
|
127
|
+
): Promise<ImageGenerationInput | null> {
|
|
128
|
+
const photos = await extractPhotosFromWizardData(wizardData);
|
|
129
|
+
if (!photos) return null;
|
|
130
|
+
|
|
131
|
+
if (!scenario.aiPrompt?.trim()) {
|
|
132
|
+
throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let prompt = scenario.aiPrompt;
|
|
136
|
+
|
|
137
|
+
const styleEnhancements: string[] = [];
|
|
138
|
+
|
|
139
|
+
const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
|
|
140
|
+
if (romanticMoods?.length) {
|
|
141
|
+
styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const artStyle = wizardData.selection_art_style as string | undefined;
|
|
145
|
+
if (artStyle && artStyle !== DEFAULT_STYLE_VALUE) {
|
|
146
|
+
styleEnhancements.push(`Art style: ${artStyle}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const artist = wizardData.selection_artist_style as string | undefined;
|
|
150
|
+
if (artist && artist !== DEFAULT_STYLE_VALUE) {
|
|
151
|
+
styleEnhancements.push(`Artist style: ${artist}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (styleEnhancements.length > 0) {
|
|
155
|
+
prompt = `${prompt}. ${styleEnhancements.join(", ")}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { photos, prompt };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Strategy Factory
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
export interface CreateImageStrategyOptions {
|
|
166
|
+
readonly scenario: WizardScenarioData;
|
|
167
|
+
readonly collectionName?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createImageStrategy(options: CreateImageStrategyOptions): WizardStrategy {
|
|
171
|
+
const { scenario, collectionName = "creations" } = options;
|
|
172
|
+
const repository = createCreationsRepository(collectionName);
|
|
173
|
+
|
|
174
|
+
let lastInputRef: ImageGenerationInput | null = null;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
execute: async (input: unknown, onProgress) => {
|
|
178
|
+
const imageInput = input as ImageGenerationInput;
|
|
179
|
+
if (!scenario.model) {
|
|
180
|
+
throw new Error("Model is required for image generation");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
lastInputRef = imageInput;
|
|
184
|
+
|
|
185
|
+
const result = await executeImageGeneration(imageInput, scenario.model, onProgress);
|
|
186
|
+
|
|
187
|
+
if (!result.success || !result.imageUrl) {
|
|
188
|
+
throw new Error(result.error || "Image generation failed");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { imageUrl: result.imageUrl };
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
getCreditCost: () => 1,
|
|
195
|
+
|
|
196
|
+
save: async (result: unknown, uid) => {
|
|
197
|
+
const input = lastInputRef;
|
|
198
|
+
const imageResult = result as { imageUrl?: string };
|
|
199
|
+
if (!input || !scenario?.id || !imageResult.imageUrl) return;
|
|
200
|
+
|
|
201
|
+
const creation = {
|
|
202
|
+
id: `${scenario.id}_${Date.now()}`,
|
|
203
|
+
uri: imageResult.imageUrl,
|
|
204
|
+
type: scenario.id,
|
|
205
|
+
prompt: input.prompt,
|
|
206
|
+
createdAt: new Date(),
|
|
207
|
+
isShared: false,
|
|
208
|
+
isFavorite: false,
|
|
209
|
+
metadata: {
|
|
210
|
+
scenarioId: scenario.id,
|
|
211
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
212
|
+
},
|
|
213
|
+
output: { imageUrl: imageResult.imageUrl },
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
await repository.create(uid, creation);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createWizardStrategy, buildWizardInput } from './wizard-strategy.factory';
|
|
2
|
-
export type { CreateWizardStrategyOptions } from './wizard-strategy.factory';
|
|
2
|
+
export type { CreateWizardStrategyOptions, WizardStrategy } from './wizard-strategy.factory';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Strategy
|
|
3
|
+
* Handles video-specific generation logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
7
|
+
import type { VideoFeatureType } from "../../../../../domain/interfaces";
|
|
8
|
+
import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
|
|
9
|
+
import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
|
|
10
|
+
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
11
|
+
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
12
|
+
import { PHOTO_KEY_PREFIX, VIDEO_FEATURE_PATTERNS } from "./wizard-strategy.constants";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface VideoGenerationInput {
|
|
21
|
+
readonly sourceImageBase64: string;
|
|
22
|
+
readonly targetImageBase64: string;
|
|
23
|
+
readonly prompt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface VideoGenerationResult {
|
|
27
|
+
readonly videoUrl: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Photo Extraction
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
async function extractPhotosFromWizardData(
|
|
35
|
+
wizardData: Record<string, unknown>,
|
|
36
|
+
): Promise<string[] | null> {
|
|
37
|
+
const photoKeys = Object.keys(wizardData)
|
|
38
|
+
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
39
|
+
.sort();
|
|
40
|
+
|
|
41
|
+
if (photoKeys.length === 0) {
|
|
42
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
console.error("[VideoStrategy] No photos found", { keys: Object.keys(wizardData) });
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const photoUris: string[] = [];
|
|
49
|
+
for (const key of photoKeys) {
|
|
50
|
+
const photo = wizardData[key] as { uri?: string };
|
|
51
|
+
if (!photo?.uri) return null;
|
|
52
|
+
photoUris.push(photo.uri);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
|
|
56
|
+
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
57
|
+
|
|
58
|
+
return validPhotos.length > 0 ? validPhotos : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Video Feature Type Detection
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
function getVideoFeatureType(scenarioId: string): VideoFeatureType {
|
|
66
|
+
const id = scenarioId.toLowerCase();
|
|
67
|
+
|
|
68
|
+
for (const [pattern, featureType] of Object.entries(VIDEO_FEATURE_PATTERNS)) {
|
|
69
|
+
if (id.includes(pattern)) {
|
|
70
|
+
return featureType;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Error(`Unknown video feature type for scenario "${scenarioId}". Add pattern to VIDEO_FEATURE_PATTERNS.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Input Builder
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
export async function buildVideoInput(
|
|
82
|
+
wizardData: Record<string, unknown>,
|
|
83
|
+
scenario: WizardScenarioData,
|
|
84
|
+
): Promise<VideoGenerationInput | null> {
|
|
85
|
+
const photos = await extractPhotosFromWizardData(wizardData);
|
|
86
|
+
if (!photos || photos.length < 1) return null;
|
|
87
|
+
|
|
88
|
+
if (!scenario.aiPrompt?.trim()) {
|
|
89
|
+
throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sourceImageBase64: photos[0],
|
|
94
|
+
targetImageBase64: photos[1] || photos[0],
|
|
95
|
+
prompt: scenario.aiPrompt,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Strategy Factory
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export interface CreateVideoStrategyOptions {
|
|
104
|
+
readonly scenario: WizardScenarioData;
|
|
105
|
+
readonly collectionName?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createVideoStrategy(options: CreateVideoStrategyOptions): WizardStrategy {
|
|
109
|
+
const { scenario, collectionName = "creations" } = options;
|
|
110
|
+
const repository = createCreationsRepository(collectionName);
|
|
111
|
+
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
112
|
+
|
|
113
|
+
let lastInputRef: VideoGenerationInput | null = null;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
execute: async (input: unknown, onProgress) => {
|
|
117
|
+
const videoInput = input as VideoGenerationInput;
|
|
118
|
+
lastInputRef = videoInput;
|
|
119
|
+
|
|
120
|
+
const result = await executeVideoFeature(
|
|
121
|
+
videoFeatureType,
|
|
122
|
+
{
|
|
123
|
+
sourceImageBase64: videoInput.sourceImageBase64,
|
|
124
|
+
targetImageBase64: videoInput.targetImageBase64,
|
|
125
|
+
prompt: videoInput.prompt,
|
|
126
|
+
},
|
|
127
|
+
{ onProgress },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!result.success || !result.videoUrl) {
|
|
131
|
+
throw new Error(result.error || "Video generation failed");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { videoUrl: result.videoUrl };
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
getCreditCost: () => 1,
|
|
138
|
+
|
|
139
|
+
save: async (result: unknown, uid) => {
|
|
140
|
+
const input = lastInputRef;
|
|
141
|
+
const videoResult = result as { videoUrl?: string };
|
|
142
|
+
if (!input || !scenario?.id || !videoResult.videoUrl) return;
|
|
143
|
+
|
|
144
|
+
const creation = {
|
|
145
|
+
id: `${scenario.id}_${Date.now()}`,
|
|
146
|
+
uri: videoResult.videoUrl,
|
|
147
|
+
type: scenario.id,
|
|
148
|
+
prompt: input.prompt,
|
|
149
|
+
createdAt: new Date(),
|
|
150
|
+
isShared: false,
|
|
151
|
+
isFavorite: false,
|
|
152
|
+
metadata: {
|
|
153
|
+
scenarioId: scenario.id,
|
|
154
|
+
scenarioTitle: scenario.title || scenario.id,
|
|
155
|
+
},
|
|
156
|
+
output: { videoUrl: videoResult.videoUrl },
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await repository.create(uid, creation);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -1,469 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wizard Strategy Factory
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Routes to correct strategy based on output type
|
|
4
|
+
* Single Responsibility: Only dispatches, doesn't contain business logic
|
|
5
5
|
*/
|
|
6
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
7
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
PHOTO_KEY_PREFIX,
|
|
17
|
-
DEFAULT_STYLE_VALUE,
|
|
18
|
-
MODEL_INPUT_DEFAULTS,
|
|
19
|
-
VIDEO_FEATURE_PATTERNS,
|
|
20
|
-
} from "./wizard-strategy.constants";
|
|
21
|
-
|
|
22
|
-
declare const __DEV__: boolean;
|
|
8
|
+
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
9
|
+
import { createImageStrategy, buildImageInput } from "./image-generation.strategy";
|
|
10
|
+
import { createVideoStrategy, buildVideoInput } from "./video-generation.strategy";
|
|
23
11
|
|
|
24
12
|
// ============================================================================
|
|
25
13
|
// Types
|
|
26
14
|
// ============================================================================
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
readonly photos: readonly string[]; // Dynamic array - supports 1, 2, 3... N photos
|
|
30
|
-
readonly prompt: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface VideoGenerationInput {
|
|
34
|
-
readonly sourceImageBase64: string;
|
|
35
|
-
readonly targetImageBase64: string;
|
|
36
|
-
readonly prompt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type WizardGenerationInput = ImageGenerationInput | VideoGenerationInput;
|
|
40
|
-
type WizardGenerationResult = { imageUrl: string } | { videoUrl: string };
|
|
41
|
-
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// Image Generation Executor
|
|
44
|
-
// ============================================================================
|
|
45
|
-
|
|
46
|
-
async function executeImageGeneration(
|
|
47
|
-
input: ImageGenerationInput,
|
|
48
|
-
model: string,
|
|
49
|
-
onProgress?: (progress: number) => void,
|
|
50
|
-
): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
|
51
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
52
|
-
console.log("[WizardStrategy] executeImageGeneration ENTRY", {
|
|
53
|
-
receivedModel: model,
|
|
54
|
-
photoCount: input.photos.length,
|
|
55
|
-
promptLength: input.prompt.length,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const { providerRegistry } = await import("../../../../../infrastructure/services/provider-registry.service");
|
|
60
|
-
|
|
61
|
-
const provider = providerRegistry.getActiveProvider();
|
|
62
|
-
if (!provider || !provider.isInitialized()) {
|
|
63
|
-
return { success: false, error: "AI provider not initialized" };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const formatBase64 = (base64: string): string => {
|
|
68
|
-
return base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const imageUrls = input.photos.map(formatBase64);
|
|
72
|
-
|
|
73
|
-
if (imageUrls.length === 0) {
|
|
74
|
-
return { success: false, error: "At least one image required" };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
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.`;
|
|
78
|
-
|
|
79
|
-
const modelInput = {
|
|
80
|
-
image_urls: imageUrls,
|
|
81
|
-
prompt: enhancedPrompt,
|
|
82
|
-
aspect_ratio: MODEL_INPUT_DEFAULTS.aspectRatio,
|
|
83
|
-
output_format: MODEL_INPUT_DEFAULTS.outputFormat,
|
|
84
|
-
num_images: MODEL_INPUT_DEFAULTS.numImages,
|
|
85
|
-
enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
89
|
-
console.log("[WizardStrategy] ABOUT TO CALL provider.subscribe", {
|
|
90
|
-
model,
|
|
91
|
-
modelType: typeof model,
|
|
92
|
-
modelLength: model?.length,
|
|
93
|
-
imageUrlsCount: imageUrls.length,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let lastStatus = "";
|
|
98
|
-
const result = await provider.subscribe(model, modelInput, {
|
|
99
|
-
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
100
|
-
onQueueUpdate: (status) => {
|
|
101
|
-
if (status.status === lastStatus) return;
|
|
102
|
-
lastStatus = status.status;
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const rawResult = result as Record<string, unknown>;
|
|
107
|
-
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
108
|
-
const imageUrl = data?.images?.[0]?.url;
|
|
109
|
-
|
|
110
|
-
onProgress?.(100);
|
|
111
|
-
|
|
112
|
-
if (!imageUrl) {
|
|
113
|
-
return { success: false, error: "No image generated" };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { success: true, imageUrl };
|
|
117
|
-
} catch (error) {
|
|
118
|
-
const message = error instanceof Error ? error.message : "Generation failed";
|
|
119
|
-
return { success: false, error: message };
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ============================================================================
|
|
124
|
-
// Photo Extraction (Generic - used by both image and video)
|
|
125
|
-
// ============================================================================
|
|
126
|
-
|
|
127
|
-
async function extractPhotosFromWizardData(
|
|
128
|
-
wizardData: Record<string, unknown>,
|
|
129
|
-
): Promise<string[] | null> {
|
|
130
|
-
// Find ALL photo keys dynamically (photo_1, photo_2, photo_3, ...)
|
|
131
|
-
const photoKeys = Object.keys(wizardData)
|
|
132
|
-
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
133
|
-
.sort(); // Sort to maintain order (photo_1, photo_2, ...)
|
|
134
|
-
|
|
135
|
-
if (photoKeys.length === 0) {
|
|
136
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
137
|
-
console.error("[WizardStrategy] No photos found in wizard data", {
|
|
138
|
-
keys: Object.keys(wizardData),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Extract photo URIs
|
|
145
|
-
const photoUris: string[] = [];
|
|
146
|
-
for (const key of photoKeys) {
|
|
147
|
-
const photo = wizardData[key] as { uri?: string; base64?: string };
|
|
148
|
-
if (!photo?.uri) {
|
|
149
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
150
|
-
console.error("[WizardStrategy] Photo missing URI", { key });
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
photoUris.push(photo.uri);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Convert all images to base64 in parallel
|
|
158
|
-
const photosBase64 = await Promise.all(
|
|
159
|
-
photoUris.map((uri) => readFileAsBase64(uri)),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Filter out nulls/undefined
|
|
163
|
-
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
164
|
-
|
|
165
|
-
if (validPhotos.length === 0) {
|
|
166
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
167
|
-
console.error("[WizardStrategy] Failed to convert any images to base64");
|
|
168
|
-
}
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
173
|
-
console.log("[WizardStrategy] Photos extracted", {
|
|
174
|
-
photoCount: validPhotos.length,
|
|
175
|
-
photoKeys,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return validPhotos;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// Image Generation Input Builder
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
async function buildImageGenerationInput(
|
|
187
|
-
wizardData: Record<string, unknown>,
|
|
188
|
-
scenario: WizardScenarioData,
|
|
189
|
-
): Promise<ImageGenerationInput | null> {
|
|
190
|
-
// Extract photos
|
|
191
|
-
const photos = await extractPhotosFromWizardData(wizardData);
|
|
192
|
-
if (!photos) return null;
|
|
193
|
-
|
|
194
|
-
// Validate scenario prompt
|
|
195
|
-
if (!scenario.aiPrompt || scenario.aiPrompt.trim() === "") {
|
|
196
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
197
|
-
console.error("[WizardStrategy] scenario.aiPrompt missing", {
|
|
198
|
-
scenarioId: scenario.id,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let prompt = scenario.aiPrompt;
|
|
205
|
-
|
|
206
|
-
// Enhance prompt with style selections (image-specific)
|
|
207
|
-
const styleEnhancements: string[] = [];
|
|
208
|
-
|
|
209
|
-
// Romantic mood (multi-select array)
|
|
210
|
-
const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
|
|
211
|
-
if (romanticMoods && romanticMoods.length > 0) {
|
|
212
|
-
styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Art style (single select)
|
|
216
|
-
const artStyle = wizardData.selection_art_style as string | undefined;
|
|
217
|
-
if (artStyle && artStyle !== DEFAULT_STYLE_VALUE) {
|
|
218
|
-
styleEnhancements.push(`Art style: ${artStyle}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Artist style (single select)
|
|
222
|
-
const artist = wizardData.selection_artist_style as string | undefined;
|
|
223
|
-
if (artist && artist !== DEFAULT_STYLE_VALUE) {
|
|
224
|
-
styleEnhancements.push(`Artist style: ${artist}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Enhance prompt with selected styles
|
|
228
|
-
if (styleEnhancements.length > 0) {
|
|
229
|
-
prompt = `${prompt}. ${styleEnhancements.join(", ")}`;
|
|
230
|
-
}
|
|
16
|
+
export type { WizardStrategy } from "./wizard-strategy.types";
|
|
231
17
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
18
|
+
export interface CreateWizardStrategyOptions {
|
|
19
|
+
readonly scenario: WizardScenarioData;
|
|
20
|
+
readonly wizardData: Record<string, unknown>;
|
|
21
|
+
readonly collectionName?: string;
|
|
236
22
|
}
|
|
237
23
|
|
|
238
24
|
// ============================================================================
|
|
239
|
-
//
|
|
25
|
+
// Strategy Factory
|
|
240
26
|
// ============================================================================
|
|
241
27
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
scenario
|
|
245
|
-
): Promise<VideoGenerationInput | null> {
|
|
246
|
-
// Extract photos
|
|
247
|
-
const photos = await extractPhotosFromWizardData(wizardData);
|
|
248
|
-
if (!photos) return null;
|
|
249
|
-
|
|
250
|
-
// Video requires at least 1 photo (can duplicate if only 1)
|
|
251
|
-
if (photos.length < 1) {
|
|
252
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
253
|
-
console.error("[WizardStrategy] Video generation needs at least 1 photo");
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
28
|
+
export function createWizardStrategy(options: CreateWizardStrategyOptions): WizardStrategy {
|
|
29
|
+
const { scenario, collectionName } = options;
|
|
30
|
+
const outputType = scenario.outputType || "video";
|
|
257
31
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
261
|
-
console.error("[WizardStrategy] scenario.aiPrompt missing", {
|
|
262
|
-
scenarioId: scenario.id,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
|
|
32
|
+
if (outputType === "image") {
|
|
33
|
+
return createImageStrategy({ scenario, collectionName });
|
|
266
34
|
}
|
|
267
35
|
|
|
268
|
-
return {
|
|
269
|
-
sourceImageBase64: photos[0],
|
|
270
|
-
targetImageBase64: photos[1] || photos[0], // Use first photo if only 1 provided
|
|
271
|
-
prompt: scenario.aiPrompt,
|
|
272
|
-
};
|
|
36
|
+
return createVideoStrategy({ scenario, collectionName });
|
|
273
37
|
}
|
|
274
38
|
|
|
275
39
|
// ============================================================================
|
|
276
|
-
// Input Builder
|
|
40
|
+
// Input Builder
|
|
277
41
|
// ============================================================================
|
|
278
42
|
|
|
279
|
-
async function
|
|
43
|
+
export async function buildWizardInput(
|
|
280
44
|
wizardData: Record<string, unknown>,
|
|
281
45
|
scenario: WizardScenarioData,
|
|
282
|
-
): Promise<
|
|
46
|
+
): Promise<unknown> {
|
|
283
47
|
const outputType = scenario.outputType || "video";
|
|
284
48
|
|
|
285
49
|
if (outputType === "image") {
|
|
286
|
-
return
|
|
287
|
-
} else {
|
|
288
|
-
return buildVideoGenerationInput(wizardData, scenario);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ============================================================================
|
|
293
|
-
// Video Feature Type Detection
|
|
294
|
-
// ============================================================================
|
|
295
|
-
|
|
296
|
-
function getVideoFeatureType(scenarioId: string): VideoFeatureType {
|
|
297
|
-
const id = scenarioId.toLowerCase();
|
|
298
|
-
|
|
299
|
-
for (const [pattern, featureType] of Object.entries(VIDEO_FEATURE_PATTERNS)) {
|
|
300
|
-
if (id.includes(pattern)) {
|
|
301
|
-
return featureType;
|
|
302
|
-
}
|
|
50
|
+
return buildImageInput(wizardData, scenario);
|
|
303
51
|
}
|
|
304
52
|
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ============================================================================
|
|
309
|
-
// Strategy Factory
|
|
310
|
-
// ============================================================================
|
|
311
|
-
|
|
312
|
-
export interface CreateWizardStrategyOptions {
|
|
313
|
-
readonly scenario: WizardScenarioData;
|
|
314
|
-
readonly wizardData: Record<string, unknown>;
|
|
315
|
-
readonly collectionName?: string;
|
|
53
|
+
return buildVideoInput(wizardData, scenario);
|
|
316
54
|
}
|
|
317
|
-
|
|
318
|
-
export const createWizardStrategy = (
|
|
319
|
-
options: CreateWizardStrategyOptions,
|
|
320
|
-
): GenerationStrategy<WizardGenerationInput, WizardGenerationResult> => {
|
|
321
|
-
const { scenario, collectionName = "creations" } = options;
|
|
322
|
-
const repository = createCreationsRepository(collectionName);
|
|
323
|
-
const outputType = scenario.outputType || "video";
|
|
324
|
-
const videoFeatureType = outputType === "video" ? getVideoFeatureType(scenario.id) : null;
|
|
325
|
-
|
|
326
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
327
|
-
console.log("[WizardStrategy] createWizardStrategy called", {
|
|
328
|
-
scenarioId: scenario.id,
|
|
329
|
-
scenarioModel: scenario.model,
|
|
330
|
-
outputType,
|
|
331
|
-
hasModel: !!scenario.model,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let lastInputRef: WizardGenerationInput | null = null;
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
execute: async (input, onProgress) => {
|
|
339
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
340
|
-
console.log("[WizardStrategy] execute() called", {
|
|
341
|
-
scenarioId: scenario.id,
|
|
342
|
-
outputType,
|
|
343
|
-
scenarioModel: scenario.model,
|
|
344
|
-
hasModel: !!scenario.model,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
lastInputRef = input;
|
|
349
|
-
|
|
350
|
-
// Execute based on output type
|
|
351
|
-
if (outputType === "image") {
|
|
352
|
-
// Validate model is provided by app
|
|
353
|
-
if (!scenario.model) {
|
|
354
|
-
throw new Error("Model is required for image generation. Please configure model in app generation.config.ts");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
358
|
-
console.log("[WizardStrategy] About to call executeImageGeneration", {
|
|
359
|
-
model: scenario.model,
|
|
360
|
-
scenarioId: scenario.id,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const imageInput = input as ImageGenerationInput;
|
|
365
|
-
const result = await executeImageGeneration(imageInput, scenario.model, onProgress);
|
|
366
|
-
|
|
367
|
-
if (!result.success || !result.imageUrl) {
|
|
368
|
-
throw new Error(result.error || "Image generation failed");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return { imageUrl: result.imageUrl };
|
|
372
|
-
} else {
|
|
373
|
-
if (!videoFeatureType) {
|
|
374
|
-
throw new Error("Video feature type is required for video generation");
|
|
375
|
-
}
|
|
376
|
-
const videoInput = input as VideoGenerationInput;
|
|
377
|
-
const result = await executeVideoFeature(
|
|
378
|
-
videoFeatureType,
|
|
379
|
-
{
|
|
380
|
-
sourceImageBase64: videoInput.sourceImageBase64,
|
|
381
|
-
targetImageBase64: videoInput.targetImageBase64,
|
|
382
|
-
prompt: videoInput.prompt,
|
|
383
|
-
},
|
|
384
|
-
{ onProgress },
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
if (!result.success || !result.videoUrl) {
|
|
388
|
-
throw new Error(result.error || "Video generation failed");
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return { videoUrl: result.videoUrl };
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
|
|
395
|
-
getCreditCost: () => 1,
|
|
396
|
-
|
|
397
|
-
save: async (result, uid) => {
|
|
398
|
-
const input = lastInputRef;
|
|
399
|
-
if (!input) return;
|
|
400
|
-
|
|
401
|
-
// Validate scenario
|
|
402
|
-
if (!scenario || !scenario.id) {
|
|
403
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
404
|
-
console.error("[WizardStrategy] Cannot save: scenario.id is undefined");
|
|
405
|
-
}
|
|
406
|
-
throw new Error("Scenario ID is required for saving creation");
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Extract prompt
|
|
410
|
-
const prompt = 'prompt' in input ? input.prompt : '';
|
|
411
|
-
|
|
412
|
-
// Extract URLs based on output type
|
|
413
|
-
let mainUri = '';
|
|
414
|
-
let imageUrl: string | undefined;
|
|
415
|
-
let videoUrl: string | undefined;
|
|
416
|
-
|
|
417
|
-
if (outputType === "image") {
|
|
418
|
-
const imageResult = result as { imageUrl?: string };
|
|
419
|
-
imageUrl = imageResult.imageUrl;
|
|
420
|
-
mainUri = imageUrl || '';
|
|
421
|
-
} else {
|
|
422
|
-
const videoResult = result as { videoUrl?: string };
|
|
423
|
-
videoUrl = videoResult.videoUrl;
|
|
424
|
-
mainUri = videoUrl || '';
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Create unique ID
|
|
428
|
-
const creationId = `${scenario.id}_${Date.now()}`;
|
|
429
|
-
|
|
430
|
-
// Build creation object
|
|
431
|
-
const creation = {
|
|
432
|
-
id: creationId,
|
|
433
|
-
uri: mainUri,
|
|
434
|
-
type: scenario.id,
|
|
435
|
-
prompt,
|
|
436
|
-
createdAt: new Date(),
|
|
437
|
-
isShared: false,
|
|
438
|
-
isFavorite: false,
|
|
439
|
-
metadata: {
|
|
440
|
-
scenarioId: scenario.id,
|
|
441
|
-
scenarioTitle: scenario.title || scenario.id,
|
|
442
|
-
},
|
|
443
|
-
output: outputType === "image"
|
|
444
|
-
? { imageUrl }
|
|
445
|
-
: { videoUrl },
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
449
|
-
console.log("[WizardStrategy] Saving creation", {
|
|
450
|
-
creationId,
|
|
451
|
-
scenarioId: scenario.id,
|
|
452
|
-
outputType,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
await repository.create(uid, creation);
|
|
457
|
-
|
|
458
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
459
|
-
console.log("[WizardStrategy] Creation saved successfully");
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
};
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
// ============================================================================
|
|
466
|
-
// Input Builder Helper (Public API)
|
|
467
|
-
// ============================================================================
|
|
468
|
-
|
|
469
|
-
export const buildWizardInput = buildGenerationInput;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard Strategy Types
|
|
3
|
+
* Shared types for all wizard strategies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface WizardStrategy {
|
|
7
|
+
execute: (
|
|
8
|
+
input: unknown,
|
|
9
|
+
onProgress?: (progress: number) => void,
|
|
10
|
+
) => Promise<{ imageUrl?: string; videoUrl?: string }>;
|
|
11
|
+
getCreditCost: () => number;
|
|
12
|
+
save?: (result: unknown, userId: string) => Promise<void>;
|
|
13
|
+
}
|