@umituz/react-native-ai-generation-content 1.35.9 → 1.36.0
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 +113 -0
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +35 -183
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.types.ts +26 -0
- package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts +70 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +24 -207
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +29 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.utils.ts +28 -0
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +1 -1
- package/src/domains/prompts/index.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.0",
|
|
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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generation Executor
|
|
3
|
+
* Handles the actual image generation execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { buildFacePreservationPrompt } from "../../../../prompts/infrastructure/builders/face-preservation-builder";
|
|
7
|
+
import { buildInteractionStylePrompt } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
|
|
8
|
+
import type { ImageGenerationInput } from "./image-generation.types";
|
|
9
|
+
import {
|
|
10
|
+
GENERATION_TIMEOUT_MS,
|
|
11
|
+
BASE64_IMAGE_PREFIX,
|
|
12
|
+
DEFAULT_STYLE_VALUE,
|
|
13
|
+
MODEL_INPUT_DEFAULTS,
|
|
14
|
+
} from "./wizard-strategy.constants";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
interface ExecutionResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
imageUrl?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Formats base64 string with proper data URI prefix
|
|
26
|
+
*/
|
|
27
|
+
function formatBase64(base64: string): string {
|
|
28
|
+
return base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Builds the final prompt based on input type (photo-based or text-to-image)
|
|
33
|
+
*/
|
|
34
|
+
function buildFinalPrompt(input: ImageGenerationInput, imageUrls: string[]): string {
|
|
35
|
+
const hasPhotos = imageUrls.length > 0;
|
|
36
|
+
|
|
37
|
+
if (hasPhotos) {
|
|
38
|
+
const facePrompt = buildFacePreservationPrompt({
|
|
39
|
+
scenarioPrompt: input.prompt,
|
|
40
|
+
personCount: imageUrls.length,
|
|
41
|
+
});
|
|
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
|
+
}
|
|
50
|
+
|
|
51
|
+
// Text-to-image with optional style
|
|
52
|
+
if (input.style && input.style !== DEFAULT_STYLE_VALUE) {
|
|
53
|
+
return `${input.prompt}. Style: ${input.style}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return input.prompt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Executes image generation using the AI provider
|
|
61
|
+
*/
|
|
62
|
+
export async function executeImageGeneration(
|
|
63
|
+
input: ImageGenerationInput,
|
|
64
|
+
model: string,
|
|
65
|
+
onProgress?: (progress: number) => void,
|
|
66
|
+
): Promise<ExecutionResult> {
|
|
67
|
+
const { providerRegistry } = await import("../../../../../infrastructure/services/provider-registry.service");
|
|
68
|
+
|
|
69
|
+
const provider = providerRegistry.getActiveProvider();
|
|
70
|
+
if (!provider?.isInitialized()) {
|
|
71
|
+
return { success: false, error: "AI provider not initialized" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const imageUrls = input.photos.map(formatBase64);
|
|
76
|
+
const finalPrompt = buildFinalPrompt(input, imageUrls);
|
|
77
|
+
|
|
78
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
|
+
const mode = imageUrls.length > 0 ? "Photo-based" : "Text-to-image";
|
|
80
|
+
console.log(`[ImageExecutor] ${mode} generation`, { personCount: imageUrls.length });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const modelInput: Record<string, unknown> = {
|
|
84
|
+
prompt: finalPrompt,
|
|
85
|
+
aspect_ratio: MODEL_INPUT_DEFAULTS.aspectRatio,
|
|
86
|
+
output_format: MODEL_INPUT_DEFAULTS.outputFormat,
|
|
87
|
+
num_images: MODEL_INPUT_DEFAULTS.numImages,
|
|
88
|
+
enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (imageUrls.length > 0) {
|
|
92
|
+
modelInput.image_urls = imageUrls;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let lastStatus = "";
|
|
96
|
+
const result = await provider.subscribe(model, modelInput, {
|
|
97
|
+
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
98
|
+
onQueueUpdate: (status) => {
|
|
99
|
+
if (status.status !== lastStatus) lastStatus = status.status;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const rawResult = result as Record<string, unknown>;
|
|
104
|
+
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
105
|
+
const imageUrl = data?.images?.[0]?.url;
|
|
106
|
+
|
|
107
|
+
onProgress?.(100);
|
|
108
|
+
|
|
109
|
+
return imageUrl ? { success: true, imageUrl } : { success: false, error: "No image generated" };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { success: false, error: error instanceof Error ? error.message : "Generation failed" };
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts
CHANGED
|
@@ -3,159 +3,18 @@
|
|
|
3
3
|
* Handles image-specific generation logic
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
7
6
|
import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
|
|
8
7
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
9
8
|
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
BASE64_IMAGE_PREFIX,
|
|
13
|
-
PHOTO_KEY_PREFIX,
|
|
14
|
-
DEFAULT_STYLE_VALUE,
|
|
15
|
-
MODEL_INPUT_DEFAULTS,
|
|
16
|
-
IMAGE_PROCESSING_PROMPTS,
|
|
17
|
-
} from "./wizard-strategy.constants";
|
|
18
|
-
import { buildFacePreservationPrompt } from "../../../../prompts/infrastructure/builders/face-preservation-builder";
|
|
19
|
-
import { buildInteractionStylePrompt, type InteractionStyle } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
|
|
9
|
+
import { DEFAULT_STYLE_VALUE, IMAGE_PROCESSING_PROMPTS } from "./wizard-strategy.constants";
|
|
10
|
+
import type { InteractionStyle } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
|
|
20
11
|
import { extractPrompt, extractSelection } from "../utils";
|
|
12
|
+
import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
|
|
13
|
+
import { executeImageGeneration } from "./image-generation.executor";
|
|
14
|
+
import type { ImageGenerationInput, CreateImageStrategyOptions } from "./image-generation.types";
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Types
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
export interface ImageGenerationInput {
|
|
29
|
-
/** Photos are optional for text-to-image */
|
|
30
|
-
readonly photos: readonly string[];
|
|
31
|
-
readonly prompt: string;
|
|
32
|
-
/** Optional interaction style for multi-person images */
|
|
33
|
-
readonly interactionStyle?: InteractionStyle;
|
|
34
|
-
/** Optional style from wizard selection */
|
|
35
|
-
readonly style?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ImageGenerationResult {
|
|
39
|
-
readonly imageUrl: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// Photo Extraction
|
|
44
|
-
// ============================================================================
|
|
45
|
-
|
|
46
|
-
async function extractPhotosFromWizardData(
|
|
47
|
-
wizardData: Record<string, unknown>,
|
|
48
|
-
): Promise<string[]> {
|
|
49
|
-
const photoKeys = Object.keys(wizardData)
|
|
50
|
-
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
51
|
-
.sort();
|
|
52
|
-
|
|
53
|
-
if (photoKeys.length === 0) {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const photoUris: string[] = [];
|
|
58
|
-
for (const key of photoKeys) {
|
|
59
|
-
const photo = wizardData[key] as { uri?: string };
|
|
60
|
-
if (photo?.uri) {
|
|
61
|
-
photoUris.push(photo.uri);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (photoUris.length === 0) {
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
|
|
70
|
-
return photosBase64.filter(Boolean) as string[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Image Generation Executor
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
async function executeImageGeneration(
|
|
78
|
-
input: ImageGenerationInput,
|
|
79
|
-
model: string,
|
|
80
|
-
onProgress?: (progress: number) => void,
|
|
81
|
-
): Promise<{ success: boolean; imageUrl?: string; error?: string }> {
|
|
82
|
-
const { providerRegistry } = await import("../../../../../infrastructure/services/provider-registry.service");
|
|
83
|
-
|
|
84
|
-
const provider = providerRegistry.getActiveProvider();
|
|
85
|
-
if (!provider?.isInitialized()) {
|
|
86
|
-
return { success: false, error: "AI provider not initialized" };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const formatBase64 = (base64: string): string =>
|
|
91
|
-
base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
|
|
92
|
-
|
|
93
|
-
const hasPhotos = input.photos.length > 0;
|
|
94
|
-
const imageUrls = hasPhotos ? input.photos.map(formatBase64) : [];
|
|
95
|
-
|
|
96
|
-
let finalPrompt = input.prompt;
|
|
97
|
-
|
|
98
|
-
if (hasPhotos) {
|
|
99
|
-
// Photo-based: Build face preservation prompt
|
|
100
|
-
const facePrompt = buildFacePreservationPrompt({
|
|
101
|
-
scenarioPrompt: input.prompt,
|
|
102
|
-
personCount: imageUrls.length,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const interactionPrompt = buildInteractionStylePrompt({
|
|
106
|
-
style: input.interactionStyle ?? "romantic",
|
|
107
|
-
personCount: imageUrls.length,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
finalPrompt = interactionPrompt
|
|
111
|
-
? `${facePrompt}\n\n${interactionPrompt}`
|
|
112
|
-
: facePrompt;
|
|
113
|
-
|
|
114
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
-
console.log("[ImageStrategy] Photo-based generation for", imageUrls.length, "person(s)");
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
// Text-to-image: Use prompt with optional style
|
|
119
|
-
if (input.style && input.style !== DEFAULT_STYLE_VALUE) {
|
|
120
|
-
finalPrompt = `${input.prompt}. Style: ${input.style}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
124
|
-
console.log("[ImageStrategy] Text-to-image generation");
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const modelInput: Record<string, unknown> = {
|
|
129
|
-
prompt: finalPrompt,
|
|
130
|
-
aspect_ratio: MODEL_INPUT_DEFAULTS.aspectRatio,
|
|
131
|
-
output_format: MODEL_INPUT_DEFAULTS.outputFormat,
|
|
132
|
-
num_images: MODEL_INPUT_DEFAULTS.numImages,
|
|
133
|
-
enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
if (hasPhotos) {
|
|
137
|
-
modelInput.image_urls = imageUrls;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let lastStatus = "";
|
|
141
|
-
const result = await provider.subscribe(model, modelInput, {
|
|
142
|
-
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
143
|
-
onQueueUpdate: (status) => {
|
|
144
|
-
if (status.status !== lastStatus) lastStatus = status.status;
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const rawResult = result as Record<string, unknown>;
|
|
149
|
-
const data = (rawResult?.data ?? rawResult) as { images?: Array<{ url: string }> };
|
|
150
|
-
const imageUrl = data?.images?.[0]?.url;
|
|
151
|
-
|
|
152
|
-
onProgress?.(100);
|
|
153
|
-
|
|
154
|
-
return imageUrl ? { success: true, imageUrl } : { success: false, error: "No image generated" };
|
|
155
|
-
} catch (error) {
|
|
156
|
-
return { success: false, error: error instanceof Error ? error.message : "Generation failed" };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
16
|
+
// Re-export types for external use
|
|
17
|
+
export type { ImageGenerationInput, ImageGenerationResult, CreateImageStrategyOptions } from "./image-generation.types";
|
|
159
18
|
|
|
160
19
|
// ============================================================================
|
|
161
20
|
// Input Builder
|
|
@@ -165,69 +24,62 @@ export async function buildImageInput(
|
|
|
165
24
|
wizardData: Record<string, unknown>,
|
|
166
25
|
scenario: WizardScenarioData,
|
|
167
26
|
): Promise<ImageGenerationInput | null> {
|
|
168
|
-
const photos = await
|
|
27
|
+
const photos = await extractPhotosAsBase64(wizardData);
|
|
169
28
|
|
|
170
|
-
// Extract prompt
|
|
29
|
+
// Extract prompt with fallback to default
|
|
171
30
|
let prompt = extractPrompt(wizardData, scenario.aiPrompt);
|
|
172
31
|
|
|
173
|
-
// For image processing features, use default prompt if none provided
|
|
174
32
|
if (!prompt) {
|
|
175
33
|
const defaultPrompt = IMAGE_PROCESSING_PROMPTS[scenario.id];
|
|
176
34
|
if (defaultPrompt) {
|
|
177
35
|
prompt = defaultPrompt;
|
|
178
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
179
|
-
console.log("[ImageStrategy] Using default prompt for", scenario.id);
|
|
180
|
-
}
|
|
181
36
|
} else {
|
|
182
37
|
throw new Error("Prompt is required for image generation");
|
|
183
38
|
}
|
|
184
39
|
}
|
|
185
40
|
|
|
186
|
-
//
|
|
41
|
+
// Apply style enhancements for photo-based generation
|
|
187
42
|
let finalPrompt = prompt;
|
|
188
43
|
if (photos.length > 0) {
|
|
189
|
-
|
|
44
|
+
finalPrompt = applyStyleEnhancements(prompt, wizardData);
|
|
45
|
+
}
|
|
190
46
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
47
|
+
// Extract style for text-to-image
|
|
48
|
+
const styleValue = extractSelection(wizardData.style);
|
|
49
|
+
const style = typeof styleValue === "string" ? styleValue : undefined;
|
|
50
|
+
const interactionStyle = (scenario.interactionStyle as InteractionStyle) ?? "romantic";
|
|
196
51
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
styleEnhancements.push(`Art style: ${artStyle}`);
|
|
200
|
-
}
|
|
52
|
+
return { photos, prompt: finalPrompt, style, interactionStyle };
|
|
53
|
+
}
|
|
201
54
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Applies style enhancements to the prompt
|
|
57
|
+
*/
|
|
58
|
+
function applyStyleEnhancements(prompt: string, wizardData: Record<string, unknown>): string {
|
|
59
|
+
const enhancements: string[] = [];
|
|
206
60
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
61
|
+
const romanticMoods = extractSelection(wizardData.selection_romantic_mood);
|
|
62
|
+
if (Array.isArray(romanticMoods) && romanticMoods.length > 0) {
|
|
63
|
+
enhancements.push(`Mood: ${romanticMoods.join(", ")}`);
|
|
210
64
|
}
|
|
211
65
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
66
|
+
const artStyle = extractSelection(wizardData.selection_art_style);
|
|
67
|
+
if (typeof artStyle === "string" && artStyle !== DEFAULT_STYLE_VALUE) {
|
|
68
|
+
enhancements.push(`Art style: ${artStyle}`);
|
|
69
|
+
}
|
|
215
70
|
|
|
216
|
-
|
|
217
|
-
|
|
71
|
+
const artist = extractSelection(wizardData.selection_artist_style);
|
|
72
|
+
if (typeof artist === "string" && artist !== DEFAULT_STYLE_VALUE) {
|
|
73
|
+
enhancements.push(`Artist style: ${artist}`);
|
|
74
|
+
}
|
|
218
75
|
|
|
219
|
-
return
|
|
76
|
+
return enhancements.length > 0 ? `${prompt}. ${enhancements.join(", ")}` : prompt;
|
|
220
77
|
}
|
|
221
78
|
|
|
222
79
|
// ============================================================================
|
|
223
80
|
// Strategy Factory
|
|
224
81
|
// ============================================================================
|
|
225
82
|
|
|
226
|
-
export interface CreateImageStrategyOptions {
|
|
227
|
-
readonly scenario: WizardScenarioData;
|
|
228
|
-
readonly collectionName?: string;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
83
|
export function createImageStrategy(options: CreateImageStrategyOptions): WizardStrategy {
|
|
232
84
|
const { scenario, collectionName = "creations" } = options;
|
|
233
85
|
const repository = createCreationsRepository(collectionName);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generation Types
|
|
3
|
+
* Type definitions for image generation strategy
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { InteractionStyle } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
|
|
7
|
+
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
8
|
+
|
|
9
|
+
export interface ImageGenerationInput {
|
|
10
|
+
/** Photos are optional for text-to-image */
|
|
11
|
+
readonly photos: readonly string[];
|
|
12
|
+
readonly prompt: string;
|
|
13
|
+
/** Optional interaction style for multi-person images */
|
|
14
|
+
readonly interactionStyle?: InteractionStyle;
|
|
15
|
+
/** Optional style from wizard selection */
|
|
16
|
+
readonly style?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImageGenerationResult {
|
|
20
|
+
readonly imageUrl: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CreateImageStrategyOptions {
|
|
24
|
+
readonly scenario: WizardScenarioData;
|
|
25
|
+
readonly collectionName?: string;
|
|
26
|
+
}
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo Extraction Utilities
|
|
3
|
+
* Shared photo extraction logic for wizard strategies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
7
|
+
import { PHOTO_KEY_PREFIX } from "../wizard-strategy.constants";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts photo URIs from wizard data
|
|
13
|
+
*/
|
|
14
|
+
function extractPhotoUris(wizardData: Record<string, unknown>): string[] {
|
|
15
|
+
const photoKeys = Object.keys(wizardData)
|
|
16
|
+
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
17
|
+
.sort();
|
|
18
|
+
|
|
19
|
+
if (photoKeys.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const photoUris: string[] = [];
|
|
24
|
+
for (const key of photoKeys) {
|
|
25
|
+
const photo = wizardData[key] as { uri?: string };
|
|
26
|
+
if (photo?.uri) {
|
|
27
|
+
photoUris.push(photo.uri);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return photoUris;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extracts and converts photos to base64 from wizard data
|
|
36
|
+
* Used by both image and video strategies
|
|
37
|
+
*/
|
|
38
|
+
export async function extractPhotosAsBase64(
|
|
39
|
+
wizardData: Record<string, unknown>,
|
|
40
|
+
enableDebugLogs = false,
|
|
41
|
+
): Promise<string[]> {
|
|
42
|
+
if (enableDebugLogs && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
console.log("[PhotoExtraction] Starting extraction", {
|
|
44
|
+
wizardDataKeys: Object.keys(wizardData),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const photoUris = extractPhotoUris(wizardData);
|
|
49
|
+
|
|
50
|
+
if (enableDebugLogs && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
|
+
console.log("[PhotoExtraction] Found photo URIs", { count: photoUris.length });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (photoUris.length === 0) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
|
|
59
|
+
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
60
|
+
|
|
61
|
+
if (enableDebugLogs && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
62
|
+
console.log("[PhotoExtraction] Converted photos", {
|
|
63
|
+
total: photoUris.length,
|
|
64
|
+
valid: validPhotos.length,
|
|
65
|
+
sizes: validPhotos.map((p) => `${(p.length / 1024).toFixed(1)}KB`),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return validPhotos;
|
|
70
|
+
}
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -3,122 +3,20 @@
|
|
|
3
3
|
* Handles video-specific generation logic
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileAsBase64 } from "@umituz/react-native-design-system";
|
|
7
|
-
import type { VideoFeatureType } from "../../../../../domain/interfaces";
|
|
8
6
|
import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
|
|
9
7
|
import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
|
|
10
8
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
11
9
|
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
declare const __DEV__: boolean;
|
|
10
|
+
import { VIDEO_PROCESSING_PROMPTS } from "./wizard-strategy.constants";
|
|
15
11
|
import { extractPrompt, extractDuration, extractAspectRatio, extractResolution } from "../utils";
|
|
12
|
+
import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
|
|
13
|
+
import { getVideoFeatureType } from "./video-generation.utils";
|
|
14
|
+
import type { VideoGenerationInput, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
// Types
|
|
19
|
-
// ============================================================================
|
|
20
|
-
|
|
21
|
-
export interface VideoGenerationInput {
|
|
22
|
-
/** Source image (optional for text-to-video) */
|
|
23
|
-
readonly sourceImageBase64?: string;
|
|
24
|
-
/** Target image (optional, uses source if not provided) */
|
|
25
|
-
readonly targetImageBase64?: string;
|
|
26
|
-
readonly prompt: string;
|
|
27
|
-
/** Video duration in seconds */
|
|
28
|
-
readonly duration?: number;
|
|
29
|
-
/** Aspect ratio (e.g., "16:9", "9:16") */
|
|
30
|
-
readonly aspectRatio?: string;
|
|
31
|
-
/** Video resolution (e.g., "720p", "1080p") */
|
|
32
|
-
readonly resolution?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface VideoGenerationResult {
|
|
36
|
-
readonly videoUrl: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ============================================================================
|
|
40
|
-
// Photo Extraction
|
|
41
|
-
// ============================================================================
|
|
42
|
-
|
|
43
|
-
async function extractPhotosFromWizardData(
|
|
44
|
-
wizardData: Record<string, unknown>,
|
|
45
|
-
): Promise<string[]> {
|
|
46
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
-
console.log("[VideoStrategy:extractPhotos] Starting extraction", {
|
|
48
|
-
wizardDataKeys: Object.keys(wizardData),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const photoKeys = Object.keys(wizardData)
|
|
53
|
-
.filter((k) => k.includes(PHOTO_KEY_PREFIX))
|
|
54
|
-
.sort();
|
|
55
|
-
|
|
56
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
-
console.log("[VideoStrategy:extractPhotos] Found photo keys", { photoKeys });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (photoKeys.length === 0) {
|
|
61
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
62
|
-
console.log("[VideoStrategy:extractPhotos] No photo keys found");
|
|
63
|
-
}
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const photoUris: string[] = [];
|
|
68
|
-
for (const key of photoKeys) {
|
|
69
|
-
const photo = wizardData[key] as { uri?: string };
|
|
70
|
-
if (photo?.uri) {
|
|
71
|
-
photoUris.push(photo.uri);
|
|
72
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
73
|
-
console.log("[VideoStrategy:extractPhotos] Found photo URI", { key, uri: photo.uri.substring(0, 50) + "..." });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (photoUris.length === 0) {
|
|
79
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
|
-
console.log("[VideoStrategy:extractPhotos] No photo URIs found");
|
|
81
|
-
}
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
86
|
-
console.log("[VideoStrategy:extractPhotos] Converting to base64", { count: photoUris.length });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
|
|
90
|
-
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
91
|
-
|
|
92
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
93
|
-
console.log("[VideoStrategy:extractPhotos] Converted photos", {
|
|
94
|
-
total: photoUris.length,
|
|
95
|
-
valid: validPhotos.length,
|
|
96
|
-
sizes: validPhotos.map((p) => `${(p.length / 1024).toFixed(1)}KB`),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return validPhotos;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// Video Feature Type Detection
|
|
105
|
-
// ============================================================================
|
|
106
|
-
|
|
107
|
-
function getVideoFeatureType(scenarioId: string): VideoFeatureType {
|
|
108
|
-
const id = scenarioId.toLowerCase();
|
|
109
|
-
|
|
110
|
-
for (const [pattern, featureType] of Object.entries(VIDEO_FEATURE_PATTERNS)) {
|
|
111
|
-
if (id.includes(pattern)) {
|
|
112
|
-
return featureType;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
16
|
+
declare const __DEV__: boolean;
|
|
115
17
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.log("[VideoStrategy:getFeatureType] Defaulting to image-to-video for content scenario", { scenarioId });
|
|
119
|
-
}
|
|
120
|
-
return "image-to-video";
|
|
121
|
-
}
|
|
18
|
+
// Re-export types for external use
|
|
19
|
+
export type { VideoGenerationInput, VideoGenerationResult, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
122
20
|
|
|
123
21
|
// ============================================================================
|
|
124
22
|
// Input Builder
|
|
@@ -129,79 +27,37 @@ export async function buildVideoInput(
|
|
|
129
27
|
scenario: WizardScenarioData,
|
|
130
28
|
): Promise<VideoGenerationInput | null> {
|
|
131
29
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
132
|
-
console.log("[VideoStrategy
|
|
133
|
-
scenarioId: scenario.id,
|
|
134
|
-
outputType: scenario.outputType,
|
|
135
|
-
hasAiPrompt: !!scenario.aiPrompt,
|
|
136
|
-
});
|
|
30
|
+
console.log("[VideoStrategy] Building input", { scenarioId: scenario.id });
|
|
137
31
|
}
|
|
138
32
|
|
|
139
|
-
const photos = await
|
|
140
|
-
|
|
141
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
142
|
-
console.log("[VideoStrategy:buildInput] Photos extracted", {
|
|
143
|
-
count: photos.length,
|
|
144
|
-
hasSource: !!photos[0],
|
|
145
|
-
hasTarget: !!photos[1],
|
|
146
|
-
});
|
|
147
|
-
}
|
|
33
|
+
const photos = await extractPhotosAsBase64(wizardData, true);
|
|
148
34
|
|
|
149
|
-
// Extract prompt
|
|
35
|
+
// Extract prompt with fallback to default
|
|
150
36
|
let prompt = extractPrompt(wizardData, scenario.aiPrompt);
|
|
151
37
|
|
|
152
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
153
|
-
console.log("[VideoStrategy:buildInput] Prompt from wizard", {
|
|
154
|
-
hasPrompt: !!prompt,
|
|
155
|
-
promptLength: prompt?.length ?? 0,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// For video processing features, use default prompt if none provided
|
|
160
38
|
if (!prompt) {
|
|
161
39
|
const defaultPrompt = VIDEO_PROCESSING_PROMPTS[scenario.id];
|
|
162
40
|
if (defaultPrompt) {
|
|
163
41
|
prompt = defaultPrompt;
|
|
164
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
165
|
-
console.log("[VideoStrategy:buildInput] Using default prompt", {
|
|
166
|
-
scenarioId: scenario.id,
|
|
167
|
-
prompt: prompt.substring(0, 50) + "...",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
42
|
} else {
|
|
171
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
172
|
-
console.log("[VideoStrategy:buildInput] ERROR: No prompt available", {
|
|
173
|
-
scenarioId: scenario.id,
|
|
174
|
-
availablePrompts: Object.keys(VIDEO_PROCESSING_PROMPTS),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
43
|
throw new Error("Prompt is required for video generation");
|
|
178
44
|
}
|
|
179
45
|
}
|
|
180
46
|
|
|
181
|
-
// Extract video generation parameters
|
|
182
|
-
const duration = extractDuration(wizardData);
|
|
183
|
-
const aspectRatio = extractAspectRatio(wizardData);
|
|
184
|
-
const resolution = extractResolution(wizardData);
|
|
185
|
-
|
|
186
47
|
const input: VideoGenerationInput = {
|
|
187
48
|
sourceImageBase64: photos[0],
|
|
188
49
|
targetImageBase64: photos[1] || photos[0],
|
|
189
50
|
prompt,
|
|
190
|
-
duration,
|
|
191
|
-
aspectRatio,
|
|
192
|
-
resolution,
|
|
51
|
+
duration: extractDuration(wizardData),
|
|
52
|
+
aspectRatio: extractAspectRatio(wizardData),
|
|
53
|
+
resolution: extractResolution(wizardData),
|
|
193
54
|
};
|
|
194
55
|
|
|
195
56
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
196
|
-
console.log("[VideoStrategy
|
|
57
|
+
console.log("[VideoStrategy] Input built", {
|
|
197
58
|
hasSource: !!input.sourceImageBase64,
|
|
198
59
|
hasTarget: !!input.targetImageBase64,
|
|
199
|
-
sourceSize: input.sourceImageBase64 ? `${(input.sourceImageBase64.length / 1024).toFixed(1)}KB` : "N/A",
|
|
200
|
-
targetSize: input.targetImageBase64 ? `${(input.targetImageBase64.length / 1024).toFixed(1)}KB` : "N/A",
|
|
201
|
-
prompt: input.prompt.substring(0, 50) + "...",
|
|
202
60
|
duration: input.duration,
|
|
203
|
-
aspectRatio: input.aspectRatio,
|
|
204
|
-
resolution: input.resolution,
|
|
205
61
|
});
|
|
206
62
|
}
|
|
207
63
|
|
|
@@ -212,22 +68,13 @@ export async function buildVideoInput(
|
|
|
212
68
|
// Strategy Factory
|
|
213
69
|
// ============================================================================
|
|
214
70
|
|
|
215
|
-
export interface CreateVideoStrategyOptions {
|
|
216
|
-
readonly scenario: WizardScenarioData;
|
|
217
|
-
readonly collectionName?: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
71
|
export function createVideoStrategy(options: CreateVideoStrategyOptions): WizardStrategy {
|
|
221
72
|
const { scenario, collectionName = "creations" } = options;
|
|
222
73
|
const repository = createCreationsRepository(collectionName);
|
|
223
74
|
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
224
75
|
|
|
225
76
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
226
|
-
console.log("[VideoStrategy
|
|
227
|
-
scenarioId: scenario.id,
|
|
228
|
-
videoFeatureType,
|
|
229
|
-
collectionName,
|
|
230
|
-
});
|
|
77
|
+
console.log("[VideoStrategy] Created", { scenarioId: scenario.id, videoFeatureType });
|
|
231
78
|
}
|
|
232
79
|
|
|
233
80
|
let lastInputRef: VideoGenerationInput | null = null;
|
|
@@ -237,51 +84,21 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
237
84
|
const videoInput = input as VideoGenerationInput;
|
|
238
85
|
lastInputRef = videoInput;
|
|
239
86
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const result = await executeVideoFeature(
|
|
250
|
-
videoFeatureType,
|
|
251
|
-
{
|
|
252
|
-
sourceImageBase64: videoInput.sourceImageBase64,
|
|
253
|
-
targetImageBase64: videoInput.targetImageBase64,
|
|
254
|
-
prompt: videoInput.prompt,
|
|
255
|
-
options: {
|
|
256
|
-
duration: videoInput.duration,
|
|
257
|
-
aspect_ratio: videoInput.aspectRatio,
|
|
258
|
-
resolution: videoInput.resolution,
|
|
259
|
-
},
|
|
87
|
+
const result = await executeVideoFeature(videoFeatureType, {
|
|
88
|
+
sourceImageBase64: videoInput.sourceImageBase64,
|
|
89
|
+
targetImageBase64: videoInput.targetImageBase64,
|
|
90
|
+
prompt: videoInput.prompt,
|
|
91
|
+
options: {
|
|
92
|
+
duration: videoInput.duration,
|
|
93
|
+
aspect_ratio: videoInput.aspectRatio,
|
|
94
|
+
resolution: videoInput.resolution,
|
|
260
95
|
},
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
264
|
-
console.log("[VideoStrategy:execute] RESULT", {
|
|
265
|
-
success: result.success,
|
|
266
|
-
hasVideoUrl: !!result.videoUrl,
|
|
267
|
-
error: result.error,
|
|
268
|
-
requestId: result.requestId,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
96
|
+
});
|
|
271
97
|
|
|
272
98
|
if (!result.success || !result.videoUrl) {
|
|
273
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
274
|
-
console.log("[VideoStrategy:execute] FAILED", { error: result.error });
|
|
275
|
-
}
|
|
276
99
|
throw new Error(result.error || "Video generation failed");
|
|
277
100
|
}
|
|
278
101
|
|
|
279
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
280
|
-
console.log("[VideoStrategy:execute] SUCCESS", {
|
|
281
|
-
videoUrl: result.videoUrl.substring(0, 80) + "...",
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
102
|
return { videoUrl: result.videoUrl };
|
|
286
103
|
},
|
|
287
104
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Types
|
|
3
|
+
* Type definitions for video generation strategy
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
7
|
+
|
|
8
|
+
export interface VideoGenerationInput {
|
|
9
|
+
/** Source image (optional for text-to-video) */
|
|
10
|
+
readonly sourceImageBase64?: string;
|
|
11
|
+
/** Target image (optional, uses source if not provided) */
|
|
12
|
+
readonly targetImageBase64?: string;
|
|
13
|
+
readonly prompt: string;
|
|
14
|
+
/** Video duration in seconds */
|
|
15
|
+
readonly duration?: number;
|
|
16
|
+
/** Aspect ratio (e.g., "16:9", "9:16") */
|
|
17
|
+
readonly aspectRatio?: string;
|
|
18
|
+
/** Video resolution (e.g., "720p", "1080p") */
|
|
19
|
+
readonly resolution?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VideoGenerationResult {
|
|
23
|
+
readonly videoUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CreateVideoStrategyOptions {
|
|
27
|
+
readonly scenario: WizardScenarioData;
|
|
28
|
+
readonly collectionName?: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Utilities
|
|
3
|
+
* Video-specific utility functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { VideoFeatureType } from "../../../../../domain/interfaces";
|
|
7
|
+
import { VIDEO_FEATURE_PATTERNS } from "./wizard-strategy.constants";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determines the video feature type based on scenario ID
|
|
13
|
+
*/
|
|
14
|
+
export function getVideoFeatureType(scenarioId: string): VideoFeatureType {
|
|
15
|
+
const id = scenarioId.toLowerCase();
|
|
16
|
+
|
|
17
|
+
for (const [pattern, featureType] of Object.entries(VIDEO_FEATURE_PATTERNS)) {
|
|
18
|
+
if (id.includes(pattern)) {
|
|
19
|
+
return featureType;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Default to image-to-video for content scenarios
|
|
24
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
25
|
+
console.log("[VideoUtils] Defaulting to image-to-video", { scenarioId });
|
|
26
|
+
}
|
|
27
|
+
return "image-to-video";
|
|
28
|
+
}
|
|
@@ -43,7 +43,7 @@ export interface GenericWizardFlowProps {
|
|
|
43
43
|
readonly scenario?: WizardScenarioData;
|
|
44
44
|
readonly scenarioId?: string;
|
|
45
45
|
readonly userId?: string;
|
|
46
|
-
readonly alertMessages
|
|
46
|
+
readonly alertMessages: AlertMessages;
|
|
47
47
|
readonly skipResultStep?: boolean;
|
|
48
48
|
readonly onStepChange?: (stepId: string, stepType: StepType | string) => void;
|
|
49
49
|
readonly onGenerationStart?: (
|
|
@@ -89,14 +89,18 @@ export {
|
|
|
89
89
|
PHOTOREALISTIC_RENDERING,
|
|
90
90
|
NATURAL_POSE_GUIDELINES,
|
|
91
91
|
MASTER_BASE_PROMPT,
|
|
92
|
-
MULTI_PERSON_PRESERVATION_RULES,
|
|
93
92
|
createEnhancedPrompt,
|
|
94
93
|
createTransformationPrompt,
|
|
95
94
|
enhanceExistingPrompt,
|
|
96
|
-
createMultiPersonPrompt,
|
|
97
95
|
} from './domain/entities/BasePromptStructure';
|
|
98
96
|
export type { CreatePromptOptions } from './domain/entities/BasePromptStructure';
|
|
99
97
|
|
|
98
|
+
export {
|
|
99
|
+
MULTI_PERSON_PRESERVATION_RULES,
|
|
100
|
+
createMultiPersonPrompt,
|
|
101
|
+
} from './domain/entities/MultiPersonPromptStructure';
|
|
102
|
+
export type { MultiPersonPreservationRules } from './domain/entities/MultiPersonPromptStructure';
|
|
103
|
+
|
|
100
104
|
export {
|
|
101
105
|
buildFacePreservationPrompt,
|
|
102
106
|
buildMinimalFacePreservationPrompt,
|