@umituz/react-native-ai-generation-content 1.17.143 → 1.17.145
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/creations/presentation/hooks/index.ts +2 -2
- package/src/domains/creations/presentation/hooks/{useMediaFilter.ts → useFilter.ts} +12 -7
- package/src/domains/creations/presentation/hooks/useGalleryFilters.ts +5 -6
- package/src/features/ai-hug/presentation/hooks/useAIHugFeature.ts +9 -113
- package/src/features/ai-kiss/presentation/hooks/useAIKissFeature.ts +9 -113
- package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +5 -9
- package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +5 -0
- package/src/features/replace-background/index.ts +5 -50
- package/src/features/replace-background/presentation/components/index.ts +0 -12
- package/src/features/replace-background/presentation/hooks/index.ts +0 -3
- package/src/features/shared/dual-image-video/domain/types/dual-image-video.types.ts +67 -0
- package/src/features/shared/dual-image-video/domain/types/index.ts +13 -0
- package/src/features/shared/dual-image-video/index.ts +16 -0
- package/src/features/shared/dual-image-video/presentation/hooks/index.ts +5 -0
- package/src/features/shared/dual-image-video/presentation/hooks/useDualImageVideoFeature.ts +124 -0
- package/src/features/shared/index.ts +6 -0
- package/src/infrastructure/utils/index.ts +1 -0
- package/src/infrastructure/utils/progress-calculator.util.ts +31 -0
- package/src/infrastructure/utils/result-validator.util.ts +0 -214
- package/src/infrastructure/utils/url-extractor.util.ts +209 -0
- package/src/presentation/hooks/flow-state.utils.ts +101 -0
- package/src/presentation/hooks/useGenerationFlow.ts +47 -178
- package/src/presentation/types/flow-config.types.ts +5 -99
- package/src/presentation/types/flow-default-configs.ts +106 -0
- package/src/domains/creations/presentation/hooks/useStatusFilter.ts +0 -54
- package/src/features/replace-background/domain/entities/background.types.ts +0 -77
- package/src/features/replace-background/domain/entities/component.types.ts +0 -87
- package/src/features/replace-background/domain/entities/config.types.ts +0 -41
- package/src/features/replace-background/domain/entities/index.ts +0 -30
- package/src/features/replace-background/infrastructure/constants/index.ts +0 -5
- package/src/features/replace-background/infrastructure/constants/prompts.constants.ts +0 -15
- package/src/features/replace-background/infrastructure/index.ts +0 -5
- package/src/features/replace-background/presentation/components/BackgroundFeature.tsx +0 -143
- package/src/features/replace-background/presentation/components/ComparisonSlider.tsx +0 -187
- package/src/features/replace-background/presentation/components/ErrorDisplay.tsx +0 -60
- package/src/features/replace-background/presentation/components/FeatureHeader.tsx +0 -80
- package/src/features/replace-background/presentation/components/GenerateButton.tsx +0 -85
- package/src/features/replace-background/presentation/components/ImagePicker.tsx +0 -136
- package/src/features/replace-background/presentation/components/ModeSelector.tsx +0 -78
- package/src/features/replace-background/presentation/components/PromptInput.tsx +0 -142
- package/src/features/replace-background/presentation/components/ResultDisplay.tsx +0 -122
- package/src/features/replace-background/presentation/hooks/useBackgroundFeature.ts +0 -119
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDualImageVideoFeature Hook
|
|
3
|
+
* Base hook for video features that take two images (ai-hug, ai-kiss, etc.)
|
|
4
|
+
* DRY: Consolidates common logic from useAIHugFeature and useAIKissFeature
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from "react";
|
|
8
|
+
import { executeVideoFeature } from "../../../../../infrastructure/services";
|
|
9
|
+
import type {
|
|
10
|
+
DualImageVideoFeatureState,
|
|
11
|
+
UseDualImageVideoFeatureProps,
|
|
12
|
+
UseDualImageVideoFeatureReturn,
|
|
13
|
+
} from "../../domain/types/dual-image-video.types";
|
|
14
|
+
|
|
15
|
+
const initialState: DualImageVideoFeatureState = {
|
|
16
|
+
sourceImageUri: null,
|
|
17
|
+
targetImageUri: null,
|
|
18
|
+
processedVideoUrl: null,
|
|
19
|
+
isProcessing: false,
|
|
20
|
+
progress: 0,
|
|
21
|
+
error: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function useDualImageVideoFeature(
|
|
25
|
+
props: UseDualImageVideoFeatureProps,
|
|
26
|
+
): UseDualImageVideoFeatureReturn {
|
|
27
|
+
const { featureType, config, onSelectSourceImage, onSelectTargetImage, onSaveVideo } = props;
|
|
28
|
+
const [state, setState] = useState<DualImageVideoFeatureState>(initialState);
|
|
29
|
+
|
|
30
|
+
const selectSourceImage = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const uri = await onSelectSourceImage();
|
|
33
|
+
if (uri) {
|
|
34
|
+
setState((prev) => ({ ...prev, sourceImageUri: uri, error: null }));
|
|
35
|
+
config.onSourceImageSelect?.(uri);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
setState((prev) => ({ ...prev, error: message }));
|
|
40
|
+
}
|
|
41
|
+
}, [onSelectSourceImage, config]);
|
|
42
|
+
|
|
43
|
+
const selectTargetImage = useCallback(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const uri = await onSelectTargetImage();
|
|
46
|
+
if (uri) {
|
|
47
|
+
setState((prev) => ({ ...prev, targetImageUri: uri, error: null }));
|
|
48
|
+
config.onTargetImageSelect?.(uri);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
setState((prev) => ({ ...prev, error: message }));
|
|
53
|
+
}
|
|
54
|
+
}, [onSelectTargetImage, config]);
|
|
55
|
+
|
|
56
|
+
const handleProgress = useCallback((progress: number) => {
|
|
57
|
+
setState((prev) => ({ ...prev, progress }));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const process = useCallback(async () => {
|
|
61
|
+
if (!state.sourceImageUri || !state.targetImageUri) return;
|
|
62
|
+
|
|
63
|
+
setState((prev) => ({
|
|
64
|
+
...prev,
|
|
65
|
+
isProcessing: true,
|
|
66
|
+
progress: 0,
|
|
67
|
+
error: null,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
config.onProcessingStart?.();
|
|
71
|
+
|
|
72
|
+
const sourceImageBase64 = await config.prepareImage(state.sourceImageUri);
|
|
73
|
+
const targetImageBase64 = await config.prepareImage(state.targetImageUri);
|
|
74
|
+
|
|
75
|
+
const result = await executeVideoFeature(
|
|
76
|
+
featureType,
|
|
77
|
+
{ sourceImageBase64, targetImageBase64 },
|
|
78
|
+
{ extractResult: config.extractResult, onProgress: handleProgress },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (result.success && result.videoUrl) {
|
|
82
|
+
setState((prev) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
isProcessing: false,
|
|
85
|
+
processedVideoUrl: result.videoUrl!,
|
|
86
|
+
progress: 100,
|
|
87
|
+
}));
|
|
88
|
+
config.onProcessingComplete?.({ success: true, videoUrl: result.videoUrl });
|
|
89
|
+
} else {
|
|
90
|
+
const errorMessage = result.error || "Processing failed";
|
|
91
|
+
setState((prev) => ({
|
|
92
|
+
...prev,
|
|
93
|
+
isProcessing: false,
|
|
94
|
+
error: errorMessage,
|
|
95
|
+
progress: 0,
|
|
96
|
+
}));
|
|
97
|
+
config.onError?.(errorMessage);
|
|
98
|
+
}
|
|
99
|
+
}, [state.sourceImageUri, state.targetImageUri, featureType, config, handleProgress]);
|
|
100
|
+
|
|
101
|
+
const save = useCallback(async () => {
|
|
102
|
+
if (!state.processedVideoUrl) return;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await onSaveVideo(state.processedVideoUrl);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
setState((prev) => ({ ...prev, error: message }));
|
|
109
|
+
}
|
|
110
|
+
}, [state.processedVideoUrl, onSaveVideo]);
|
|
111
|
+
|
|
112
|
+
const reset = useCallback(() => {
|
|
113
|
+
setState(initialState);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...state,
|
|
118
|
+
selectSourceImage,
|
|
119
|
+
selectTargetImage,
|
|
120
|
+
process,
|
|
121
|
+
save,
|
|
122
|
+
reset,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -7,6 +7,7 @@ export * from "./polling-interval.util";
|
|
|
7
7
|
export * from "./progress-calculator.util";
|
|
8
8
|
export * from "./status-checker.util";
|
|
9
9
|
export * from "./result-validator.util";
|
|
10
|
+
export * from "./url-extractor.util";
|
|
10
11
|
export * from "./photo-generation";
|
|
11
12
|
export * from "./feature-utils";
|
|
12
13
|
export * from "./video-helpers";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress Calculator Utility
|
|
3
3
|
* Calculates progress based on generation stage
|
|
4
|
+
* Maps provider status to generation progress
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { GenerationStatus } from "../../domain/entities";
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
DEFAULT_PROGRESS_STAGES,
|
|
9
10
|
type ProgressStageConfig,
|
|
10
11
|
} from "../../domain/entities";
|
|
12
|
+
import type { AIJobStatusType } from "../../domain/interfaces/ai-provider.interface";
|
|
11
13
|
|
|
12
14
|
export interface ProgressOptions {
|
|
13
15
|
status: GenerationStatus;
|
|
@@ -95,3 +97,32 @@ export function createProgressTracker(stages?: ProgressStageConfig[]) {
|
|
|
95
97
|
},
|
|
96
98
|
};
|
|
97
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Maps provider job status to generation status
|
|
103
|
+
* Provider: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED
|
|
104
|
+
* Generation: idle, preparing, submitting, generating, polling, finalizing, completed, failed
|
|
105
|
+
*/
|
|
106
|
+
export function mapJobStatusToGenerationStatus(
|
|
107
|
+
jobStatus: AIJobStatusType
|
|
108
|
+
): GenerationStatus {
|
|
109
|
+
const statusMap: Record<AIJobStatusType, GenerationStatus> = {
|
|
110
|
+
IN_QUEUE: "submitting",
|
|
111
|
+
IN_PROGRESS: "generating",
|
|
112
|
+
COMPLETED: "completed",
|
|
113
|
+
FAILED: "failed",
|
|
114
|
+
};
|
|
115
|
+
return statusMap[jobStatus] ?? "generating";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Calculate progress from provider job status
|
|
120
|
+
* Uses default progress stages for consistent progress reporting
|
|
121
|
+
*/
|
|
122
|
+
export function getProgressFromJobStatus(
|
|
123
|
+
jobStatus: AIJobStatusType,
|
|
124
|
+
stages: ProgressStageConfig[] = DEFAULT_PROGRESS_STAGES
|
|
125
|
+
): number {
|
|
126
|
+
const generationStatus = mapJobStatusToGenerationStatus(jobStatus);
|
|
127
|
+
return getProgressForStatus({ status: generationStatus, stages });
|
|
128
|
+
}
|
|
@@ -13,9 +13,7 @@ export interface ResultValidation {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface ValidateResultOptions {
|
|
16
|
-
/** Custom output field names to check */
|
|
17
16
|
outputFields?: string[];
|
|
18
|
-
/** Whether empty results are allowed */
|
|
19
17
|
allowEmpty?: boolean;
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -44,7 +42,6 @@ export function validateResult(
|
|
|
44
42
|
const { outputFields = DEFAULT_OUTPUT_FIELDS, allowEmpty = false } =
|
|
45
43
|
options ?? {};
|
|
46
44
|
|
|
47
|
-
// Handle null/undefined
|
|
48
45
|
if (result === null || result === undefined) {
|
|
49
46
|
return {
|
|
50
47
|
isValid: allowEmpty,
|
|
@@ -54,7 +51,6 @@ export function validateResult(
|
|
|
54
51
|
};
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
// Handle non-object results
|
|
58
54
|
if (typeof result !== "object") {
|
|
59
55
|
return {
|
|
60
56
|
isValid: true,
|
|
@@ -65,7 +61,6 @@ export function validateResult(
|
|
|
65
61
|
|
|
66
62
|
const resultObj = result as Record<string, unknown>;
|
|
67
63
|
|
|
68
|
-
// Check for error fields
|
|
69
64
|
const errorValue = resultObj.error || resultObj.detail;
|
|
70
65
|
const errorString = errorValue ? String(errorValue).toLowerCase() : "";
|
|
71
66
|
|
|
@@ -74,15 +69,12 @@ export function validateResult(
|
|
|
74
69
|
errorString.includes("500") ||
|
|
75
70
|
errorString === "internal server error";
|
|
76
71
|
|
|
77
|
-
// Check for empty object
|
|
78
72
|
const isEmpty = Object.keys(resultObj).length === 0;
|
|
79
73
|
|
|
80
|
-
// Check for output in expected fields
|
|
81
74
|
const hasOutput = outputFields.some((field) => {
|
|
82
75
|
const value = resultObj[field];
|
|
83
76
|
if (!value) return false;
|
|
84
77
|
|
|
85
|
-
// Handle nested output structures
|
|
86
78
|
if (typeof value === "object" && value !== null) {
|
|
87
79
|
const nested = value as Record<string, unknown>;
|
|
88
80
|
return !!(
|
|
@@ -96,7 +88,6 @@ export function validateResult(
|
|
|
96
88
|
return true;
|
|
97
89
|
});
|
|
98
90
|
|
|
99
|
-
// Determine if result has error
|
|
100
91
|
const hasError =
|
|
101
92
|
hasInternalServerError || (isEmpty && !hasOutput && !allowEmpty);
|
|
102
93
|
|
|
@@ -113,213 +104,8 @@ export function validateResult(
|
|
|
113
104
|
isValid: validation.isValid,
|
|
114
105
|
hasOutput: validation.hasOutput,
|
|
115
106
|
hasError: validation.hasError,
|
|
116
|
-
checkedFields: outputFields.join(", "),
|
|
117
107
|
});
|
|
118
108
|
}
|
|
119
109
|
|
|
120
110
|
return validation;
|
|
121
111
|
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Extract output URL from result
|
|
125
|
-
* Supports various AI provider response formats
|
|
126
|
-
*/
|
|
127
|
-
export function extractOutputUrl(
|
|
128
|
-
result: unknown,
|
|
129
|
-
urlFields?: string[],
|
|
130
|
-
): string | undefined {
|
|
131
|
-
if (!result || typeof result !== "object") {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const fields = urlFields ?? [
|
|
136
|
-
"url",
|
|
137
|
-
"image_url",
|
|
138
|
-
"video_url",
|
|
139
|
-
"output_url",
|
|
140
|
-
"result_url",
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
const resultObj = result as Record<string, unknown>;
|
|
144
|
-
|
|
145
|
-
// Check top-level fields
|
|
146
|
-
for (const field of fields) {
|
|
147
|
-
const value = resultObj[field];
|
|
148
|
-
if (typeof value === "string" && value.length > 0) {
|
|
149
|
-
return value;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Check nested data/output objects
|
|
154
|
-
const nested =
|
|
155
|
-
(resultObj.data as Record<string, unknown>) ||
|
|
156
|
-
(resultObj.output as Record<string, unknown>) ||
|
|
157
|
-
(resultObj.result as Record<string, unknown>);
|
|
158
|
-
|
|
159
|
-
if (nested && typeof nested === "object") {
|
|
160
|
-
for (const field of fields) {
|
|
161
|
-
const value = nested[field];
|
|
162
|
-
if (typeof value === "string" && value.length > 0) {
|
|
163
|
-
return value;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Check for nested image/video objects
|
|
168
|
-
const media =
|
|
169
|
-
(nested.image as Record<string, unknown>) ||
|
|
170
|
-
(nested.video as Record<string, unknown>);
|
|
171
|
-
if (media && typeof media === "object" && typeof media.url === "string") {
|
|
172
|
-
return media.url;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Extract multiple output URLs from result
|
|
181
|
-
*/
|
|
182
|
-
export function extractOutputUrls(
|
|
183
|
-
result: unknown,
|
|
184
|
-
urlFields?: string[],
|
|
185
|
-
): string[] {
|
|
186
|
-
if (!result || typeof result !== "object") {
|
|
187
|
-
return [];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const urls: string[] = [];
|
|
191
|
-
const resultObj = result as Record<string, unknown>;
|
|
192
|
-
|
|
193
|
-
// Check for arrays
|
|
194
|
-
const arrayFields = ["images", "videos", "outputs", "results", "urls"];
|
|
195
|
-
for (const field of arrayFields) {
|
|
196
|
-
const arr = resultObj[field];
|
|
197
|
-
if (Array.isArray(arr)) {
|
|
198
|
-
for (const item of arr) {
|
|
199
|
-
const url = extractOutputUrl(item, urlFields);
|
|
200
|
-
if (url) {
|
|
201
|
-
urls.push(url);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Check nested data/output
|
|
208
|
-
const nested = resultObj.data || resultObj.output;
|
|
209
|
-
if (nested && typeof nested === "object") {
|
|
210
|
-
for (const field of arrayFields) {
|
|
211
|
-
const arr = (nested as Record<string, unknown>)[field];
|
|
212
|
-
if (Array.isArray(arr)) {
|
|
213
|
-
for (const item of arr) {
|
|
214
|
-
const url = extractOutputUrl(item, urlFields);
|
|
215
|
-
if (url) {
|
|
216
|
-
urls.push(url);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// If no array found, try single URL
|
|
224
|
-
if (urls.length === 0) {
|
|
225
|
-
const singleUrl = extractOutputUrl(result, urlFields);
|
|
226
|
-
if (singleUrl) {
|
|
227
|
-
urls.push(singleUrl);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return urls;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Extract video URL from AI generation result
|
|
236
|
-
*/
|
|
237
|
-
export function extractVideoUrl(result: unknown): string | undefined {
|
|
238
|
-
return extractOutputUrl(result, [
|
|
239
|
-
"video_url",
|
|
240
|
-
"videoUrl",
|
|
241
|
-
"video",
|
|
242
|
-
"url",
|
|
243
|
-
]);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Extract thumbnail URL from AI generation result
|
|
248
|
-
*/
|
|
249
|
-
export function extractThumbnailUrl(result: unknown): string | undefined {
|
|
250
|
-
if (!result || typeof result !== "object") {
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const resultObj = result as Record<string, unknown>;
|
|
255
|
-
|
|
256
|
-
// Check direct fields
|
|
257
|
-
const fields = ["thumbnail_url", "thumbnailUrl", "thumbnail", "poster"];
|
|
258
|
-
for (const field of fields) {
|
|
259
|
-
const value = resultObj[field];
|
|
260
|
-
if (typeof value === "string" && value.length > 0) {
|
|
261
|
-
return value;
|
|
262
|
-
}
|
|
263
|
-
if (value && typeof value === "object") {
|
|
264
|
-
const nested = value as Record<string, unknown>;
|
|
265
|
-
if (typeof nested.url === "string") {
|
|
266
|
-
return nested.url;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Extract audio URL from AI generation result
|
|
276
|
-
*/
|
|
277
|
-
export function extractAudioUrl(result: unknown): string | undefined {
|
|
278
|
-
return extractOutputUrl(result, [
|
|
279
|
-
"audio_url",
|
|
280
|
-
"audioUrl",
|
|
281
|
-
"audio",
|
|
282
|
-
"url",
|
|
283
|
-
]);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Extract image URLs from AI generation result
|
|
288
|
-
*/
|
|
289
|
-
export function extractImageUrls(result: unknown): string[] {
|
|
290
|
-
if (!result || typeof result !== "object") {
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const urls: string[] = [];
|
|
295
|
-
const resultObj = result as Record<string, unknown>;
|
|
296
|
-
|
|
297
|
-
// Check images array
|
|
298
|
-
if (Array.isArray(resultObj.images)) {
|
|
299
|
-
for (const img of resultObj.images) {
|
|
300
|
-
if (typeof img === "string" && img.length > 0) {
|
|
301
|
-
urls.push(img);
|
|
302
|
-
} else if (img && typeof img === "object") {
|
|
303
|
-
const imgObj = img as Record<string, unknown>;
|
|
304
|
-
if (typeof imgObj.url === "string") {
|
|
305
|
-
urls.push(imgObj.url);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Check single image
|
|
312
|
-
if (urls.length === 0) {
|
|
313
|
-
const singleUrl = extractOutputUrl(result, [
|
|
314
|
-
"image_url",
|
|
315
|
-
"imageUrl",
|
|
316
|
-
"image",
|
|
317
|
-
"url",
|
|
318
|
-
]);
|
|
319
|
-
if (singleUrl) {
|
|
320
|
-
urls.push(singleUrl);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return urls;
|
|
325
|
-
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Extractor Utility
|
|
3
|
+
* Extracts output URLs from AI generation results
|
|
4
|
+
* Supports various provider response formats
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract output URL from result
|
|
9
|
+
* Supports various AI provider response formats
|
|
10
|
+
*/
|
|
11
|
+
export function extractOutputUrl(
|
|
12
|
+
result: unknown,
|
|
13
|
+
urlFields?: string[],
|
|
14
|
+
): string | undefined {
|
|
15
|
+
if (!result || typeof result !== "object") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fields = urlFields ?? [
|
|
20
|
+
"url",
|
|
21
|
+
"image_url",
|
|
22
|
+
"video_url",
|
|
23
|
+
"output_url",
|
|
24
|
+
"result_url",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const resultObj = result as Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
// Check top-level fields
|
|
30
|
+
for (const field of fields) {
|
|
31
|
+
const value = resultObj[field];
|
|
32
|
+
if (typeof value === "string" && value.length > 0) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check nested data/output objects
|
|
38
|
+
const nested =
|
|
39
|
+
(resultObj.data as Record<string, unknown>) ||
|
|
40
|
+
(resultObj.output as Record<string, unknown>) ||
|
|
41
|
+
(resultObj.result as Record<string, unknown>);
|
|
42
|
+
|
|
43
|
+
if (nested && typeof nested === "object") {
|
|
44
|
+
for (const field of fields) {
|
|
45
|
+
const value = nested[field];
|
|
46
|
+
if (typeof value === "string" && value.length > 0) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for nested image/video objects
|
|
52
|
+
const media =
|
|
53
|
+
(nested.image as Record<string, unknown>) ||
|
|
54
|
+
(nested.video as Record<string, unknown>);
|
|
55
|
+
if (media && typeof media === "object" && typeof media.url === "string") {
|
|
56
|
+
return media.url;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract multiple output URLs from result
|
|
65
|
+
*/
|
|
66
|
+
export function extractOutputUrls(
|
|
67
|
+
result: unknown,
|
|
68
|
+
urlFields?: string[],
|
|
69
|
+
): string[] {
|
|
70
|
+
if (!result || typeof result !== "object") {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const urls: string[] = [];
|
|
75
|
+
const resultObj = result as Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
// Check for arrays
|
|
78
|
+
const arrayFields = ["images", "videos", "outputs", "results", "urls"];
|
|
79
|
+
for (const field of arrayFields) {
|
|
80
|
+
const arr = resultObj[field];
|
|
81
|
+
if (Array.isArray(arr)) {
|
|
82
|
+
for (const item of arr) {
|
|
83
|
+
const url = extractOutputUrl(item, urlFields);
|
|
84
|
+
if (url) {
|
|
85
|
+
urls.push(url);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check nested data/output
|
|
92
|
+
const nested = resultObj.data || resultObj.output;
|
|
93
|
+
if (nested && typeof nested === "object") {
|
|
94
|
+
for (const field of arrayFields) {
|
|
95
|
+
const arr = (nested as Record<string, unknown>)[field];
|
|
96
|
+
if (Array.isArray(arr)) {
|
|
97
|
+
for (const item of arr) {
|
|
98
|
+
const url = extractOutputUrl(item, urlFields);
|
|
99
|
+
if (url) {
|
|
100
|
+
urls.push(url);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If no array found, try single URL
|
|
108
|
+
if (urls.length === 0) {
|
|
109
|
+
const singleUrl = extractOutputUrl(result, urlFields);
|
|
110
|
+
if (singleUrl) {
|
|
111
|
+
urls.push(singleUrl);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return urls;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract video URL from AI generation result
|
|
120
|
+
*/
|
|
121
|
+
export function extractVideoUrl(result: unknown): string | undefined {
|
|
122
|
+
return extractOutputUrl(result, [
|
|
123
|
+
"video_url",
|
|
124
|
+
"videoUrl",
|
|
125
|
+
"video",
|
|
126
|
+
"url",
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract thumbnail URL from AI generation result
|
|
132
|
+
*/
|
|
133
|
+
export function extractThumbnailUrl(result: unknown): string | undefined {
|
|
134
|
+
if (!result || typeof result !== "object") {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const resultObj = result as Record<string, unknown>;
|
|
139
|
+
|
|
140
|
+
// Check direct fields
|
|
141
|
+
const fields = ["thumbnail_url", "thumbnailUrl", "thumbnail", "poster"];
|
|
142
|
+
for (const field of fields) {
|
|
143
|
+
const value = resultObj[field];
|
|
144
|
+
if (typeof value === "string" && value.length > 0) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
if (value && typeof value === "object") {
|
|
148
|
+
const nested = value as Record<string, unknown>;
|
|
149
|
+
if (typeof nested.url === "string") {
|
|
150
|
+
return nested.url;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extract audio URL from AI generation result
|
|
160
|
+
*/
|
|
161
|
+
export function extractAudioUrl(result: unknown): string | undefined {
|
|
162
|
+
return extractOutputUrl(result, [
|
|
163
|
+
"audio_url",
|
|
164
|
+
"audioUrl",
|
|
165
|
+
"audio",
|
|
166
|
+
"url",
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract image URLs from AI generation result
|
|
172
|
+
*/
|
|
173
|
+
export function extractImageUrls(result: unknown): string[] {
|
|
174
|
+
if (!result || typeof result !== "object") {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const urls: string[] = [];
|
|
179
|
+
const resultObj = result as Record<string, unknown>;
|
|
180
|
+
|
|
181
|
+
// Check images array
|
|
182
|
+
if (Array.isArray(resultObj.images)) {
|
|
183
|
+
for (const img of resultObj.images) {
|
|
184
|
+
if (typeof img === "string" && img.length > 0) {
|
|
185
|
+
urls.push(img);
|
|
186
|
+
} else if (img && typeof img === "object") {
|
|
187
|
+
const imgObj = img as Record<string, unknown>;
|
|
188
|
+
if (typeof imgObj.url === "string") {
|
|
189
|
+
urls.push(imgObj.url);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check single image
|
|
196
|
+
if (urls.length === 0) {
|
|
197
|
+
const singleUrl = extractOutputUrl(result, [
|
|
198
|
+
"image_url",
|
|
199
|
+
"imageUrl",
|
|
200
|
+
"image",
|
|
201
|
+
"url",
|
|
202
|
+
]);
|
|
203
|
+
if (singleUrl) {
|
|
204
|
+
urls.push(singleUrl);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return urls;
|
|
209
|
+
}
|