@umituz/react-native-ai-generation-content 1.27.7 → 1.27.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +3 -30
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +3 -30
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.constants.ts +0 -2
- package/src/domains/scenarios/configs/wizard-configs.ts +28 -28
- package/src/domains/scenarios/index.ts +3 -3
- package/src/features/image-to-video/README.md +414 -0
- package/src/features/image-to-video/domain/constants/animation.constants.ts +47 -0
- package/src/features/image-to-video/domain/constants/duration.constants.ts +13 -0
- package/src/features/image-to-video/domain/constants/form.constants.ts +22 -0
- package/src/features/image-to-video/domain/constants/index.ts +23 -0
- package/src/features/image-to-video/domain/constants/music.constants.ts +53 -0
- package/src/features/image-to-video/domain/index.ts +5 -0
- package/src/features/image-to-video/domain/types/animation.types.ts +20 -0
- package/src/features/image-to-video/domain/types/config.types.ts +56 -0
- package/src/features/image-to-video/domain/types/duration.types.ts +11 -0
- package/src/features/image-to-video/domain/types/form.types.ts +35 -0
- package/src/features/image-to-video/domain/types/image-to-video.types.ts +122 -0
- package/src/features/image-to-video/domain/types/index.ts +39 -0
- package/src/features/image-to-video/domain/types/music.types.ts +21 -0
- package/src/features/image-to-video/index.ts +116 -0
- package/src/features/image-to-video/infrastructure/index.ts +1 -0
- package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +165 -0
- package/src/features/image-to-video/infrastructure/services/index.ts +5 -0
- package/src/features/image-to-video/presentation/components/AddMoreCard.tsx +52 -0
- package/src/features/image-to-video/presentation/components/AnimationStyleSelector.tsx +135 -0
- package/src/features/image-to-video/presentation/components/DurationSelector.tsx +110 -0
- package/src/features/image-to-video/presentation/components/EmptyGridState.tsx +69 -0
- package/src/features/image-to-video/presentation/components/GridImageItem.tsx +64 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.styles.ts +84 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.tsx +77 -0
- package/src/features/image-to-video/presentation/components/ImageSelectionGrid.types.ts +18 -0
- package/src/features/image-to-video/presentation/components/MusicMoodSelector.tsx +181 -0
- package/src/features/image-to-video/presentation/components/index.ts +30 -0
- package/src/features/image-to-video/presentation/hooks/index.ts +27 -0
- package/src/features/image-to-video/presentation/hooks/useFormState.ts +116 -0
- package/src/features/image-to-video/presentation/hooks/useGeneration.ts +85 -0
- package/src/features/image-to-video/presentation/hooks/useGenerationExecution.ts +143 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoFeature.ts +107 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoForm.ts +119 -0
- package/src/features/image-to-video/presentation/hooks/useImageToVideoValidation.ts +46 -0
- package/src/features/image-to-video/presentation/index.ts +5 -0
- package/src/features/text-to-image/README.md +394 -0
- package/src/features/text-to-image/domain/constants/index.ts +25 -0
- package/src/features/text-to-image/domain/constants/options.constants.ts +39 -0
- package/src/features/text-to-image/domain/constants/styles.constants.ts +34 -0
- package/src/features/text-to-image/domain/index.ts +7 -0
- package/src/features/text-to-image/domain/types/config.types.ts +75 -0
- package/src/features/text-to-image/domain/types/form.types.ts +58 -0
- package/src/features/text-to-image/domain/types/index.ts +38 -0
- package/src/features/text-to-image/domain/types/text-to-image.types.ts +58 -0
- package/src/features/text-to-image/index.ts +116 -0
- package/src/features/text-to-image/infrastructure/index.ts +1 -0
- package/src/features/text-to-image/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-image/infrastructure/services/text-to-image-executor.ts +147 -0
- package/src/features/text-to-image/presentation/components/index.ts +30 -0
- package/src/features/text-to-image/presentation/hooks/index.ts +30 -0
- package/src/features/text-to-image/presentation/hooks/useFormState.ts +103 -0
- package/src/features/text-to-image/presentation/hooks/useGeneration.ts +134 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageFeature.ts +111 -0
- package/src/features/text-to-image/presentation/hooks/useTextToImageForm.ts +58 -0
- package/src/features/text-to-image/presentation/index.ts +7 -0
- package/src/features/text-to-video/README.md +412 -0
- package/src/features/text-to-video/domain/index.ts +1 -0
- package/src/features/text-to-video/domain/types/callback.types.ts +69 -0
- package/src/features/text-to-video/domain/types/component.types.ts +106 -0
- package/src/features/text-to-video/domain/types/config.types.ts +61 -0
- package/src/features/text-to-video/domain/types/index.ts +56 -0
- package/src/features/text-to-video/domain/types/request.types.ts +36 -0
- package/src/features/text-to-video/domain/types/state.types.ts +53 -0
- package/src/features/text-to-video/index.ts +68 -0
- package/src/features/text-to-video/infrastructure/index.ts +1 -0
- package/src/features/text-to-video/infrastructure/services/index.ts +5 -0
- package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +141 -0
- package/src/features/text-to-video/presentation/components/FrameSelector.tsx +153 -0
- package/src/features/text-to-video/presentation/components/GenerationTabs.tsx +73 -0
- package/src/features/text-to-video/presentation/components/HeroSection.tsx +61 -0
- package/src/features/text-to-video/presentation/components/HintCarousel.tsx +96 -0
- package/src/features/text-to-video/presentation/components/OptionsPanel.tsx +121 -0
- package/src/features/text-to-video/presentation/components/index.ts +10 -0
- package/src/features/text-to-video/presentation/hooks/index.ts +17 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoFeature.ts +187 -0
- package/src/features/text-to-video/presentation/hooks/useTextToVideoForm.ts +134 -0
- package/src/features/text-to-video/presentation/index.ts +7 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Selection Grid Component
|
|
3
|
+
* Generic component for image selection display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, ScrollView } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import { EmptyGridState } from "./EmptyGridState";
|
|
13
|
+
import { GridImageItem } from "./GridImageItem";
|
|
14
|
+
import { AddMoreCard } from "./AddMoreCard";
|
|
15
|
+
import { createImageSelectionGridStyles } from "./ImageSelectionGrid.styles";
|
|
16
|
+
import type { ImageSelectionGridProps } from "./ImageSelectionGrid.types";
|
|
17
|
+
|
|
18
|
+
export const ImageSelectionGrid: React.FC<ImageSelectionGridProps> = ({
|
|
19
|
+
images,
|
|
20
|
+
maxImages,
|
|
21
|
+
onSelectImages,
|
|
22
|
+
onRemoveImage,
|
|
23
|
+
translations,
|
|
24
|
+
}) => {
|
|
25
|
+
const tokens = useAppDesignTokens();
|
|
26
|
+
const styles = useMemo(
|
|
27
|
+
() => createImageSelectionGridStyles(tokens),
|
|
28
|
+
[tokens]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (images.length === 0) {
|
|
32
|
+
return (
|
|
33
|
+
<EmptyGridState
|
|
34
|
+
styles={styles}
|
|
35
|
+
maxImages={maxImages}
|
|
36
|
+
translations={translations}
|
|
37
|
+
onSelectImages={onSelectImages}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.section}>
|
|
44
|
+
<AtomicText
|
|
45
|
+
type="bodyMedium"
|
|
46
|
+
style={[styles.label, { color: tokens.colors.textPrimary }]}
|
|
47
|
+
>
|
|
48
|
+
{translations.selectedImages} ({images.length}/{maxImages})
|
|
49
|
+
</AtomicText>
|
|
50
|
+
<ScrollView
|
|
51
|
+
horizontal
|
|
52
|
+
showsHorizontalScrollIndicator={false}
|
|
53
|
+
style={styles.scroll}
|
|
54
|
+
>
|
|
55
|
+
{images.map((uri, index) => (
|
|
56
|
+
<GridImageItem
|
|
57
|
+
key={index}
|
|
58
|
+
styles={styles}
|
|
59
|
+
uri={uri}
|
|
60
|
+
index={index}
|
|
61
|
+
onRemove={() => onRemoveImage(index)}
|
|
62
|
+
/>
|
|
63
|
+
))}
|
|
64
|
+
|
|
65
|
+
{images.length < maxImages && (
|
|
66
|
+
<AddMoreCard
|
|
67
|
+
styles={styles}
|
|
68
|
+
addMoreText={translations.addMore}
|
|
69
|
+
onPress={onSelectImages}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</ScrollView>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type { ImageSelectionGridProps, ImageSelectionGridTranslations } from "./ImageSelectionGrid.types";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImageSelectionGrid Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ImageSelectionGridTranslations {
|
|
6
|
+
selectedImages: string;
|
|
7
|
+
selectImages: string;
|
|
8
|
+
chooseUpTo: string;
|
|
9
|
+
addMore: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ImageSelectionGridProps {
|
|
13
|
+
images: string[];
|
|
14
|
+
maxImages: number;
|
|
15
|
+
onSelectImages: () => void;
|
|
16
|
+
onRemoveImage: (index: number) => void;
|
|
17
|
+
translations: ImageSelectionGridTranslations;
|
|
18
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Music Mood Selector Component
|
|
3
|
+
* Generic component for music mood selection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
} from "@umituz/react-native-design-system";
|
|
13
|
+
import type { MusicMood, MusicMoodId } from "../../domain/types";
|
|
14
|
+
|
|
15
|
+
export interface MusicMoodSelectorProps {
|
|
16
|
+
moods: MusicMood[];
|
|
17
|
+
selectedMood: MusicMoodId;
|
|
18
|
+
onMoodSelect: (moodId: MusicMoodId) => void;
|
|
19
|
+
label: string;
|
|
20
|
+
hasCustomAudio?: boolean;
|
|
21
|
+
customAudioLabel?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const MusicMoodSelector: React.FC<MusicMoodSelectorProps> = ({
|
|
25
|
+
moods,
|
|
26
|
+
selectedMood,
|
|
27
|
+
onMoodSelect,
|
|
28
|
+
label,
|
|
29
|
+
hasCustomAudio = false,
|
|
30
|
+
customAudioLabel,
|
|
31
|
+
}) => {
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={componentStyles.section}>
|
|
36
|
+
<AtomicText
|
|
37
|
+
type="bodyMedium"
|
|
38
|
+
style={[componentStyles.label, { color: tokens.colors.textPrimary }]}
|
|
39
|
+
>
|
|
40
|
+
{label}
|
|
41
|
+
</AtomicText>
|
|
42
|
+
<ScrollView
|
|
43
|
+
horizontal
|
|
44
|
+
showsHorizontalScrollIndicator={false}
|
|
45
|
+
style={componentStyles.scroll}
|
|
46
|
+
>
|
|
47
|
+
{moods.map((mood) => {
|
|
48
|
+
const isSelected = selectedMood === mood.id;
|
|
49
|
+
return (
|
|
50
|
+
<TouchableOpacity
|
|
51
|
+
key={mood.id}
|
|
52
|
+
style={[
|
|
53
|
+
componentStyles.card,
|
|
54
|
+
{
|
|
55
|
+
backgroundColor: isSelected
|
|
56
|
+
? tokens.colors.primary
|
|
57
|
+
: tokens.colors.surface,
|
|
58
|
+
borderColor: isSelected
|
|
59
|
+
? tokens.colors.primary
|
|
60
|
+
: tokens.colors.borderLight,
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
onPress={() => onMoodSelect(mood.id)}
|
|
64
|
+
activeOpacity={0.7}
|
|
65
|
+
>
|
|
66
|
+
<View
|
|
67
|
+
style={[
|
|
68
|
+
componentStyles.iconContainer,
|
|
69
|
+
{
|
|
70
|
+
backgroundColor: isSelected
|
|
71
|
+
? tokens.colors.modalOverlay
|
|
72
|
+
: tokens.colors.surface,
|
|
73
|
+
},
|
|
74
|
+
]}
|
|
75
|
+
>
|
|
76
|
+
<AtomicIcon
|
|
77
|
+
name={mood.icon as never}
|
|
78
|
+
size="lg"
|
|
79
|
+
color={isSelected ? "onSurface" : "primary"}
|
|
80
|
+
/>
|
|
81
|
+
</View>
|
|
82
|
+
<AtomicText
|
|
83
|
+
type="bodySmall"
|
|
84
|
+
style={[
|
|
85
|
+
componentStyles.moodName,
|
|
86
|
+
{
|
|
87
|
+
color: isSelected
|
|
88
|
+
? tokens.colors.textInverse
|
|
89
|
+
: tokens.colors.textPrimary,
|
|
90
|
+
},
|
|
91
|
+
]}
|
|
92
|
+
>
|
|
93
|
+
{mood.name}
|
|
94
|
+
</AtomicText>
|
|
95
|
+
<AtomicText
|
|
96
|
+
type="labelSmall"
|
|
97
|
+
style={[
|
|
98
|
+
componentStyles.moodDescription,
|
|
99
|
+
{
|
|
100
|
+
color: isSelected
|
|
101
|
+
? tokens.colors.textInverse
|
|
102
|
+
: tokens.colors.textSecondary,
|
|
103
|
+
opacity: isSelected ? 0.9 : 0.7,
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
{mood.description}
|
|
108
|
+
</AtomicText>
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</ScrollView>
|
|
113
|
+
{selectedMood === "custom" && hasCustomAudio && customAudioLabel && (
|
|
114
|
+
<View
|
|
115
|
+
style={[
|
|
116
|
+
componentStyles.customBadge,
|
|
117
|
+
{ backgroundColor: tokens.colors.surfaceVariant },
|
|
118
|
+
]}
|
|
119
|
+
>
|
|
120
|
+
<AtomicIcon name="checkmark-circle-outline" size="sm" color="primary" />
|
|
121
|
+
<AtomicText
|
|
122
|
+
type="labelSmall"
|
|
123
|
+
style={[componentStyles.customBadgeText, { color: tokens.colors.primary }]}
|
|
124
|
+
>
|
|
125
|
+
{customAudioLabel}
|
|
126
|
+
</AtomicText>
|
|
127
|
+
</View>
|
|
128
|
+
)}
|
|
129
|
+
</View>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const componentStyles = StyleSheet.create({
|
|
134
|
+
section: {
|
|
135
|
+
padding: 16,
|
|
136
|
+
marginBottom: 8,
|
|
137
|
+
},
|
|
138
|
+
label: {
|
|
139
|
+
fontWeight: "600",
|
|
140
|
+
marginBottom: 12,
|
|
141
|
+
},
|
|
142
|
+
scroll: {
|
|
143
|
+
marginHorizontal: -16,
|
|
144
|
+
paddingHorizontal: 16,
|
|
145
|
+
},
|
|
146
|
+
card: {
|
|
147
|
+
width: 140,
|
|
148
|
+
padding: 16,
|
|
149
|
+
borderRadius: 16,
|
|
150
|
+
borderWidth: 2,
|
|
151
|
+
marginRight: 12,
|
|
152
|
+
alignItems: "center",
|
|
153
|
+
},
|
|
154
|
+
iconContainer: {
|
|
155
|
+
width: 56,
|
|
156
|
+
height: 56,
|
|
157
|
+
borderRadius: 28,
|
|
158
|
+
alignItems: "center",
|
|
159
|
+
justifyContent: "center",
|
|
160
|
+
},
|
|
161
|
+
moodName: {
|
|
162
|
+
fontWeight: "600",
|
|
163
|
+
marginTop: 8,
|
|
164
|
+
textAlign: "center",
|
|
165
|
+
},
|
|
166
|
+
moodDescription: {
|
|
167
|
+
marginTop: 4,
|
|
168
|
+
textAlign: "center",
|
|
169
|
+
},
|
|
170
|
+
customBadge: {
|
|
171
|
+
flexDirection: "row",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
padding: 12,
|
|
174
|
+
borderRadius: 12,
|
|
175
|
+
marginTop: 12,
|
|
176
|
+
},
|
|
177
|
+
customBadgeText: {
|
|
178
|
+
marginLeft: 8,
|
|
179
|
+
flex: 1,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image-to-Video Components Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Animation Style Selector
|
|
6
|
+
export { AnimationStyleSelector as ImageToVideoAnimationStyleSelector } from "./AnimationStyleSelector";
|
|
7
|
+
export type { AnimationStyleSelectorProps as ImageToVideoAnimationStyleSelectorProps } from "./AnimationStyleSelector";
|
|
8
|
+
|
|
9
|
+
// Duration Selector
|
|
10
|
+
export { DurationSelector as ImageToVideoDurationSelector } from "./DurationSelector";
|
|
11
|
+
export type { DurationSelectorProps as ImageToVideoDurationSelectorProps } from "./DurationSelector";
|
|
12
|
+
|
|
13
|
+
// Music Mood Selector
|
|
14
|
+
export { MusicMoodSelector as ImageToVideoMusicMoodSelector } from "./MusicMoodSelector";
|
|
15
|
+
export type { MusicMoodSelectorProps as ImageToVideoMusicMoodSelectorProps } from "./MusicMoodSelector";
|
|
16
|
+
|
|
17
|
+
// Image Selection Grid
|
|
18
|
+
export { ImageSelectionGrid as ImageToVideoSelectionGrid } from "./ImageSelectionGrid";
|
|
19
|
+
export type {
|
|
20
|
+
ImageSelectionGridProps as ImageToVideoSelectionGridProps,
|
|
21
|
+
ImageSelectionGridTranslations as ImageToVideoSelectionGridTranslations,
|
|
22
|
+
} from "./ImageSelectionGrid";
|
|
23
|
+
|
|
24
|
+
// Hero Section - Using AIGenerationHero from common components
|
|
25
|
+
// export { HeroSection as ImageToVideoHeroSection } from "./HeroSection";
|
|
26
|
+
// export type { HeroSectionProps as ImageToVideoHeroSectionProps } from "./HeroSection";
|
|
27
|
+
|
|
28
|
+
// Action Components
|
|
29
|
+
export { GenerateButton as ImageToVideoGenerateButton } from "../../../../presentation/components/buttons";
|
|
30
|
+
export type { GenerateButtonProps as ImageToVideoGenerateButtonProps } from "../../../../presentation/components/buttons";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Form State Hook
|
|
2
|
+
export { useFormState as useImageToVideoFormState } from "./useFormState";
|
|
3
|
+
export type {
|
|
4
|
+
UseFormStateOptions as UseImageToVideoFormStateOptions,
|
|
5
|
+
UseFormStateReturn as UseImageToVideoFormStateReturn,
|
|
6
|
+
} from "./useFormState";
|
|
7
|
+
|
|
8
|
+
// Generation Hook
|
|
9
|
+
export { useGeneration as useImageToVideoGeneration } from "./useGeneration";
|
|
10
|
+
export type {
|
|
11
|
+
UseGenerationOptions as UseImageToVideoGenerationOptions,
|
|
12
|
+
UseGenerationReturn as UseImageToVideoGenerationReturn,
|
|
13
|
+
} from "./useGeneration";
|
|
14
|
+
|
|
15
|
+
// Combined Form Hook
|
|
16
|
+
export { useImageToVideoForm } from "./useImageToVideoForm";
|
|
17
|
+
export type {
|
|
18
|
+
UseImageToVideoFormOptions,
|
|
19
|
+
UseImageToVideoFormReturn,
|
|
20
|
+
} from "./useImageToVideoForm";
|
|
21
|
+
|
|
22
|
+
// Provider-based Feature Hook
|
|
23
|
+
export { useImageToVideoFeature } from "./useImageToVideoFeature";
|
|
24
|
+
export type {
|
|
25
|
+
UseImageToVideoFeatureProps,
|
|
26
|
+
UseImageToVideoFeatureReturn,
|
|
27
|
+
} from "./useImageToVideoFeature";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form State Hook for Image-to-Video
|
|
3
|
+
* Manages form state with actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useMemo } from "react";
|
|
7
|
+
import type {
|
|
8
|
+
ImageToVideoFormState,
|
|
9
|
+
ImageToVideoFormActions,
|
|
10
|
+
ImageToVideoFormDefaults,
|
|
11
|
+
AnimationStyleId,
|
|
12
|
+
MusicMoodId,
|
|
13
|
+
VideoDuration,
|
|
14
|
+
} from "../../domain/types";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_ANIMATION_STYLE_ID,
|
|
17
|
+
DEFAULT_MUSIC_MOOD_ID,
|
|
18
|
+
DEFAULT_VIDEO_DURATION,
|
|
19
|
+
} from "../../domain/constants";
|
|
20
|
+
|
|
21
|
+
export interface UseFormStateOptions {
|
|
22
|
+
defaults?: ImageToVideoFormDefaults;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseFormStateReturn {
|
|
26
|
+
state: ImageToVideoFormState;
|
|
27
|
+
actions: ImageToVideoFormActions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createInitialState(defaults?: ImageToVideoFormDefaults): ImageToVideoFormState {
|
|
31
|
+
return {
|
|
32
|
+
selectedImages: [],
|
|
33
|
+
animationStyle: defaults?.animationStyle ?? DEFAULT_ANIMATION_STYLE_ID,
|
|
34
|
+
duration: defaults?.duration ?? DEFAULT_VIDEO_DURATION,
|
|
35
|
+
musicMood: defaults?.musicMood ?? DEFAULT_MUSIC_MOOD_ID,
|
|
36
|
+
customAudioUri: null,
|
|
37
|
+
motionPrompt: "",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useFormState(options?: UseFormStateOptions): UseFormStateReturn {
|
|
42
|
+
const { defaults } = options ?? {};
|
|
43
|
+
|
|
44
|
+
const [state, setState] = useState<ImageToVideoFormState>(() =>
|
|
45
|
+
createInitialState(defaults)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const setSelectedImages = useCallback((images: string[]) => {
|
|
49
|
+
setState((prev) => ({ ...prev, selectedImages: images }));
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const addImages = useCallback((images: string[]) => {
|
|
53
|
+
setState((prev) => ({
|
|
54
|
+
...prev,
|
|
55
|
+
selectedImages: [...prev.selectedImages, ...images],
|
|
56
|
+
}));
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const removeImage = useCallback((index: number) => {
|
|
60
|
+
setState((prev) => ({
|
|
61
|
+
...prev,
|
|
62
|
+
selectedImages: prev.selectedImages.filter((_, i) => i !== index),
|
|
63
|
+
}));
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const setAnimationStyle = useCallback((style: AnimationStyleId) => {
|
|
67
|
+
setState((prev) => ({ ...prev, animationStyle: style }));
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const setDuration = useCallback((duration: VideoDuration) => {
|
|
71
|
+
setState((prev) => ({ ...prev, duration }));
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const setMusicMood = useCallback((mood: MusicMoodId) => {
|
|
75
|
+
setState((prev) => ({ ...prev, musicMood: mood }));
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const setCustomAudioUri = useCallback((uri: string | null) => {
|
|
79
|
+
setState((prev) => ({ ...prev, customAudioUri: uri }));
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const setMotionPrompt = useCallback((prompt: string) => {
|
|
83
|
+
setState((prev) => ({ ...prev, motionPrompt: prompt }));
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const reset = useCallback(() => {
|
|
87
|
+
setState(createInitialState(defaults));
|
|
88
|
+
}, [defaults]);
|
|
89
|
+
|
|
90
|
+
const actions = useMemo<ImageToVideoFormActions>(
|
|
91
|
+
() => ({
|
|
92
|
+
setSelectedImages,
|
|
93
|
+
addImages,
|
|
94
|
+
removeImage,
|
|
95
|
+
setAnimationStyle,
|
|
96
|
+
setDuration,
|
|
97
|
+
setMusicMood,
|
|
98
|
+
setCustomAudioUri,
|
|
99
|
+
setMotionPrompt,
|
|
100
|
+
reset,
|
|
101
|
+
}),
|
|
102
|
+
[
|
|
103
|
+
setSelectedImages,
|
|
104
|
+
addImages,
|
|
105
|
+
removeImage,
|
|
106
|
+
setAnimationStyle,
|
|
107
|
+
setDuration,
|
|
108
|
+
setMusicMood,
|
|
109
|
+
setCustomAudioUri,
|
|
110
|
+
setMotionPrompt,
|
|
111
|
+
reset,
|
|
112
|
+
]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return { state, actions };
|
|
116
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation Hook for Image-to-Video
|
|
3
|
+
* Manages generation state and execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useMemo } from "react";
|
|
7
|
+
import type {
|
|
8
|
+
ImageToVideoFormState,
|
|
9
|
+
ImageToVideoGenerationState,
|
|
10
|
+
ImageToVideoCallbacks,
|
|
11
|
+
} from "../../domain/types";
|
|
12
|
+
|
|
13
|
+
export interface UseGenerationOptions {
|
|
14
|
+
formState: ImageToVideoFormState;
|
|
15
|
+
callbacks: ImageToVideoCallbacks;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseGenerationReturn {
|
|
19
|
+
generationState: ImageToVideoGenerationState;
|
|
20
|
+
handleGenerate: () => Promise<void>;
|
|
21
|
+
setProgress: (progress: number) => void;
|
|
22
|
+
setError: (error: string | null) => void;
|
|
23
|
+
isReady: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const INITIAL_GENERATION_STATE: ImageToVideoGenerationState = {
|
|
27
|
+
isGenerating: false,
|
|
28
|
+
progress: 0,
|
|
29
|
+
error: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function useGeneration(options: UseGenerationOptions): UseGenerationReturn {
|
|
33
|
+
const { formState, callbacks } = options;
|
|
34
|
+
|
|
35
|
+
const [generationState, setGenerationState] = useState<ImageToVideoGenerationState>(
|
|
36
|
+
INITIAL_GENERATION_STATE
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const setProgress = useCallback((progress: number) => {
|
|
40
|
+
setGenerationState((prev) => ({ ...prev, progress }));
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const setError = useCallback((error: string | null) => {
|
|
44
|
+
setGenerationState((prev) => ({ ...prev, error, isGenerating: false }));
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleGenerate = useCallback(async () => {
|
|
48
|
+
if (formState.selectedImages.length === 0) {
|
|
49
|
+
callbacks.onError?.("No images selected");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setGenerationState({
|
|
54
|
+
isGenerating: true,
|
|
55
|
+
progress: 0,
|
|
56
|
+
error: null,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await callbacks.onGenerate(formState);
|
|
61
|
+
setGenerationState((prev) => ({ ...prev, isGenerating: false, progress: 100 }));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
+
setGenerationState({
|
|
65
|
+
isGenerating: false,
|
|
66
|
+
progress: 0,
|
|
67
|
+
error: errorMessage,
|
|
68
|
+
});
|
|
69
|
+
callbacks.onError?.(errorMessage);
|
|
70
|
+
}
|
|
71
|
+
}, [formState, callbacks]);
|
|
72
|
+
|
|
73
|
+
const isReady = useMemo(
|
|
74
|
+
() => formState.selectedImages.length > 0 && !generationState.isGenerating,
|
|
75
|
+
[formState.selectedImages.length, generationState.isGenerating]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
generationState,
|
|
80
|
+
handleGenerate,
|
|
81
|
+
setProgress,
|
|
82
|
+
setError,
|
|
83
|
+
isReady,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGenerationExecution Hook
|
|
3
|
+
* Handles the core generation execution logic for image-to-video
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { executeImageToVideo } from "../../infrastructure/services";
|
|
8
|
+
import type {
|
|
9
|
+
ImageToVideoFeatureConfig,
|
|
10
|
+
ImageToVideoFeatureCallbacks,
|
|
11
|
+
ImageToVideoResult,
|
|
12
|
+
ImageToVideoGenerateParams,
|
|
13
|
+
ImageToVideoFeatureState,
|
|
14
|
+
} from "../../domain/types";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
interface UseGenerationExecutionParams {
|
|
19
|
+
userId: string;
|
|
20
|
+
config: ImageToVideoFeatureConfig;
|
|
21
|
+
callbacks?: ImageToVideoFeatureCallbacks;
|
|
22
|
+
setState: React.Dispatch<React.SetStateAction<ImageToVideoFeatureState>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useGenerationExecution({
|
|
26
|
+
userId,
|
|
27
|
+
config,
|
|
28
|
+
callbacks,
|
|
29
|
+
setState,
|
|
30
|
+
}: UseGenerationExecutionParams) {
|
|
31
|
+
return useCallback(
|
|
32
|
+
async (
|
|
33
|
+
imageUri: string,
|
|
34
|
+
motionPrompt: string,
|
|
35
|
+
options?: Omit<ImageToVideoGenerateParams, "imageUri" | "motionPrompt">,
|
|
36
|
+
): Promise<ImageToVideoResult> => {
|
|
37
|
+
const creationId = `image-to-video_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
38
|
+
|
|
39
|
+
setState((prev) => ({
|
|
40
|
+
...prev,
|
|
41
|
+
imageUri,
|
|
42
|
+
isProcessing: true,
|
|
43
|
+
progress: 0,
|
|
44
|
+
error: null,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
+
console.log("[ImageToVideoFeature] Starting generation, creationId:", creationId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
config.onProcessingStart?.();
|
|
52
|
+
|
|
53
|
+
if (callbacks?.onGenerationStart) {
|
|
54
|
+
callbacks.onGenerationStart({
|
|
55
|
+
creationId,
|
|
56
|
+
type: "image-to-video",
|
|
57
|
+
imageUri,
|
|
58
|
+
metadata: options as Record<string, unknown> | undefined,
|
|
59
|
+
}).catch((err) => {
|
|
60
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
+
console.warn("[ImageToVideoFeature] onGenerationStart failed:", err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const imageBase64 = await config.prepareImage(imageUri);
|
|
68
|
+
|
|
69
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
70
|
+
console.log("[ImageToVideoFeature] Image prepared, calling executeImageToVideo");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await executeImageToVideo(
|
|
74
|
+
{
|
|
75
|
+
imageUri,
|
|
76
|
+
imageBase64,
|
|
77
|
+
userId,
|
|
78
|
+
motionPrompt: motionPrompt || undefined,
|
|
79
|
+
options,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
model: config.model,
|
|
83
|
+
buildInput: config.buildInput,
|
|
84
|
+
extractResult: config.extractResult,
|
|
85
|
+
onProgress: (progress) => {
|
|
86
|
+
setState((prev) => ({ ...prev, progress }));
|
|
87
|
+
callbacks?.onProgress?.(progress);
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (result.success && result.videoUrl) {
|
|
93
|
+
setState((prev) => ({
|
|
94
|
+
...prev,
|
|
95
|
+
videoUrl: result.videoUrl ?? null,
|
|
96
|
+
thumbnailUrl: result.thumbnailUrl ?? null,
|
|
97
|
+
isProcessing: false,
|
|
98
|
+
progress: 100,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
if (callbacks?.onCreditDeduct && config.creditCost) {
|
|
102
|
+
await callbacks.onCreditDeduct(config.creditCost);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (callbacks?.onCreationSave) {
|
|
106
|
+
await callbacks.onCreationSave({
|
|
107
|
+
creationId,
|
|
108
|
+
type: "image-to-video",
|
|
109
|
+
videoUrl: result.videoUrl,
|
|
110
|
+
thumbnailUrl: result.thumbnailUrl,
|
|
111
|
+
imageUri,
|
|
112
|
+
metadata: options as Record<string, unknown> | undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
callbacks?.onGenerate?.(result);
|
|
117
|
+
} else {
|
|
118
|
+
const error = result.error || "Generation failed";
|
|
119
|
+
setState((prev) => ({ ...prev, isProcessing: false, error }));
|
|
120
|
+
config.onError?.(error);
|
|
121
|
+
callbacks?.onError?.(error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
config.onProcessingComplete?.(result);
|
|
125
|
+
return result;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
128
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
129
|
+
console.error("[ImageToVideoFeature] Generation error:", errorMessage);
|
|
130
|
+
}
|
|
131
|
+
setState((prev) => ({
|
|
132
|
+
...prev,
|
|
133
|
+
isProcessing: false,
|
|
134
|
+
error: errorMessage,
|
|
135
|
+
}));
|
|
136
|
+
config.onError?.(errorMessage);
|
|
137
|
+
callbacks?.onError?.(errorMessage);
|
|
138
|
+
return { success: false, error: errorMessage };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
[userId, config, callbacks],
|
|
142
|
+
);
|
|
143
|
+
}
|