@umituz/react-native-ai-generation-content 1.26.26 → 1.26.29
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/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/constants/index.ts +11 -0
- package/src/infrastructure/constants/progress.constants.ts +36 -0
- package/src/infrastructure/services/image-feature-executor.service.ts +11 -83
- package/src/infrastructure/services/index.ts +1 -1
- package/src/infrastructure/services/provider-validator.ts +17 -38
- package/src/infrastructure/services/video-feature-executor.service.ts +13 -38
- package/src/infrastructure/services/video-feature-executor.types.ts +1 -4
- package/src/infrastructure/utils/error-message-extractor.util.ts +41 -0
- package/src/infrastructure/utils/index.ts +1 -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.29",
|
|
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
|
|
|
@@ -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 }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Constants
|
|
3
|
+
* Standardized progress values for generation pipelines
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Image generation progress stages */
|
|
7
|
+
export const IMAGE_PROGRESS = {
|
|
8
|
+
START: 10,
|
|
9
|
+
INPUT_PREPARED: 30,
|
|
10
|
+
REQUEST_SENT: 40,
|
|
11
|
+
RESULT_RECEIVED: 90,
|
|
12
|
+
COMPLETE: 100,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/** Video generation progress stages */
|
|
16
|
+
export const VIDEO_PROGRESS = {
|
|
17
|
+
START: 5,
|
|
18
|
+
INPUT_PREPARED: 10,
|
|
19
|
+
REQUEST_SENT: 15,
|
|
20
|
+
IN_QUEUE: 20,
|
|
21
|
+
IN_PROGRESS: 50,
|
|
22
|
+
RESULT_RECEIVED: 90,
|
|
23
|
+
COMPLETE: 100,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
/** Polling progress stages */
|
|
27
|
+
export const POLLING_PROGRESS = {
|
|
28
|
+
RESULT_RECEIVED: 90,
|
|
29
|
+
COMPLETE: 100,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/** Video generation timeout in milliseconds (5 minutes) */
|
|
33
|
+
export const VIDEO_TIMEOUT_MS = 300000;
|
|
34
|
+
|
|
35
|
+
/** Maximum consecutive transient errors before failing */
|
|
36
|
+
export const MAX_TRANSIENT_ERRORS = 5;
|
|
@@ -5,16 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { providerRegistry } from "./provider-registry.service";
|
|
8
|
-
import { cleanBase64 } from "../utils";
|
|
8
|
+
import { cleanBase64, extractErrorMessage } from "../utils";
|
|
9
|
+
import { extractImageResult } from "../utils/url-extractor";
|
|
10
|
+
import { IMAGE_PROGRESS } from "../constants";
|
|
11
|
+
import type { ImageResultExtractor } from "../utils/url-extractor";
|
|
9
12
|
import type { ImageFeatureType, ImageFeatureInputData } from "../../domain/interfaces";
|
|
10
13
|
|
|
11
14
|
declare const __DEV__: boolean;
|
|
12
15
|
|
|
13
|
-
/**
|
|
14
|
-
* Result extractor function type
|
|
15
|
-
*/
|
|
16
|
-
export type ImageResultExtractor = (result: unknown) => string | undefined;
|
|
17
|
-
|
|
18
16
|
/**
|
|
19
17
|
* Execution options
|
|
20
18
|
*/
|
|
@@ -43,57 +41,6 @@ export interface ImageFeatureRequest {
|
|
|
43
41
|
options?: Record<string, unknown>;
|
|
44
42
|
}
|
|
45
43
|
|
|
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
44
|
/**
|
|
98
45
|
* Execute any image feature using the active provider
|
|
99
46
|
*/
|
|
@@ -122,7 +69,7 @@ export async function executeImageFeature(
|
|
|
122
69
|
}
|
|
123
70
|
|
|
124
71
|
try {
|
|
125
|
-
onProgress?.(
|
|
72
|
+
onProgress?.(IMAGE_PROGRESS.START);
|
|
126
73
|
|
|
127
74
|
const inputData: ImageFeatureInputData = {
|
|
128
75
|
imageBase64: request.imageBase64 ? cleanBase64(request.imageBase64) : "",
|
|
@@ -133,20 +80,20 @@ export async function executeImageFeature(
|
|
|
133
80
|
options: request.options,
|
|
134
81
|
};
|
|
135
82
|
|
|
136
|
-
onProgress?.(
|
|
83
|
+
onProgress?.(IMAGE_PROGRESS.INPUT_PREPARED);
|
|
137
84
|
|
|
138
85
|
const input = provider.buildImageFeatureInput(featureType, inputData);
|
|
139
86
|
|
|
140
|
-
onProgress?.(
|
|
87
|
+
onProgress?.(IMAGE_PROGRESS.REQUEST_SENT);
|
|
141
88
|
|
|
142
89
|
const result = await provider.run(model, input);
|
|
143
90
|
|
|
144
|
-
onProgress?.(
|
|
91
|
+
onProgress?.(IMAGE_PROGRESS.RESULT_RECEIVED);
|
|
145
92
|
|
|
146
|
-
const extractor = extractResult ??
|
|
93
|
+
const extractor = extractResult ?? extractImageResult;
|
|
147
94
|
const imageUrl = extractor(result);
|
|
148
95
|
|
|
149
|
-
onProgress?.(
|
|
96
|
+
onProgress?.(IMAGE_PROGRESS.COMPLETE);
|
|
150
97
|
|
|
151
98
|
if (!imageUrl) {
|
|
152
99
|
return { success: false, error: "No image in response" };
|
|
@@ -158,26 +105,7 @@ export async function executeImageFeature(
|
|
|
158
105
|
requestId: (result as { requestId?: string })?.requestId,
|
|
159
106
|
};
|
|
160
107
|
} catch (error) {
|
|
161
|
-
|
|
162
|
-
let message = "Processing failed";
|
|
163
|
-
if (error instanceof Error) {
|
|
164
|
-
message = error.message;
|
|
165
|
-
} else if (typeof error === "object" && error !== null) {
|
|
166
|
-
const errObj = error as Record<string, unknown>;
|
|
167
|
-
// FAL API error format: {detail: [{msg, type, loc}]} or {message}
|
|
168
|
-
if (Array.isArray(errObj.detail) && errObj.detail[0]?.msg) {
|
|
169
|
-
message = String(errObj.detail[0].msg);
|
|
170
|
-
} else if (errObj.detail) {
|
|
171
|
-
message = JSON.stringify(errObj.detail);
|
|
172
|
-
} else if (errObj.message) {
|
|
173
|
-
message = String(errObj.message);
|
|
174
|
-
} else if (errObj.msg) {
|
|
175
|
-
message = String(errObj.msg);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (__DEV__) {
|
|
179
|
-
console.error(`[Image:${featureType}] Error:`, message, error);
|
|
180
|
-
}
|
|
108
|
+
const message = extractErrorMessage(error, "Processing failed", `Image:${featureType}`);
|
|
181
109
|
return { success: false, error: message };
|
|
182
110
|
}
|
|
183
111
|
}
|
|
@@ -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,
|
|
@@ -9,44 +9,23 @@ import { providerRegistry } from "./provider-registry.service";
|
|
|
9
9
|
declare const __DEV__: boolean;
|
|
10
10
|
|
|
11
11
|
export class ProviderValidator {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
22
|
-
|
|
23
|
-
console.error("[ProviderValidator] No active provider found!");
|
|
24
|
-
}
|
|
25
|
-
throw new Error(
|
|
26
|
-
"No active AI provider. Register and set a provider first.",
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!provider.isInitialized()) {
|
|
31
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
32
|
-
|
|
33
|
-
console.error("[ProviderValidator] Provider not initialized:", provider.providerId);
|
|
34
|
-
}
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Provider ${provider.providerId} is not initialized.`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
41
|
-
|
|
42
|
-
console.log("[ProviderValidator] getProvider() returning:", {
|
|
43
|
-
providerId: provider.providerId,
|
|
44
|
-
isInitialized: provider.isInitialized(),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
12
|
+
getProvider(): IAIProvider {
|
|
13
|
+
const provider = providerRegistry.getActiveProvider();
|
|
14
|
+
|
|
15
|
+
if (!provider) {
|
|
16
|
+
if (__DEV__) {
|
|
17
|
+
console.error("[ProviderValidator] No active provider found");
|
|
18
|
+
}
|
|
19
|
+
throw new Error("No active AI provider. Register and set a provider first.");
|
|
20
|
+
}
|
|
47
21
|
|
|
48
|
-
|
|
22
|
+
if (!provider.isInitialized()) {
|
|
23
|
+
if (__DEV__) {
|
|
24
|
+
console.error("[ProviderValidator] Provider not initialized:", provider.providerId);
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Provider ${provider.providerId} is not initialized.`);
|
|
49
27
|
}
|
|
50
|
-
}
|
|
51
28
|
|
|
52
|
-
|
|
29
|
+
return provider;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { providerRegistry } from "./provider-registry.service";
|
|
8
|
-
import { cleanBase64 } from "../utils";
|
|
9
|
-
import {
|
|
8
|
+
import { cleanBase64, extractErrorMessage } from "../utils";
|
|
9
|
+
import { extractVideoResult } from "../utils/url-extractor";
|
|
10
|
+
import { VIDEO_PROGRESS, VIDEO_TIMEOUT_MS } from "../constants";
|
|
10
11
|
import type { VideoFeatureType, VideoFeatureInputData } from "../../domain/interfaces";
|
|
11
12
|
import type {
|
|
12
13
|
ExecuteVideoFeatureOptions,
|
|
@@ -44,7 +45,7 @@ export async function executeVideoFeature(
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
try {
|
|
47
|
-
onProgress?.(
|
|
48
|
+
onProgress?.(VIDEO_PROGRESS.START);
|
|
48
49
|
|
|
49
50
|
const inputData: VideoFeatureInputData = {
|
|
50
51
|
sourceImageBase64: cleanBase64(request.sourceImageBase64),
|
|
@@ -53,34 +54,32 @@ export async function executeVideoFeature(
|
|
|
53
54
|
options: request.options,
|
|
54
55
|
};
|
|
55
56
|
|
|
56
|
-
onProgress?.(
|
|
57
|
+
onProgress?.(VIDEO_PROGRESS.INPUT_PREPARED);
|
|
57
58
|
|
|
58
59
|
const input = provider.buildVideoFeatureInput(featureType, inputData);
|
|
59
60
|
|
|
60
|
-
onProgress?.(
|
|
61
|
+
onProgress?.(VIDEO_PROGRESS.REQUEST_SENT);
|
|
61
62
|
|
|
62
|
-
// Use subscribe for video features - provides queue updates and handles long-running tasks
|
|
63
63
|
const result = await provider.subscribe(model, input, {
|
|
64
|
-
timeoutMs:
|
|
64
|
+
timeoutMs: VIDEO_TIMEOUT_MS,
|
|
65
65
|
onQueueUpdate: (status) => {
|
|
66
66
|
if (__DEV__) {
|
|
67
67
|
console.log(`[Video:${featureType}] Queue update:`, status.status);
|
|
68
68
|
}
|
|
69
|
-
// Map queue status to progress percentage
|
|
70
69
|
if (status.status === "IN_QUEUE") {
|
|
71
|
-
onProgress?.(
|
|
70
|
+
onProgress?.(VIDEO_PROGRESS.IN_QUEUE);
|
|
72
71
|
} else if (status.status === "IN_PROGRESS") {
|
|
73
|
-
onProgress?.(
|
|
72
|
+
onProgress?.(VIDEO_PROGRESS.IN_PROGRESS);
|
|
74
73
|
}
|
|
75
74
|
},
|
|
76
75
|
});
|
|
77
76
|
|
|
78
|
-
onProgress?.(
|
|
77
|
+
onProgress?.(VIDEO_PROGRESS.RESULT_RECEIVED);
|
|
79
78
|
|
|
80
|
-
const extractor = extractResult ??
|
|
79
|
+
const extractor = extractResult ?? extractVideoResult;
|
|
81
80
|
const videoUrl = extractor(result);
|
|
82
81
|
|
|
83
|
-
onProgress?.(
|
|
82
|
+
onProgress?.(VIDEO_PROGRESS.COMPLETE);
|
|
84
83
|
|
|
85
84
|
if (!videoUrl) {
|
|
86
85
|
if (__DEV__) {
|
|
@@ -95,35 +94,11 @@ export async function executeVideoFeature(
|
|
|
95
94
|
requestId: (result as { requestId?: string })?.requestId,
|
|
96
95
|
};
|
|
97
96
|
} catch (error) {
|
|
98
|
-
const message = extractErrorMessage(error, featureType);
|
|
97
|
+
const message = extractErrorMessage(error, "Processing failed", `Video:${featureType}`);
|
|
99
98
|
return { success: false, error: message };
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
/**
|
|
104
|
-
* Extract error message from various error formats
|
|
105
|
-
*/
|
|
106
|
-
function extractErrorMessage(error: unknown, featureType: VideoFeatureType): string {
|
|
107
|
-
let message = "Processing failed";
|
|
108
|
-
|
|
109
|
-
if (error instanceof Error) {
|
|
110
|
-
message = error.message;
|
|
111
|
-
} else if (typeof error === "object" && error !== null) {
|
|
112
|
-
const errObj = error as Record<string, unknown>;
|
|
113
|
-
if (errObj.detail) {
|
|
114
|
-
message = JSON.stringify(errObj.detail);
|
|
115
|
-
} else if (errObj.message) {
|
|
116
|
-
message = String(errObj.message);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (__DEV__) {
|
|
121
|
-
console.error(`[Video:${featureType}] Error:`, message, error);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return message;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
102
|
/**
|
|
128
103
|
* Check if video features are supported
|
|
129
104
|
*/
|
|
@@ -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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Message Extractor
|
|
3
|
+
* Extracts error messages from various API error formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const __DEV__: boolean;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract error message from FAL API and other error formats
|
|
10
|
+
* Supports: Error instances, FAL API errors, generic objects
|
|
11
|
+
*/
|
|
12
|
+
export function extractErrorMessage(
|
|
13
|
+
error: unknown,
|
|
14
|
+
defaultMessage = "Processing failed",
|
|
15
|
+
debugPrefix?: string,
|
|
16
|
+
): string {
|
|
17
|
+
let message = defaultMessage;
|
|
18
|
+
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
message = error.message;
|
|
21
|
+
} else if (typeof error === "object" && error !== null) {
|
|
22
|
+
const errObj = error as Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
// FAL API error format: {detail: [{msg, type, loc}]}
|
|
25
|
+
if (Array.isArray(errObj.detail) && errObj.detail[0]?.msg) {
|
|
26
|
+
message = String(errObj.detail[0].msg);
|
|
27
|
+
} else if (errObj.detail) {
|
|
28
|
+
message = JSON.stringify(errObj.detail);
|
|
29
|
+
} else if (errObj.message) {
|
|
30
|
+
message = String(errObj.message);
|
|
31
|
+
} else if (errObj.msg) {
|
|
32
|
+
message = String(errObj.msg);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (__DEV__ && debugPrefix) {
|
|
37
|
+
console.error(`[${debugPrefix}] Error:`, message, error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return message;
|
|
41
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export * from "./error-classifier.util";
|
|
6
|
+
export * from "./error-message-extractor.util";
|
|
6
7
|
export * from "./polling-interval.util";
|
|
7
8
|
export * from "./progress-calculator.util";
|
|
8
9
|
export * from "./status-checker.util";
|
|
@@ -11,5 +12,4 @@ export * from "./url-extractor.util";
|
|
|
11
12
|
export * from "./photo-generation";
|
|
12
13
|
export * from "./feature-utils";
|
|
13
14
|
export * from "./video-helpers";
|
|
14
|
-
export * from "./video-result-extractor.util";
|
|
15
15
|
export * from "./media-actions.util";
|
|
@@ -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
|
-
};
|