@umituz/react-native-ai-generation-content 1.26.36 → 1.26.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.26.36",
3
+ "version": "1.26.38",
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 type { GenerationStrategy } from "../../../../../presentation/hooks/generation/types";
8
+ import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
9
+ import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
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(
171
+ options: CreateImageStrategyOptions,
172
+ ): GenerationStrategy<ImageGenerationInput, ImageGenerationResult> {
173
+ const { scenario, collectionName = "creations" } = options;
174
+ const repository = createCreationsRepository(collectionName);
175
+
176
+ let lastInputRef: ImageGenerationInput | null = null;
177
+
178
+ return {
179
+ execute: async (input, onProgress) => {
180
+ if (!scenario.model) {
181
+ throw new Error("Model is required for image generation");
182
+ }
183
+
184
+ lastInputRef = input;
185
+
186
+ const result = await executeImageGeneration(input, scenario.model, onProgress);
187
+
188
+ if (!result.success || !result.imageUrl) {
189
+ throw new Error(result.error || "Image generation failed");
190
+ }
191
+
192
+ return { imageUrl: result.imageUrl };
193
+ },
194
+
195
+ getCreditCost: () => 1,
196
+
197
+ save: async (result, uid) => {
198
+ const input = lastInputRef;
199
+ if (!input || !scenario?.id) return;
200
+
201
+ const creation = {
202
+ id: `${scenario.id}_${Date.now()}`,
203
+ uri: result.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: result.imageUrl },
214
+ };
215
+
216
+ await repository.create(uid, creation);
217
+ },
218
+ };
219
+ }
@@ -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 { GenerationStrategy } from "../../../../../presentation/hooks/generation/types";
8
+ import type { VideoFeatureType } from "../../../../../domain/interfaces";
9
+ import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
10
+ import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
11
+ import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
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(
109
+ options: CreateVideoStrategyOptions,
110
+ ): GenerationStrategy<VideoGenerationInput, VideoGenerationResult> {
111
+ const { scenario, collectionName = "creations" } = options;
112
+ const repository = createCreationsRepository(collectionName);
113
+ const videoFeatureType = getVideoFeatureType(scenario.id);
114
+
115
+ let lastInputRef: VideoGenerationInput | null = null;
116
+
117
+ return {
118
+ execute: async (input, onProgress) => {
119
+ lastInputRef = input;
120
+
121
+ const result = await executeVideoFeature(
122
+ videoFeatureType,
123
+ {
124
+ sourceImageBase64: input.sourceImageBase64,
125
+ targetImageBase64: input.targetImageBase64,
126
+ prompt: input.prompt,
127
+ },
128
+ { onProgress },
129
+ );
130
+
131
+ if (!result.success || !result.videoUrl) {
132
+ throw new Error(result.error || "Video generation failed");
133
+ }
134
+
135
+ return { videoUrl: result.videoUrl };
136
+ },
137
+
138
+ getCreditCost: () => 1,
139
+
140
+ save: async (result, uid) => {
141
+ const input = lastInputRef;
142
+ if (!input || !scenario?.id) return;
143
+
144
+ const creation = {
145
+ id: `${scenario.id}_${Date.now()}`,
146
+ uri: result.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: result.videoUrl },
157
+ };
158
+
159
+ await repository.create(uid, creation);
160
+ },
161
+ };
162
+ }
@@ -1,469 +1,67 @@
1
1
  /**
2
2
  * Wizard Strategy Factory
3
- * Creates generation strategies for wizard-based scenarios
4
- * Centralized strategy creation for all wizard flows
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
7
  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
8
  import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
13
9
  import {
14
- GENERATION_TIMEOUT_MS,
15
- BASE64_IMAGE_PREFIX,
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;
10
+ createImageStrategy,
11
+ buildImageInput,
12
+ type ImageGenerationInput,
13
+ type ImageGenerationResult,
14
+ } from "./image-generation.strategy";
15
+ import {
16
+ createVideoStrategy,
17
+ buildVideoInput,
18
+ type VideoGenerationInput,
19
+ type VideoGenerationResult,
20
+ } from "./video-generation.strategy";
23
21
 
24
22
  // ============================================================================
25
23
  // Types
26
24
  // ============================================================================
27
25
 
28
- interface ImageGenerationInput {
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
26
  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);
27
+ type WizardGenerationResult = ImageGenerationResult | VideoGenerationResult;
72
28
 
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
- }
231
-
232
- return {
233
- photos, // Dynamic array - supports 1, 2, 3... N photos
234
- prompt,
235
- };
29
+ export interface CreateWizardStrategyOptions {
30
+ readonly scenario: WizardScenarioData;
31
+ readonly wizardData: Record<string, unknown>;
32
+ readonly collectionName?: string;
236
33
  }
237
34
 
238
35
  // ============================================================================
239
- // Video Generation Input Builder
36
+ // Strategy Factory
240
37
  // ============================================================================
241
38
 
242
- async function buildVideoGenerationInput(
243
- wizardData: Record<string, unknown>,
244
- scenario: WizardScenarioData,
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
- }
39
+ export function createWizardStrategy(
40
+ options: CreateWizardStrategyOptions,
41
+ ): GenerationStrategy<WizardGenerationInput, WizardGenerationResult> {
42
+ const { scenario, collectionName } = options;
43
+ const outputType = scenario.outputType || "video";
257
44
 
258
- // Validate scenario prompt
259
- if (!scenario.aiPrompt || scenario.aiPrompt.trim() === "") {
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`);
45
+ if (outputType === "image") {
46
+ return createImageStrategy({ scenario, collectionName }) as GenerationStrategy<WizardGenerationInput, WizardGenerationResult>;
266
47
  }
267
48
 
268
- return {
269
- sourceImageBase64: photos[0],
270
- targetImageBase64: photos[1] || photos[0], // Use first photo if only 1 provided
271
- prompt: scenario.aiPrompt,
272
- };
49
+ return createVideoStrategy({ scenario, collectionName }) as GenerationStrategy<WizardGenerationInput, WizardGenerationResult>;
273
50
  }
274
51
 
275
52
  // ============================================================================
276
- // Input Builder Dispatcher (Routes to correct builder)
53
+ // Input Builder
277
54
  // ============================================================================
278
55
 
279
- async function buildGenerationInput(
56
+ export async function buildWizardInput(
280
57
  wizardData: Record<string, unknown>,
281
58
  scenario: WizardScenarioData,
282
59
  ): Promise<WizardGenerationInput | null> {
283
60
  const outputType = scenario.outputType || "video";
284
61
 
285
62
  if (outputType === "image") {
286
- return buildImageGenerationInput(wizardData, scenario);
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
- }
63
+ return buildImageInput(wizardData, scenario);
303
64
  }
304
65
 
305
- throw new Error(`Unknown video feature type for scenario "${scenarioId}". Add pattern to VIDEO_FEATURE_PATTERNS.`);
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;
66
+ return buildVideoInput(wizardData, scenario);
316
67
  }
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;