@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 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.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 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 }}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Infrastructure Constants
3
+ */
4
+
5
+ export {
6
+ IMAGE_PROGRESS,
7
+ VIDEO_PROGRESS,
8
+ POLLING_PROGRESS,
9
+ VIDEO_TIMEOUT_MS,
10
+ MAX_TRANSIENT_ERRORS,
11
+ } from "./progress.constants";
@@ -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?.(10);
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?.(30);
83
+ onProgress?.(IMAGE_PROGRESS.INPUT_PREPARED);
137
84
 
138
85
  const input = provider.buildImageFeatureInput(featureType, inputData);
139
86
 
140
- onProgress?.(40);
87
+ onProgress?.(IMAGE_PROGRESS.REQUEST_SENT);
141
88
 
142
89
  const result = await provider.run(model, input);
143
90
 
144
- onProgress?.(90);
91
+ onProgress?.(IMAGE_PROGRESS.RESULT_RECEIVED);
145
92
 
146
- const extractor = extractResult ?? defaultExtractImageResult;
93
+ const extractor = extractResult ?? extractImageResult;
147
94
  const imageUrl = extractor(result);
148
95
 
149
- onProgress?.(100);
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
- // Extract detailed error message from FAL API errors
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
- getProvider(): IAIProvider {
13
- if (typeof __DEV__ !== "undefined" && __DEV__) {
14
-
15
- console.log("[ProviderValidator] getProvider() called");
16
- }
17
-
18
- const provider = providerRegistry.getActiveProvider();
19
-
20
- if (!provider) {
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
- return provider;
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
- export const providerValidator = new ProviderValidator();
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 { defaultExtractVideoResult } from "../utils/video-result-extractor.util";
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?.(5);
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?.(10);
57
+ onProgress?.(VIDEO_PROGRESS.INPUT_PREPARED);
57
58
 
58
59
  const input = provider.buildVideoFeatureInput(featureType, inputData);
59
60
 
60
- onProgress?.(15);
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: 300000, // 5 minutes timeout for video generation
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?.(20);
70
+ onProgress?.(VIDEO_PROGRESS.IN_QUEUE);
72
71
  } else if (status.status === "IN_PROGRESS") {
73
- onProgress?.(50);
72
+ onProgress?.(VIDEO_PROGRESS.IN_PROGRESS);
74
73
  }
75
74
  },
76
75
  });
77
76
 
78
- onProgress?.(90);
77
+ onProgress?.(VIDEO_PROGRESS.RESULT_RECEIVED);
79
78
 
80
- const extractor = extractResult ?? defaultExtractVideoResult;
79
+ const extractor = extractResult ?? extractVideoResult;
81
80
  const videoUrl = extractor(result);
82
81
 
83
- onProgress?.(100);
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
- 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
- };