@umituz/react-native-ai-generation-content 1.26.26 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.26.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 partnerABase64: string;
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
- hasPartnerA: !!input.partnerABase64,
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 = [input.partnerABase64, input.partnerBBase64]
68
- .filter(Boolean)
69
- .map(formatBase64);
65
+ const imageUrls = input.photos.map(formatBase64);
70
66
 
71
- if (imageUrls.length < 2) {
72
- return { success: false, error: "Two images required" };
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
- // Input Builder
124
+ // Photo Extraction (Generic - used by both image and video)
129
125
  // ============================================================================
130
126
 
131
- async function buildGenerationInput(
127
+ async function extractPhotosFromWizardData(
132
128
  wizardData: Record<string, unknown>,
133
- scenario: WizardScenarioData,
134
- ): Promise<WizardGenerationInput | null> {
135
- const photo1Key = Object.keys(wizardData).find((k) => k.includes("photo_1"));
136
- const photo2Key = Object.keys(wizardData).find((k) => k.includes("photo_2"));
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 (!photo1Key || !photo2Key) {
135
+ if (photoKeys.length === 0) {
139
136
  if (typeof __DEV__ !== "undefined" && __DEV__) {
140
- console.error("[WizardStrategy] Missing photos in wizard data", {
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
- const photo1 = wizardData[photo1Key] as { uri?: string; base64?: string };
150
- const photo2 = wizardData[photo2Key] as { uri?: string; base64?: string };
151
-
152
- if (!photo1?.uri || !photo2?.uri) {
153
- if (typeof __DEV__ !== "undefined" && __DEV__) {
154
- console.error("[WizardStrategy] Photos missing URI", {
155
- photo1HasUri: !!photo1?.uri,
156
- photo2HasUri: !!photo2?.uri,
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
- return null;
154
+ photoUris.push(photo.uri);
160
155
  }
161
156
 
162
- // Convert images to base64
163
- const [photo1Base64, photo2Base64] = await Promise.all([
164
- readFileAsBase64(photo1.uri),
165
- readFileAsBase64(photo2.uri),
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
- if (!photo1Base64 || !photo2Base64) {
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
- // NO FALLBACK - scenario.aiPrompt is REQUIRED
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] CRITICAL: scenario.aiPrompt is missing or empty!", {
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
- // For image generation, enhance prompt with style selections
190
- if (outputType === "image") {
191
- const styleEnhancements: string[] = [];
206
+ // Enhance prompt with style selections (image-specific)
207
+ const styleEnhancements: string[] = [];
192
208
 
193
- // Romantic mood (multi-select array)
194
- const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
195
- if (romanticMoods && romanticMoods.length > 0) {
196
- styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
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
- // Art style (single select)
200
- const artStyle = wizardData.selection_art_style as string | undefined;
201
- if (artStyle && artStyle !== "original") {
202
- styleEnhancements.push(`Art style: ${artStyle}`);
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
- // Artist style (single select)
206
- const artist = wizardData.selection_artist_style as string | undefined;
207
- if (artist && artist !== "original") {
208
- styleEnhancements.push(`Artist style: ${artist}`);
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
- // Enhance prompt with selected styles
212
- if (styleEnhancements.length > 0) {
213
- prompt = `${prompt}. ${styleEnhancements.join(", ")}`;
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
- return {
217
- partnerABase64: photo1Base64,
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]: any;
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
- // Fallback to emoji/icon if no image (though we expect images now)
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={!item.emoji ? (item.icon as any) : undefined}
89
+ leftIcon={item.icon}
90
90
  badge={item.emoji}
91
91
  title={title}
92
92
  subtitle={description}
93
- // Smaller aspect ratio for subcategories if they have images, or standard if not
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 ?? defaultExtractImageResult;
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 { defaultExtractVideoResult } from "../utils/video-result-extractor.util";
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 ?? defaultExtractVideoResult;
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
@@ -11,5 +11,4 @@ export * from "./url-extractor.util";
11
11
  export * from "./photo-generation";
12
12
  export * from "./feature-utils";
13
13
  export * from "./video-helpers";
14
- export * from "./video-result-extractor.util";
15
14
  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
- extractOutputUrl,
8
- } from "./base-extractor";
9
-
10
- export {
11
- extractVideoUrl,
12
- extractAudioUrl,
13
- extractImageUrls,
14
- } from "./media-extractors";
15
-
16
- export {
17
- extractOutputUrls,
18
- } from "./multi-extractor";
19
-
20
- export {
21
- extractThumbnailUrl,
22
- } from "./thumbnail-extractor";
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
- };