@umituz/react-native-ai-generation-content 1.17.310 → 1.18.0
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/features/anime-selfie/domain/types/anime-selfie.types.ts +1 -0
- package/src/features/anime-selfie/presentation/hooks/useAnimeSelfieFeature.ts +31 -126
- package/src/features/hd-touch-up/domain/types/hd-touch-up.types.ts +1 -0
- package/src/features/hd-touch-up/presentation/hooks/useHDTouchUpFeature.ts +25 -105
- package/src/features/image-to-image/presentation/hooks/useDualImageFeature.ts +111 -75
- package/src/features/image-to-image/presentation/hooks/useImageWithPromptFeature.ts +115 -84
- package/src/features/image-to-image/presentation/hooks/useSingleImageFeature.ts +93 -69
- package/src/features/photo-restoration/domain/types/photo-restore.types.ts +1 -0
- package/src/features/photo-restoration/presentation/hooks/usePhotoRestoreFeature.ts +25 -121
- package/src/features/remove-object/presentation/hooks/useRemoveObjectFeature.ts +125 -79
- package/src/features/shared/dual-image-video/presentation/hooks/useDualImageVideoFeature.ts +106 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
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",
|
|
@@ -59,6 +59,7 @@ export interface AnimeSelfieTranslations {
|
|
|
59
59
|
export type AnimeSelfieResultExtractor = (result: unknown) => string | undefined;
|
|
60
60
|
|
|
61
61
|
export interface AnimeSelfieFeatureConfig {
|
|
62
|
+
featureType: "anime-selfie";
|
|
62
63
|
creditCost?: number;
|
|
63
64
|
defaultStyle?: AnimeSelfieStyle;
|
|
64
65
|
extractResult?: AnimeSelfieResultExtractor;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useAnimeSelfieFeature Hook
|
|
3
|
-
*
|
|
3
|
+
* Uses base single image hook for anime selfie transformation
|
|
4
|
+
* Uses centralized orchestrator for credit/error handling
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
import {
|
|
9
|
+
useSingleImageFeature,
|
|
10
|
+
type BaseSingleImageHookReturn,
|
|
11
|
+
} from "../../../image-to-image";
|
|
12
|
+
import type { AlertMessages } from "../../../../presentation/hooks/generation";
|
|
9
13
|
import { createAnimeSelfiePrompt } from "../../../../domains/prompts";
|
|
10
|
-
import type {
|
|
11
|
-
AnimeSelfieFeatureState,
|
|
12
|
-
AnimeSelfieFeatureConfig,
|
|
13
|
-
AnimeSelfieResult,
|
|
14
|
-
} from "../../domain/types";
|
|
14
|
+
import type { AnimeSelfieFeatureConfig } from "../../domain/types";
|
|
15
15
|
|
|
16
16
|
export interface UseAnimeSelfieFeatureProps {
|
|
17
17
|
config: AnimeSelfieFeatureConfig;
|
|
@@ -20,135 +20,40 @@ export interface UseAnimeSelfieFeatureProps {
|
|
|
20
20
|
onBeforeProcess?: () => Promise<boolean>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export interface
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
export interface UseAnimeSelfieFeatureOptions {
|
|
24
|
+
/** Alert messages for error handling */
|
|
25
|
+
alertMessages?: AlertMessages;
|
|
26
|
+
/** User ID for credit operations */
|
|
27
|
+
userId?: string;
|
|
28
|
+
/** Callback when credits are exhausted */
|
|
29
|
+
onCreditsExhausted?: () => void;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
imageUri: null,
|
|
32
|
-
processedUrl: null,
|
|
33
|
-
isProcessing: false,
|
|
34
|
-
progress: 0,
|
|
35
|
-
error: null,
|
|
36
|
-
};
|
|
32
|
+
export interface UseAnimeSelfieFeatureReturn extends BaseSingleImageHookReturn {}
|
|
37
33
|
|
|
38
34
|
export function useAnimeSelfieFeature(
|
|
39
35
|
props: UseAnimeSelfieFeatureProps,
|
|
36
|
+
options?: UseAnimeSelfieFeatureOptions,
|
|
40
37
|
): UseAnimeSelfieFeatureReturn {
|
|
41
38
|
const { config, onSelectImage, onSaveImage, onBeforeProcess } = props;
|
|
42
|
-
const [state, setState] = useState<AnimeSelfieFeatureState>(initialState);
|
|
43
|
-
const creationIdRef = useRef<string | null>(null);
|
|
44
39
|
|
|
45
40
|
const promptConfig = useMemo(
|
|
46
41
|
() => createAnimeSelfiePrompt(config.defaultStyle),
|
|
47
42
|
[config.defaultStyle],
|
|
48
43
|
);
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
60
|
-
}
|
|
61
|
-
}, [onSelectImage, config]);
|
|
62
|
-
|
|
63
|
-
const handleProgress = useCallback((progress: number) => {
|
|
64
|
-
setState((prev) => ({ ...prev, progress }));
|
|
65
|
-
}, []);
|
|
66
|
-
|
|
67
|
-
const process = useCallback(async () => {
|
|
68
|
-
if (!state.imageUri) return;
|
|
69
|
-
|
|
70
|
-
if (onBeforeProcess) {
|
|
71
|
-
const canProceed = await onBeforeProcess();
|
|
72
|
-
if (!canProceed) return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const creationId = generateUUID();
|
|
76
|
-
creationIdRef.current = creationId;
|
|
77
|
-
|
|
78
|
-
setState((prev) => ({
|
|
79
|
-
...prev,
|
|
80
|
-
isProcessing: true,
|
|
81
|
-
progress: 0,
|
|
82
|
-
error: null,
|
|
83
|
-
}));
|
|
84
|
-
|
|
85
|
-
config.onProcessingStart?.({ creationId, imageUri: state.imageUri });
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const imageBase64 = await config.prepareImage(state.imageUri);
|
|
89
|
-
|
|
90
|
-
const result = await executeImageFeature(
|
|
91
|
-
"anime-selfie",
|
|
92
|
-
{
|
|
93
|
-
imageBase64,
|
|
94
|
-
prompt: promptConfig.prompt,
|
|
95
|
-
options: {
|
|
96
|
-
guidance_scale: promptConfig.guidance_scale,
|
|
97
|
-
},
|
|
45
|
+
// Cast config to any to bypass strict type checking while maintaining runtime behavior
|
|
46
|
+
return useSingleImageFeature(
|
|
47
|
+
{ config: config as never, onSelectImage, onSaveImage, onBeforeProcess },
|
|
48
|
+
{
|
|
49
|
+
buildInput: (imageBase64) => ({
|
|
50
|
+
imageBase64,
|
|
51
|
+
prompt: promptConfig.prompt,
|
|
52
|
+
options: {
|
|
53
|
+
guidance_scale: promptConfig.guidance_scale,
|
|
98
54
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
setState((prev) => ({
|
|
104
|
-
...prev,
|
|
105
|
-
isProcessing: false,
|
|
106
|
-
processedUrl: result.imageUrl!,
|
|
107
|
-
progress: 100,
|
|
108
|
-
}));
|
|
109
|
-
config.onProcessingComplete?.({ ...result, creationId } as AnimeSelfieResult);
|
|
110
|
-
} else {
|
|
111
|
-
const errorMessage = result.error || "Processing failed";
|
|
112
|
-
setState((prev) => ({
|
|
113
|
-
...prev,
|
|
114
|
-
isProcessing: false,
|
|
115
|
-
error: errorMessage,
|
|
116
|
-
progress: 0,
|
|
117
|
-
}));
|
|
118
|
-
config.onError?.(errorMessage, creationId);
|
|
119
|
-
}
|
|
120
|
-
} catch (error) {
|
|
121
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
122
|
-
setState((prev) => ({
|
|
123
|
-
...prev,
|
|
124
|
-
isProcessing: false,
|
|
125
|
-
error: errorMessage,
|
|
126
|
-
progress: 0,
|
|
127
|
-
}));
|
|
128
|
-
config.onError?.(errorMessage, creationIdRef.current ?? undefined);
|
|
129
|
-
}
|
|
130
|
-
}, [state.imageUri, config, handleProgress, onBeforeProcess, promptConfig]);
|
|
131
|
-
|
|
132
|
-
const save = useCallback(async () => {
|
|
133
|
-
if (!state.processedUrl) return;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
await onSaveImage(state.processedUrl);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
140
|
-
}
|
|
141
|
-
}, [state.processedUrl, onSaveImage]);
|
|
142
|
-
|
|
143
|
-
const reset = useCallback(() => {
|
|
144
|
-
setState(initialState);
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
...state,
|
|
149
|
-
selectImage,
|
|
150
|
-
process,
|
|
151
|
-
save,
|
|
152
|
-
reset,
|
|
153
|
-
};
|
|
55
|
+
}),
|
|
56
|
+
...options,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
154
59
|
}
|
|
@@ -51,6 +51,7 @@ export interface HDTouchUpTranslations {
|
|
|
51
51
|
export type HDTouchUpResultExtractor = (result: unknown) => string | undefined;
|
|
52
52
|
|
|
53
53
|
export interface HDTouchUpFeatureConfig {
|
|
54
|
+
featureType: "hd-touch-up";
|
|
54
55
|
creditCost?: number;
|
|
55
56
|
extractResult?: HDTouchUpResultExtractor;
|
|
56
57
|
prepareImage: (imageUri: string) => Promise<string>;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useHDTouchUpFeature Hook
|
|
3
|
-
*
|
|
3
|
+
* Uses base single image hook for HD touch up
|
|
4
|
+
* Uses centralized orchestrator for credit/error handling
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "../../domain/types";
|
|
7
|
+
import {
|
|
8
|
+
useSingleImageFeature,
|
|
9
|
+
type BaseSingleImageHookReturn,
|
|
10
|
+
} from "../../../image-to-image";
|
|
11
|
+
import type { AlertMessages } from "../../../../presentation/hooks/generation";
|
|
12
|
+
import type { HDTouchUpFeatureConfig } from "../../domain/types";
|
|
13
13
|
|
|
14
14
|
export interface UseHDTouchUpFeatureProps {
|
|
15
15
|
config: HDTouchUpFeatureConfig;
|
|
@@ -18,109 +18,29 @@ export interface UseHDTouchUpFeatureProps {
|
|
|
18
18
|
onBeforeProcess?: () => Promise<boolean>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export interface
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
export interface UseHDTouchUpFeatureOptions {
|
|
22
|
+
/** Alert messages for error handling */
|
|
23
|
+
alertMessages?: AlertMessages;
|
|
24
|
+
/** User ID for credit operations */
|
|
25
|
+
userId?: string;
|
|
26
|
+
/** Callback when credits are exhausted */
|
|
27
|
+
onCreditsExhausted?: () => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
imageUri: null,
|
|
30
|
-
processedUrl: null,
|
|
31
|
-
isProcessing: false,
|
|
32
|
-
progress: 0,
|
|
33
|
-
error: null,
|
|
34
|
-
};
|
|
30
|
+
export interface UseHDTouchUpFeatureReturn extends BaseSingleImageHookReturn {}
|
|
35
31
|
|
|
36
32
|
export function useHDTouchUpFeature(
|
|
37
33
|
props: UseHDTouchUpFeatureProps,
|
|
34
|
+
options?: UseHDTouchUpFeatureOptions,
|
|
38
35
|
): UseHDTouchUpFeatureReturn {
|
|
39
36
|
const { config, onSelectImage, onSaveImage, onBeforeProcess } = props;
|
|
40
|
-
const [state, setState] = useState<HDTouchUpFeatureState>(initialState);
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
52
|
-
}
|
|
53
|
-
}, [onSelectImage, config]);
|
|
54
|
-
|
|
55
|
-
const handleProgress = useCallback((progress: number) => {
|
|
56
|
-
setState((prev) => ({ ...prev, progress }));
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
const process = useCallback(async () => {
|
|
60
|
-
if (!state.imageUri) return;
|
|
61
|
-
|
|
62
|
-
if (onBeforeProcess) {
|
|
63
|
-
const canProceed = await onBeforeProcess();
|
|
64
|
-
if (!canProceed) return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
setState((prev) => ({
|
|
68
|
-
...prev,
|
|
69
|
-
isProcessing: true,
|
|
70
|
-
progress: 0,
|
|
71
|
-
error: null,
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
config.onProcessingStart?.();
|
|
75
|
-
|
|
76
|
-
const imageBase64 = await config.prepareImage(state.imageUri);
|
|
77
|
-
|
|
78
|
-
const result = await executeImageFeature(
|
|
79
|
-
"hd-touch-up",
|
|
80
|
-
{ imageBase64 },
|
|
81
|
-
{ extractResult: config.extractResult, onProgress: handleProgress },
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
if (result.success && result.imageUrl) {
|
|
85
|
-
setState((prev) => ({
|
|
86
|
-
...prev,
|
|
87
|
-
isProcessing: false,
|
|
88
|
-
processedUrl: result.imageUrl!,
|
|
89
|
-
progress: 100,
|
|
90
|
-
}));
|
|
91
|
-
config.onProcessingComplete?.(result as HDTouchUpResult);
|
|
92
|
-
} else {
|
|
93
|
-
const errorMessage = result.error || "Processing failed";
|
|
94
|
-
setState((prev) => ({
|
|
95
|
-
...prev,
|
|
96
|
-
isProcessing: false,
|
|
97
|
-
error: errorMessage,
|
|
98
|
-
progress: 0,
|
|
99
|
-
}));
|
|
100
|
-
config.onError?.(errorMessage);
|
|
101
|
-
}
|
|
102
|
-
}, [state.imageUri, config, handleProgress, onBeforeProcess]);
|
|
103
|
-
|
|
104
|
-
const save = useCallback(async () => {
|
|
105
|
-
if (!state.processedUrl) return;
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
await onSaveImage(state.processedUrl);
|
|
109
|
-
} catch (error) {
|
|
110
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
111
|
-
setState((prev) => ({ ...prev, error: message }));
|
|
112
|
-
}
|
|
113
|
-
}, [state.processedUrl, onSaveImage]);
|
|
114
|
-
|
|
115
|
-
const reset = useCallback(() => {
|
|
116
|
-
setState(initialState);
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
...state,
|
|
121
|
-
selectImage,
|
|
122
|
-
process,
|
|
123
|
-
save,
|
|
124
|
-
reset,
|
|
125
|
-
};
|
|
38
|
+
// Cast config to any to bypass strict type checking while maintaining runtime behavior
|
|
39
|
+
return useSingleImageFeature(
|
|
40
|
+
{ config: config as never, onSelectImage, onSaveImage, onBeforeProcess },
|
|
41
|
+
{
|
|
42
|
+
buildInput: (imageBase64) => ({ imageBase64 }),
|
|
43
|
+
...options,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
126
46
|
}
|
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useDualImageFeature Hook Factory
|
|
3
3
|
* Base hook for dual image processing features (e.g., face-swap)
|
|
4
|
+
* Uses centralized orchestrator for credit/error handling
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import { useState, useCallback, useRef } from "react";
|
|
7
|
+
import { useState, useCallback, useRef, useMemo } from "react";
|
|
7
8
|
import { generateUUID } from "@umituz/react-native-design-system";
|
|
8
9
|
import { executeImageFeature } from "../../../../infrastructure/services";
|
|
10
|
+
import {
|
|
11
|
+
useGenerationOrchestrator,
|
|
12
|
+
type GenerationStrategy,
|
|
13
|
+
type AlertMessages,
|
|
14
|
+
} from "../../../../presentation/hooks/generation";
|
|
9
15
|
import type {
|
|
10
|
-
BaseDualImageState,
|
|
11
16
|
BaseDualImageHookProps,
|
|
12
17
|
BaseDualImageHookReturn,
|
|
13
18
|
DualImageConfig,
|
|
14
19
|
BaseImageResult,
|
|
15
20
|
} from "../../domain/types";
|
|
16
21
|
|
|
17
|
-
const INITIAL_STATE: BaseDualImageState = {
|
|
18
|
-
sourceImageUri: null,
|
|
19
|
-
targetImageUri: null,
|
|
20
|
-
processedUrl: null,
|
|
21
|
-
isProcessing: false,
|
|
22
|
-
progress: 0,
|
|
23
|
-
error: null,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
22
|
export interface DualImageFeatureOptions<TConfig extends DualImageConfig> {
|
|
27
23
|
buildInput?: (
|
|
28
24
|
sourceBase64: string,
|
|
29
25
|
targetBase64: string,
|
|
30
26
|
config: TConfig,
|
|
31
27
|
) => Record<string, unknown>;
|
|
28
|
+
/** Alert messages for error handling */
|
|
29
|
+
alertMessages?: AlertMessages;
|
|
30
|
+
/** User ID for credit operations */
|
|
31
|
+
userId?: string;
|
|
32
|
+
/** Callback when credits are exhausted */
|
|
33
|
+
onCreditsExhausted?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_ALERT_MESSAGES: AlertMessages = {
|
|
37
|
+
networkError: "No internet connection. Please check your network.",
|
|
38
|
+
policyViolation: "Content not allowed. Please try different images.",
|
|
39
|
+
saveFailed: "Failed to save result. Please try again.",
|
|
40
|
+
creditFailed: "Credit operation failed. Please try again.",
|
|
41
|
+
unknown: "An error occurred. Please try again.",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
interface DualImageInput {
|
|
45
|
+
sourceImageBase64: string;
|
|
46
|
+
targetImageBase64: string;
|
|
47
|
+
options?: Record<string, unknown>;
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
export function useDualImageFeature<
|
|
@@ -39,19 +55,65 @@ export function useDualImageFeature<
|
|
|
39
55
|
options?: DualImageFeatureOptions<TConfig>,
|
|
40
56
|
): BaseDualImageHookReturn {
|
|
41
57
|
const { config, onSelectSourceImage, onSelectTargetImage, onSaveImage, onBeforeProcess } = props;
|
|
42
|
-
|
|
58
|
+
|
|
59
|
+
// Image selection state (separate from orchestrator state)
|
|
60
|
+
const [sourceImageUri, setSourceImageUri] = useState<string | null>(null);
|
|
61
|
+
const [targetImageUri, setTargetImageUri] = useState<string | null>(null);
|
|
62
|
+
const [imageError, setImageError] = useState<string | null>(null);
|
|
43
63
|
const creationIdRef = useRef<string | null>(null);
|
|
44
64
|
|
|
65
|
+
// Create strategy for orchestrator
|
|
66
|
+
const strategy: GenerationStrategy<DualImageInput, string> = useMemo(
|
|
67
|
+
() => ({
|
|
68
|
+
execute: async (input, onProgress) => {
|
|
69
|
+
const executorInput = input.options
|
|
70
|
+
? { ...input.options }
|
|
71
|
+
: { imageBase64: input.sourceImageBase64, targetImageBase64: input.targetImageBase64 };
|
|
72
|
+
|
|
73
|
+
const result = await executeImageFeature(
|
|
74
|
+
config.featureType,
|
|
75
|
+
executorInput,
|
|
76
|
+
{ extractResult: config.extractResult, onProgress },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!result.success || !result.imageUrl) {
|
|
80
|
+
throw new Error(result.error || "Processing failed");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Notify completion with creationId
|
|
84
|
+
const creationId = creationIdRef.current;
|
|
85
|
+
if (creationId) {
|
|
86
|
+
config.onProcessingComplete?.({ ...result, creationId } as unknown as TResult);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result.imageUrl;
|
|
90
|
+
},
|
|
91
|
+
getCreditCost: () => config.creditCost || 1,
|
|
92
|
+
}),
|
|
93
|
+
[config],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Use orchestrator for generation
|
|
97
|
+
const orchestrator = useGenerationOrchestrator(strategy, {
|
|
98
|
+
userId: options?.userId,
|
|
99
|
+
alertMessages: options?.alertMessages || DEFAULT_ALERT_MESSAGES,
|
|
100
|
+
onCreditsExhausted: options?.onCreditsExhausted,
|
|
101
|
+
onError: (error) => {
|
|
102
|
+
config.onError?.(error.message, creationIdRef.current ?? undefined);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
45
106
|
const selectSourceImage = useCallback(async () => {
|
|
46
107
|
try {
|
|
47
108
|
const uri = await onSelectSourceImage();
|
|
48
109
|
if (uri) {
|
|
49
|
-
|
|
110
|
+
setSourceImageUri(uri);
|
|
111
|
+
setImageError(null);
|
|
50
112
|
config.onSourceImageSelect?.(uri);
|
|
51
113
|
}
|
|
52
114
|
} catch (error) {
|
|
53
115
|
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
-
|
|
116
|
+
setImageError(message);
|
|
55
117
|
}
|
|
56
118
|
}, [onSelectSourceImage, config]);
|
|
57
119
|
|
|
@@ -59,21 +121,18 @@ export function useDualImageFeature<
|
|
|
59
121
|
try {
|
|
60
122
|
const uri = await onSelectTargetImage();
|
|
61
123
|
if (uri) {
|
|
62
|
-
|
|
124
|
+
setTargetImageUri(uri);
|
|
125
|
+
setImageError(null);
|
|
63
126
|
config.onTargetImageSelect?.(uri);
|
|
64
127
|
}
|
|
65
128
|
} catch (error) {
|
|
66
129
|
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
-
|
|
130
|
+
setImageError(message);
|
|
68
131
|
}
|
|
69
132
|
}, [onSelectTargetImage, config]);
|
|
70
133
|
|
|
71
|
-
const handleProgress = useCallback((progress: number) => {
|
|
72
|
-
setState((prev) => ({ ...prev, progress }));
|
|
73
|
-
}, []);
|
|
74
|
-
|
|
75
134
|
const process = useCallback(async () => {
|
|
76
|
-
if (!
|
|
135
|
+
if (!sourceImageUri || !targetImageUri) return;
|
|
77
136
|
|
|
78
137
|
if (onBeforeProcess) {
|
|
79
138
|
const canProceed = await onBeforeProcess();
|
|
@@ -83,82 +142,59 @@ export function useDualImageFeature<
|
|
|
83
142
|
const creationId = generateUUID();
|
|
84
143
|
creationIdRef.current = creationId;
|
|
85
144
|
|
|
86
|
-
setState((prev) => ({
|
|
87
|
-
...prev,
|
|
88
|
-
isProcessing: true,
|
|
89
|
-
progress: 0,
|
|
90
|
-
error: null,
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
145
|
config.onProcessingStart?.({
|
|
94
146
|
creationId,
|
|
95
|
-
sourceImageUri
|
|
96
|
-
targetImageUri
|
|
147
|
+
sourceImageUri,
|
|
148
|
+
targetImageUri,
|
|
97
149
|
});
|
|
98
150
|
|
|
99
151
|
try {
|
|
100
152
|
const [sourceBase64, targetBase64] = await Promise.all([
|
|
101
|
-
config.prepareImage(
|
|
102
|
-
config.prepareImage(
|
|
153
|
+
config.prepareImage(sourceImageUri),
|
|
154
|
+
config.prepareImage(targetImageUri),
|
|
103
155
|
]);
|
|
104
156
|
|
|
105
|
-
const input = options?.buildInput
|
|
106
|
-
?
|
|
157
|
+
const input: DualImageInput = options?.buildInput
|
|
158
|
+
? {
|
|
159
|
+
sourceImageBase64: sourceBase64,
|
|
160
|
+
targetImageBase64: targetBase64,
|
|
161
|
+
options: options.buildInput(sourceBase64, targetBase64, config),
|
|
162
|
+
}
|
|
107
163
|
: { sourceImageBase64: sourceBase64, targetImageBase64: targetBase64 };
|
|
108
164
|
|
|
109
|
-
|
|
110
|
-
config.featureType,
|
|
111
|
-
input,
|
|
112
|
-
{ extractResult: config.extractResult, onProgress: handleProgress },
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
if (result.success && result.imageUrl) {
|
|
116
|
-
setState((prev) => ({
|
|
117
|
-
...prev,
|
|
118
|
-
isProcessing: false,
|
|
119
|
-
processedUrl: result.imageUrl!,
|
|
120
|
-
progress: 100,
|
|
121
|
-
}));
|
|
122
|
-
config.onProcessingComplete?.({ ...result, creationId } as unknown as TResult);
|
|
123
|
-
} else {
|
|
124
|
-
const errorMessage = result.error || "Processing failed";
|
|
125
|
-
setState((prev) => ({
|
|
126
|
-
...prev,
|
|
127
|
-
isProcessing: false,
|
|
128
|
-
error: errorMessage,
|
|
129
|
-
progress: 0,
|
|
130
|
-
}));
|
|
131
|
-
config.onError?.(errorMessage, creationId);
|
|
132
|
-
}
|
|
165
|
+
await orchestrator.generate(input);
|
|
133
166
|
} catch (error) {
|
|
134
|
-
|
|
135
|
-
setState((prev) => ({
|
|
136
|
-
...prev,
|
|
137
|
-
isProcessing: false,
|
|
138
|
-
error: message,
|
|
139
|
-
progress: 0,
|
|
140
|
-
}));
|
|
141
|
-
config.onError?.(message, creationIdRef.current ?? undefined);
|
|
167
|
+
// Error already handled by orchestrator
|
|
142
168
|
}
|
|
143
|
-
}, [
|
|
169
|
+
}, [sourceImageUri, targetImageUri, config, options, onBeforeProcess, orchestrator]);
|
|
144
170
|
|
|
145
171
|
const save = useCallback(async () => {
|
|
146
|
-
if (!
|
|
172
|
+
if (!orchestrator.result) return;
|
|
147
173
|
|
|
148
174
|
try {
|
|
149
|
-
await onSaveImage(
|
|
175
|
+
await onSaveImage(orchestrator.result);
|
|
150
176
|
} catch (error) {
|
|
151
177
|
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
-
|
|
178
|
+
setImageError(message);
|
|
153
179
|
}
|
|
154
|
-
}, [
|
|
180
|
+
}, [orchestrator.result, onSaveImage]);
|
|
155
181
|
|
|
156
182
|
const reset = useCallback(() => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
setSourceImageUri(null);
|
|
184
|
+
setTargetImageUri(null);
|
|
185
|
+
setImageError(null);
|
|
186
|
+
creationIdRef.current = null;
|
|
187
|
+
orchestrator.reset();
|
|
188
|
+
}, [orchestrator]);
|
|
189
|
+
|
|
190
|
+
// Combine states for backward compatibility
|
|
160
191
|
return {
|
|
161
|
-
|
|
192
|
+
sourceImageUri,
|
|
193
|
+
targetImageUri,
|
|
194
|
+
processedUrl: orchestrator.result,
|
|
195
|
+
isProcessing: orchestrator.isGenerating,
|
|
196
|
+
progress: orchestrator.progress,
|
|
197
|
+
error: orchestrator.error?.message || imageError,
|
|
162
198
|
selectSourceImage,
|
|
163
199
|
selectTargetImage,
|
|
164
200
|
process,
|