@umituz/react-native-ai-generation-content 1.26.25 → 1.26.28
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/wizard-strategy.factory.ts +132 -70
- package/src/domains/generation/wizard/presentation/components/GenericWizardFlow.tsx +65 -1
- package/src/domains/scenarios/domain/scenario.types.ts +1 -1
- package/src/domains/scenarios/presentation/screens/MainCategoryScreen.tsx +1 -2
- package/src/domains/scenarios/presentation/screens/SubCategoryScreen.tsx +2 -3
- package/src/infrastructure/services/image-feature-executor.service.ts +3 -57
- package/src/infrastructure/services/index.ts +1 -1
- package/src/infrastructure/services/video-feature-executor.service.ts +2 -2
- package/src/infrastructure/services/video-feature-executor.types.ts +1 -4
- package/src/infrastructure/utils/index.ts +0 -1
- package/src/infrastructure/utils/url-extractor/extraction-rules.ts +57 -0
- package/src/infrastructure/utils/url-extractor/image-result-extractor.ts +21 -0
- package/src/infrastructure/utils/url-extractor/index.ts +18 -17
- package/src/infrastructure/utils/url-extractor/rule-executor.ts +67 -0
- package/src/infrastructure/utils/url-extractor/video-result-extractor.ts +21 -0
- package/src/infrastructure/utils/url-extractor.util.ts +11 -0
- package/src/infrastructure/utils/video-result-extractor.util.ts +0 -59
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.28",
|
|
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",
|
|
@@ -18,8 +18,7 @@ declare const __DEV__: boolean;
|
|
|
18
18
|
// ============================================================================
|
|
19
19
|
|
|
20
20
|
interface ImageGenerationInput {
|
|
21
|
-
readonly
|
|
22
|
-
readonly partnerBBase64: string;
|
|
21
|
+
readonly photos: readonly string[]; // Dynamic array - supports 1, 2, 3... N photos
|
|
23
22
|
readonly prompt: string;
|
|
24
23
|
}
|
|
25
24
|
|
|
@@ -44,8 +43,7 @@ async function executeImageGeneration(
|
|
|
44
43
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
44
|
console.log("[WizardStrategy] executeImageGeneration ENTRY", {
|
|
46
45
|
receivedModel: model,
|
|
47
|
-
|
|
48
|
-
hasPartnerB: !!input.partnerBBase64,
|
|
46
|
+
photoCount: input.photos.length,
|
|
49
47
|
promptLength: input.prompt.length,
|
|
50
48
|
});
|
|
51
49
|
}
|
|
@@ -64,12 +62,10 @@ async function executeImageGeneration(
|
|
|
64
62
|
return base64.startsWith("data:") ? base64 : `data:image/jpeg;base64,${base64}`;
|
|
65
63
|
};
|
|
66
64
|
|
|
67
|
-
const imageUrls =
|
|
68
|
-
.filter(Boolean)
|
|
69
|
-
.map(formatBase64);
|
|
65
|
+
const imageUrls = input.photos.map(formatBase64);
|
|
70
66
|
|
|
71
|
-
if (imageUrls.length
|
|
72
|
-
return { success: false, error: "
|
|
67
|
+
if (imageUrls.length === 0) {
|
|
68
|
+
return { success: false, error: "At least one image required" };
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
onProgress?.(10);
|
|
@@ -125,105 +121,171 @@ async function executeImageGeneration(
|
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
// ============================================================================
|
|
128
|
-
//
|
|
124
|
+
// Photo Extraction (Generic - used by both image and video)
|
|
129
125
|
// ============================================================================
|
|
130
126
|
|
|
131
|
-
async function
|
|
127
|
+
async function extractPhotosFromWizardData(
|
|
132
128
|
wizardData: Record<string, unknown>,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
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_"))
|
|
133
|
+
.sort(); // Sort to maintain order (photo_1, photo_2, ...)
|
|
137
134
|
|
|
138
|
-
if (
|
|
135
|
+
if (photoKeys.length === 0) {
|
|
139
136
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
140
|
-
console.error("[WizardStrategy]
|
|
137
|
+
console.error("[WizardStrategy] No photos found in wizard data", {
|
|
141
138
|
keys: Object.keys(wizardData),
|
|
142
|
-
hasPhoto1: !!photo1Key,
|
|
143
|
-
hasPhoto2: !!photo2Key,
|
|
144
139
|
});
|
|
145
140
|
}
|
|
146
141
|
return null;
|
|
147
142
|
}
|
|
148
143
|
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
158
153
|
}
|
|
159
|
-
|
|
154
|
+
photoUris.push(photo.uri);
|
|
160
155
|
}
|
|
161
156
|
|
|
162
|
-
// Convert images to base64
|
|
163
|
-
const
|
|
164
|
-
readFileAsBase64(
|
|
165
|
-
|
|
166
|
-
]);
|
|
157
|
+
// Convert all images to base64 in parallel
|
|
158
|
+
const photosBase64 = await Promise.all(
|
|
159
|
+
photoUris.map((uri) => readFileAsBase64(uri)),
|
|
160
|
+
);
|
|
167
161
|
|
|
168
|
-
|
|
162
|
+
// Filter out nulls/undefined
|
|
163
|
+
const validPhotos = photosBase64.filter(Boolean) as string[];
|
|
164
|
+
|
|
165
|
+
if (validPhotos.length === 0) {
|
|
169
166
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
170
|
-
console.error("[WizardStrategy] Failed to convert images to base64");
|
|
167
|
+
console.error("[WizardStrategy] Failed to convert any images to base64");
|
|
171
168
|
}
|
|
172
169
|
return null;
|
|
173
170
|
}
|
|
174
171
|
|
|
175
|
-
|
|
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
|
|
176
195
|
if (!scenario.aiPrompt || scenario.aiPrompt.trim() === "") {
|
|
177
196
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
178
|
-
console.error("[WizardStrategy]
|
|
197
|
+
console.error("[WizardStrategy] scenario.aiPrompt missing", {
|
|
179
198
|
scenarioId: scenario.id,
|
|
180
|
-
aiPrompt: scenario.aiPrompt,
|
|
181
199
|
});
|
|
182
200
|
}
|
|
183
201
|
throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
let prompt = scenario.aiPrompt;
|
|
187
|
-
const outputType = scenario.outputType || "video";
|
|
188
205
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
const styleEnhancements: string[] = [];
|
|
206
|
+
// Enhance prompt with style selections (image-specific)
|
|
207
|
+
const styleEnhancements: string[] = [];
|
|
192
208
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
}
|
|
198
214
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
215
|
+
// Art style (single select)
|
|
216
|
+
const artStyle = wizardData.selection_art_style as string | undefined;
|
|
217
|
+
if (artStyle && artStyle !== "original") {
|
|
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 !== "original") {
|
|
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
|
+
}
|
|
204
231
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
return {
|
|
233
|
+
photos, // Dynamic array - supports 1, 2, 3... N photos
|
|
234
|
+
prompt,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// Video Generation Input Builder
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
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");
|
|
209
254
|
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
210
257
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
});
|
|
214
264
|
}
|
|
265
|
+
throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
sourceImageBase64: photos[0],
|
|
270
|
+
targetImageBase64: photos[1] || photos[0], // Use first photo if only 1 provided
|
|
271
|
+
prompt: scenario.aiPrompt,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Input Builder Dispatcher (Routes to correct builder)
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
async function buildGenerationInput(
|
|
280
|
+
wizardData: Record<string, unknown>,
|
|
281
|
+
scenario: WizardScenarioData,
|
|
282
|
+
): Promise<WizardGenerationInput | null> {
|
|
283
|
+
const outputType = scenario.outputType || "video";
|
|
215
284
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
partnerBBase64: photo2Base64,
|
|
219
|
-
prompt,
|
|
220
|
-
} as ImageGenerationInput;
|
|
285
|
+
if (outputType === "image") {
|
|
286
|
+
return buildImageGenerationInput(wizardData, scenario);
|
|
221
287
|
} else {
|
|
222
|
-
return
|
|
223
|
-
sourceImageBase64: photo1Base64,
|
|
224
|
-
targetImageBase64: photo2Base64,
|
|
225
|
-
prompt,
|
|
226
|
-
} as VideoGenerationInput;
|
|
288
|
+
return buildVideoGenerationInput(wizardData, scenario);
|
|
227
289
|
}
|
|
228
290
|
}
|
|
229
291
|
|
|
@@ -17,10 +17,12 @@ import { View, StyleSheet } from "react-native";
|
|
|
17
17
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
18
18
|
import { useFlow } from "../../../infrastructure/flow/useFlow";
|
|
19
19
|
import { StepType, type StepDefinition } from "../../../../../domain/entities/flow-config.types";
|
|
20
|
-
import type { WizardFeatureConfig } from "../../domain/entities/wizard-config.types";
|
|
20
|
+
import type { WizardFeatureConfig, WizardStepConfig } from "../../domain/entities/wizard-config.types";
|
|
21
21
|
import { buildFlowStepsFromWizard } from "../../infrastructure/builders/dynamic-step-builder";
|
|
22
22
|
import { useWizardGeneration, type WizardScenarioData } from "../hooks/useWizardGeneration";
|
|
23
23
|
import type { AlertMessages } from "../../../../../presentation/hooks/generation/types";
|
|
24
|
+
import { PhotoStep } from "../../../../../presentation/components/photo-step/PhotoStep";
|
|
25
|
+
import { usePhotoUploadState } from "../hooks/usePhotoUploadState";
|
|
24
26
|
|
|
25
27
|
export interface GenericWizardFlowProps {
|
|
26
28
|
readonly featureConfig: WizardFeatureConfig;
|
|
@@ -232,6 +234,25 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
232
234
|
}
|
|
233
235
|
}, [currentStepIndex, previousStep, onBack]);
|
|
234
236
|
|
|
237
|
+
// Photo upload state translations (generic, used for all photo upload steps)
|
|
238
|
+
const photoUploadTranslations = useMemo(() => ({
|
|
239
|
+
fileTooLarge: t("common.errors.file_too_large"),
|
|
240
|
+
maxFileSize: t("common.errors.max_file_size"),
|
|
241
|
+
error: t("common.error"),
|
|
242
|
+
uploadFailed: t("common.errors.upload_failed"),
|
|
243
|
+
}), [t]);
|
|
244
|
+
|
|
245
|
+
const photoUploadHook = usePhotoUploadState({
|
|
246
|
+
translations: photoUploadTranslations,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Save photo when uploaded
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (photoUploadHook.image && currentStep) {
|
|
252
|
+
setCustomData(currentStep.id, photoUploadHook.image);
|
|
253
|
+
}
|
|
254
|
+
}, [photoUploadHook.image, currentStep, setCustomData]);
|
|
255
|
+
|
|
235
256
|
// Render current step
|
|
236
257
|
const renderCurrentStep = useCallback(() => {
|
|
237
258
|
const step = currentStep;
|
|
@@ -261,6 +282,49 @@ export const GenericWizardFlow: React.FC<GenericWizardFlowProps> = ({
|
|
|
261
282
|
case StepType.RESULT_PREVIEW:
|
|
262
283
|
return renderResult?.(generationResult) || null;
|
|
263
284
|
|
|
285
|
+
case StepType.PARTNER_UPLOAD: {
|
|
286
|
+
// Get wizard step config
|
|
287
|
+
const wizardConfig = step.config as WizardStepConfig;
|
|
288
|
+
|
|
289
|
+
// Use titleKey from config, fallback to step-specific translation key
|
|
290
|
+
const titleKey = wizardConfig?.titleKey || `wizard.steps.${step.id}.title`;
|
|
291
|
+
const title = t(titleKey);
|
|
292
|
+
|
|
293
|
+
// Subtitle is optional
|
|
294
|
+
const subtitle = wizardConfig?.subtitleKey ? t(wizardConfig.subtitleKey) : undefined;
|
|
295
|
+
|
|
296
|
+
// Get existing photo for this step from customData
|
|
297
|
+
const existingPhoto = customData[step.id];
|
|
298
|
+
const imageUri = existingPhoto && typeof existingPhoto === "object" && "uri" in existingPhoto
|
|
299
|
+
? (existingPhoto.uri as string)
|
|
300
|
+
: photoUploadHook.image?.uri || null;
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<PhotoStep
|
|
304
|
+
config={{
|
|
305
|
+
enabled: true,
|
|
306
|
+
order: currentStepIndex,
|
|
307
|
+
id: step.id,
|
|
308
|
+
header: {},
|
|
309
|
+
photoCard: {},
|
|
310
|
+
enableValidation: false,
|
|
311
|
+
}}
|
|
312
|
+
imageUri={imageUri}
|
|
313
|
+
isValidating={false}
|
|
314
|
+
isValid={null}
|
|
315
|
+
onPhotoSelect={photoUploadHook.handlePickImage}
|
|
316
|
+
disabled={false}
|
|
317
|
+
title={title}
|
|
318
|
+
subtitle={subtitle}
|
|
319
|
+
translations={{
|
|
320
|
+
tapToUpload: t("photoUpload.tapToUpload"),
|
|
321
|
+
selectPhoto: t("photoUpload.selectPhoto"),
|
|
322
|
+
change: t("common.change"),
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
264
328
|
default:
|
|
265
329
|
// Other step types should be handled by custom render props
|
|
266
330
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -187,7 +187,7 @@ export type CoupleFeatureId = string;
|
|
|
187
187
|
*/
|
|
188
188
|
export interface CoupleFeatureSelection {
|
|
189
189
|
readonly featureId: CoupleFeatureId | null;
|
|
190
|
-
readonly [key: string]:
|
|
190
|
+
readonly [key: string]: CoupleFeatureId | string | boolean | number | null | undefined;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
export interface ScenarioData {
|
|
@@ -69,8 +69,7 @@ export const MainCategoryScreen: React.FC<MainCategoryScreenProps> = ({
|
|
|
69
69
|
return (
|
|
70
70
|
<AtomicCard
|
|
71
71
|
image={item.image}
|
|
72
|
-
|
|
73
|
-
leftIcon={!item.image && !item.emoji ? (item.icon as any) : undefined}
|
|
72
|
+
leftIcon={item.icon}
|
|
74
73
|
title={title}
|
|
75
74
|
subtitle={description}
|
|
76
75
|
imageAspectRatio={1.5}
|
|
@@ -86,12 +86,11 @@ export const SubCategoryScreen: React.FC<SubCategoryScreenProps> = ({
|
|
|
86
86
|
return (
|
|
87
87
|
<AtomicCard
|
|
88
88
|
image={item.image}
|
|
89
|
-
leftIcon={
|
|
89
|
+
leftIcon={item.icon}
|
|
90
90
|
badge={item.emoji}
|
|
91
91
|
title={title}
|
|
92
92
|
subtitle={description}
|
|
93
|
-
|
|
94
|
-
imageAspectRatio={item.image ? 1.5 : undefined}
|
|
93
|
+
imageAspectRatio={item.image ? 1.5 : undefined}
|
|
95
94
|
onPress={() => handleSubCategoryPress(item.id)}
|
|
96
95
|
testID={`sub-category-${item.id}`}
|
|
97
96
|
style={{ marginBottom: tokens.spacing.md }}
|
|
@@ -6,15 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
import { providerRegistry } from "./provider-registry.service";
|
|
8
8
|
import { cleanBase64 } from "../utils";
|
|
9
|
+
import { extractImageResult } from "../utils/url-extractor";
|
|
10
|
+
import type { ImageResultExtractor } from "../utils/url-extractor";
|
|
9
11
|
import type { ImageFeatureType, ImageFeatureInputData } from "../../domain/interfaces";
|
|
10
12
|
|
|
11
13
|
declare const __DEV__: boolean;
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
* Result extractor function type
|
|
15
|
-
*/
|
|
16
|
-
export type ImageResultExtractor = (result: unknown) => string | undefined;
|
|
17
|
-
|
|
18
15
|
/**
|
|
19
16
|
* Execution options
|
|
20
17
|
*/
|
|
@@ -43,57 +40,6 @@ export interface ImageFeatureRequest {
|
|
|
43
40
|
options?: Record<string, unknown>;
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
/**
|
|
47
|
-
* Default result extractor - handles common response formats
|
|
48
|
-
* Supports both direct response and data-wrapped response (fal.ai)
|
|
49
|
-
*/
|
|
50
|
-
function defaultExtractImageResult(result: unknown): string | undefined {
|
|
51
|
-
if (typeof result !== "object" || result === null) return undefined;
|
|
52
|
-
|
|
53
|
-
const r = result as Record<string, unknown>;
|
|
54
|
-
|
|
55
|
-
if (__DEV__) {
|
|
56
|
-
console.log("[ImageExtractor] Result keys:", Object.keys(r));
|
|
57
|
-
console.log("[ImageExtractor] Has data:", !!r.data);
|
|
58
|
-
console.log("[ImageExtractor] Has images:", !!r.images);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Handle fal.ai data wrapper
|
|
62
|
-
const data = (r.data as Record<string, unknown>) ?? r;
|
|
63
|
-
|
|
64
|
-
if (__DEV__) {
|
|
65
|
-
console.log("[ImageExtractor] Data keys:", Object.keys(data));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Direct string values
|
|
69
|
-
if (typeof data.image === "string") return data.image;
|
|
70
|
-
if (typeof data.imageUrl === "string") return data.imageUrl;
|
|
71
|
-
if (typeof data.output === "string") return data.output;
|
|
72
|
-
|
|
73
|
-
// Object with url property (birefnet, rembg format: data.image.url)
|
|
74
|
-
const imageObj = data.image as Record<string, unknown> | undefined;
|
|
75
|
-
if (imageObj && typeof imageObj === "object" && typeof imageObj.url === "string") {
|
|
76
|
-
if (__DEV__) {
|
|
77
|
-
console.log("[ImageExtractor] Found data.image.url:", imageObj.url);
|
|
78
|
-
}
|
|
79
|
-
return imageObj.url;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Array format (flux, etc: data.images[0].url)
|
|
83
|
-
if (Array.isArray(data.images) && typeof data.images[0]?.url === "string") {
|
|
84
|
-
if (__DEV__) {
|
|
85
|
-
console.log("[ImageExtractor] Found images[0].url:", data.images[0].url);
|
|
86
|
-
}
|
|
87
|
-
return data.images[0].url;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (__DEV__) {
|
|
91
|
-
console.log("[ImageExtractor] No image URL found in result");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
43
|
/**
|
|
98
44
|
* Execute any image feature using the active provider
|
|
99
45
|
*/
|
|
@@ -143,7 +89,7 @@ export async function executeImageFeature(
|
|
|
143
89
|
|
|
144
90
|
onProgress?.(90);
|
|
145
91
|
|
|
146
|
-
const extractor = extractResult ??
|
|
92
|
+
const extractor = extractResult ?? extractImageResult;
|
|
147
93
|
const imageUrl = extractor(result);
|
|
148
94
|
|
|
149
95
|
onProgress?.(100);
|
|
@@ -17,11 +17,11 @@ export {
|
|
|
17
17
|
hasImageFeatureSupport,
|
|
18
18
|
} from "./image-feature-executor.service";
|
|
19
19
|
export type {
|
|
20
|
-
ImageResultExtractor,
|
|
21
20
|
ExecuteImageFeatureOptions,
|
|
22
21
|
ImageFeatureResult,
|
|
23
22
|
ImageFeatureRequest,
|
|
24
23
|
} from "./image-feature-executor.service";
|
|
24
|
+
export type { ImageResultExtractor } from "../utils/url-extractor";
|
|
25
25
|
export {
|
|
26
26
|
executeVideoFeature,
|
|
27
27
|
hasVideoFeatureSupport,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { providerRegistry } from "./provider-registry.service";
|
|
8
8
|
import { cleanBase64 } from "../utils";
|
|
9
|
-
import {
|
|
9
|
+
import { extractVideoResult } from "../utils/url-extractor";
|
|
10
10
|
import type { VideoFeatureType, VideoFeatureInputData } from "../../domain/interfaces";
|
|
11
11
|
import type {
|
|
12
12
|
ExecuteVideoFeatureOptions,
|
|
@@ -77,7 +77,7 @@ export async function executeVideoFeature(
|
|
|
77
77
|
|
|
78
78
|
onProgress?.(90);
|
|
79
79
|
|
|
80
|
-
const extractor = extractResult ??
|
|
80
|
+
const extractor = extractResult ?? extractVideoResult;
|
|
81
81
|
const videoUrl = extractor(result);
|
|
82
82
|
|
|
83
83
|
onProgress?.(100);
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* Video Feature Executor Type Definitions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* Result extractor function type
|
|
7
|
-
*/
|
|
8
|
-
export type VideoResultExtractor = (result: unknown) => string | undefined;
|
|
5
|
+
import type { VideoResultExtractor } from "../utils/url-extractor";
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* Execution options
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extraction Rules
|
|
3
|
+
* Declarative configuration for AI provider response formats
|
|
4
|
+
* OCP compliant - add new formats without modifying extraction logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Rule definition for extracting URL from response
|
|
9
|
+
*/
|
|
10
|
+
export interface ExtractionRule {
|
|
11
|
+
/** Field path to check: ['data', 'image', 'url'] */
|
|
12
|
+
path: readonly string[];
|
|
13
|
+
/** Human-readable description for debugging */
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Image extraction rules - checked in order, first success wins
|
|
19
|
+
* Supports: FAL.ai wrapper, birefnet, rembg, flux, and direct formats
|
|
20
|
+
*/
|
|
21
|
+
export const IMAGE_EXTRACTION_RULES: readonly ExtractionRule[] = [
|
|
22
|
+
// FAL.ai data wrapper formats
|
|
23
|
+
{ path: ["data", "image"], description: "data.image (string)" },
|
|
24
|
+
{ path: ["data", "imageUrl"], description: "data.imageUrl" },
|
|
25
|
+
{ path: ["data", "output"], description: "data.output" },
|
|
26
|
+
{ path: ["data", "image", "url"], description: "data.image.url (birefnet/rembg)" },
|
|
27
|
+
{ path: ["data", "images", "0", "url"], description: "data.images[0].url (flux)" },
|
|
28
|
+
// Direct formats (no wrapper)
|
|
29
|
+
{ path: ["image"], description: "image (string)" },
|
|
30
|
+
{ path: ["imageUrl"], description: "imageUrl" },
|
|
31
|
+
{ path: ["output"], description: "output" },
|
|
32
|
+
{ path: ["image", "url"], description: "image.url" },
|
|
33
|
+
{ path: ["images", "0", "url"], description: "images[0].url" },
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Video extraction rules - checked in order, first success wins
|
|
38
|
+
* Supports: FAL.ai wrapper, direct formats, nested objects, arrays
|
|
39
|
+
*/
|
|
40
|
+
export const VIDEO_EXTRACTION_RULES: readonly ExtractionRule[] = [
|
|
41
|
+
// FAL.ai data wrapper formats
|
|
42
|
+
{ path: ["data", "video"], description: "data.video (string)" },
|
|
43
|
+
{ path: ["data", "videoUrl"], description: "data.videoUrl" },
|
|
44
|
+
{ path: ["data", "video_url"], description: "data.video_url" },
|
|
45
|
+
{ path: ["data", "output"], description: "data.output" },
|
|
46
|
+
{ path: ["data", "url"], description: "data.url" },
|
|
47
|
+
{ path: ["data", "video", "url"], description: "data.video.url" },
|
|
48
|
+
{ path: ["data", "videos", "0", "url"], description: "data.videos[0].url" },
|
|
49
|
+
// Direct formats (no wrapper)
|
|
50
|
+
{ path: ["video"], description: "video (string)" },
|
|
51
|
+
{ path: ["videoUrl"], description: "videoUrl" },
|
|
52
|
+
{ path: ["video_url"], description: "video_url" },
|
|
53
|
+
{ path: ["output"], description: "output" },
|
|
54
|
+
{ path: ["url"], description: "url" },
|
|
55
|
+
{ path: ["video", "url"], description: "video.url" },
|
|
56
|
+
{ path: ["videos", "0", "url"], description: "videos[0].url" },
|
|
57
|
+
] as const;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Result Extractor
|
|
3
|
+
* Extracts image URL from AI provider responses using declarative rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IMAGE_EXTRACTION_RULES } from "./extraction-rules";
|
|
7
|
+
import { executeRules } from "./rule-executor";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Result extractor function type
|
|
11
|
+
*/
|
|
12
|
+
export type ImageResultExtractor = (result: unknown) => string | undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract image URL from AI generation result
|
|
16
|
+
* Uses Chain of Responsibility pattern with declarative rules
|
|
17
|
+
* Supports: FAL.ai wrapper, birefnet, rembg, flux, and direct formats
|
|
18
|
+
*/
|
|
19
|
+
export const extractImageResult: ImageResultExtractor = (result) => {
|
|
20
|
+
return executeRules(result, IMAGE_EXTRACTION_RULES, "ImageExtractor");
|
|
21
|
+
};
|
|
@@ -3,20 +3,21 @@
|
|
|
3
3
|
* Exports all URL extraction functions
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
} from "./
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "./
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
|
|
22
|
-
} from "./
|
|
6
|
+
export { extractOutputUrl } from "./base-extractor";
|
|
7
|
+
|
|
8
|
+
export { extractVideoUrl, extractAudioUrl, extractImageUrls } from "./media-extractors";
|
|
9
|
+
|
|
10
|
+
export { extractOutputUrls } from "./multi-extractor";
|
|
11
|
+
|
|
12
|
+
export { extractThumbnailUrl } from "./thumbnail-extractor";
|
|
13
|
+
|
|
14
|
+
export type { ExtractionRule } from "./extraction-rules";
|
|
15
|
+
export { IMAGE_EXTRACTION_RULES, VIDEO_EXTRACTION_RULES } from "./extraction-rules";
|
|
16
|
+
|
|
17
|
+
export { executeRules } from "./rule-executor";
|
|
18
|
+
|
|
19
|
+
export type { ImageResultExtractor } from "./image-result-extractor";
|
|
20
|
+
export { extractImageResult } from "./image-result-extractor";
|
|
21
|
+
|
|
22
|
+
export type { VideoResultExtractor } from "./video-result-extractor";
|
|
23
|
+
export { extractVideoResult } from "./video-result-extractor";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Executor
|
|
3
|
+
* Chain of Responsibility pattern for URL extraction
|
|
4
|
+
* Tries each rule in order, returns first successful extraction
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtractionRule } from "./extraction-rules";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get value from object by path array
|
|
13
|
+
* Supports array indices as string numbers: ['images', '0', 'url']
|
|
14
|
+
*/
|
|
15
|
+
function getValueByPath(obj: unknown, path: readonly string[]): unknown {
|
|
16
|
+
let current: unknown = obj;
|
|
17
|
+
|
|
18
|
+
for (const key of path) {
|
|
19
|
+
if (current === null || current === undefined) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof current !== "object") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const record = current as Record<string, unknown>;
|
|
28
|
+
current = record[key];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute extraction rules in order
|
|
36
|
+
* Returns first successful string extraction or undefined
|
|
37
|
+
*/
|
|
38
|
+
export function executeRules(
|
|
39
|
+
result: unknown,
|
|
40
|
+
rules: readonly ExtractionRule[],
|
|
41
|
+
debugPrefix?: string,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
if (typeof result !== "object" || result === null) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (__DEV__ && debugPrefix) {
|
|
48
|
+
console.log(`[${debugPrefix}] Result keys:`, Object.keys(result as object));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const rule of rules) {
|
|
52
|
+
const value = getValueByPath(result, rule.path);
|
|
53
|
+
|
|
54
|
+
if (typeof value === "string" && value.length > 0) {
|
|
55
|
+
if (__DEV__ && debugPrefix) {
|
|
56
|
+
console.log(`[${debugPrefix}] Found via ${rule.description}:`, value);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (__DEV__ && debugPrefix) {
|
|
63
|
+
console.log(`[${debugPrefix}] No URL found in result`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Result Extractor
|
|
3
|
+
* Extracts video URL from AI provider responses using declarative rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { VIDEO_EXTRACTION_RULES } from "./extraction-rules";
|
|
7
|
+
import { executeRules } from "./rule-executor";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Result extractor function type
|
|
11
|
+
*/
|
|
12
|
+
export type VideoResultExtractor = (result: unknown) => string | undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract video URL from AI generation result
|
|
16
|
+
* Uses Chain of Responsibility pattern with declarative rules
|
|
17
|
+
* Supports: FAL.ai wrapper, direct formats, nested objects, arrays
|
|
18
|
+
*/
|
|
19
|
+
export const extractVideoResult: VideoResultExtractor = (result) => {
|
|
20
|
+
return executeRules(result, VIDEO_EXTRACTION_RULES, "VideoExtractor");
|
|
21
|
+
};
|
|
@@ -11,4 +11,15 @@ export {
|
|
|
11
11
|
extractImageUrls,
|
|
12
12
|
extractOutputUrls,
|
|
13
13
|
extractThumbnailUrl,
|
|
14
|
+
extractImageResult,
|
|
15
|
+
extractVideoResult,
|
|
16
|
+
executeRules,
|
|
17
|
+
IMAGE_EXTRACTION_RULES,
|
|
18
|
+
VIDEO_EXTRACTION_RULES,
|
|
19
|
+
} from "./url-extractor";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
ExtractionRule,
|
|
23
|
+
ImageResultExtractor,
|
|
24
|
+
VideoResultExtractor,
|
|
14
25
|
} from "./url-extractor";
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Video Result Extractor Utility
|
|
3
|
-
* Extracts video URL from various provider response formats
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { VideoResultExtractor } from "../services/video-feature-executor.types";
|
|
7
|
-
|
|
8
|
-
declare const __DEV__: boolean;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Default result extractor - handles common response formats
|
|
12
|
-
* Supports FAL data wrapper and nested object formats
|
|
13
|
-
*/
|
|
14
|
-
export const defaultExtractVideoResult: VideoResultExtractor = (result) => {
|
|
15
|
-
if (typeof result !== "object" || result === null) return undefined;
|
|
16
|
-
|
|
17
|
-
const r = result as Record<string, unknown>;
|
|
18
|
-
|
|
19
|
-
if (__DEV__) {
|
|
20
|
-
console.log("[VideoExtractor] Result keys:", Object.keys(r));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Handle fal.ai data wrapper
|
|
24
|
-
const data = (r.data as Record<string, unknown>) ?? r;
|
|
25
|
-
|
|
26
|
-
if (__DEV__) {
|
|
27
|
-
console.log("[VideoExtractor] Data keys:", Object.keys(data));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Direct string values
|
|
31
|
-
if (typeof data.video === "string") return data.video;
|
|
32
|
-
if (typeof data.videoUrl === "string") return data.videoUrl;
|
|
33
|
-
if (typeof data.video_url === "string") return data.video_url;
|
|
34
|
-
if (typeof data.output === "string") return data.output;
|
|
35
|
-
if (typeof data.url === "string") return data.url;
|
|
36
|
-
|
|
37
|
-
// Object with url property (e.g., { video: { url: "..." } })
|
|
38
|
-
const videoObj = data.video as Record<string, unknown> | undefined;
|
|
39
|
-
if (videoObj && typeof videoObj === "object" && typeof videoObj.url === "string") {
|
|
40
|
-
if (__DEV__) {
|
|
41
|
-
console.log("[VideoExtractor] Found data.video.url:", videoObj.url);
|
|
42
|
-
}
|
|
43
|
-
return videoObj.url;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Array format (e.g., { videos: [{ url: "..." }] })
|
|
47
|
-
if (Array.isArray(data.videos) && typeof data.videos[0]?.url === "string") {
|
|
48
|
-
if (__DEV__) {
|
|
49
|
-
console.log("[VideoExtractor] Found videos[0].url:", data.videos[0].url);
|
|
50
|
-
}
|
|
51
|
-
return data.videos[0].url;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (__DEV__) {
|
|
55
|
-
console.log("[VideoExtractor] No video URL found in result");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return undefined;
|
|
59
|
-
};
|