@umituz/react-native-ai-generation-content 1.27.13 → 1.27.15

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.27.13",
3
+ "version": "1.27.15",
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",
@@ -38,8 +38,7 @@
38
38
  "url": "git+https://github.com/umituz/react-native-ai-generation-content.git"
39
39
  },
40
40
  "dependencies": {
41
- "@umituz/react-native-auth": "*",
42
- "@umituz/react-native-firebase": "*"
41
+ "@umituz/react-native-auth": "^3.6.25"
43
42
  },
44
43
  "peerDependencies": {
45
44
  "@react-navigation/native": ">=6.0.0",
@@ -68,7 +67,7 @@
68
67
  "@typescript-eslint/eslint-plugin": "^8.0.0",
69
68
  "@typescript-eslint/parser": "^8.0.0",
70
69
  "@umituz/react-native-design-system": "^2.9.44",
71
- "@umituz/react-native-firebase": "*",
70
+ "@umituz/react-native-firebase": "^1.13.87",
72
71
  "@umituz/react-native-localization": "*",
73
72
  "@umituz/react-native-subscription": "*",
74
73
  "eslint": "^9.0.0",
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Image to Video Wizard Config
3
+ * Config-driven wizard steps for image-to-video generation
4
+ */
5
+
6
+ import type { WizardFeatureConfig } from "../domain/entities/wizard-config.types";
7
+
8
+ export const IMAGE_TO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
9
+ id: "image-to-video",
10
+ name: "Image to Video",
11
+ steps: [
12
+ {
13
+ id: "photo_1",
14
+ type: "photo_upload",
15
+ label: "Your Photo",
16
+ showFaceDetection: false,
17
+ showPhotoTips: true,
18
+ required: true,
19
+ },
20
+ {
21
+ id: "motion_prompt",
22
+ type: "text_input",
23
+ required: false,
24
+ placeholderKey: "imageToVideo.motionPromptPlaceholder",
25
+ maxLength: 200,
26
+ },
27
+ {
28
+ id: "duration",
29
+ type: "selection",
30
+ selectionType: "duration",
31
+ options: [
32
+ { id: "5s", label: "5 seconds", value: 5 },
33
+ { id: "10s", label: "10 seconds", value: 10 },
34
+ ],
35
+ required: true,
36
+ defaultValue: "5s",
37
+ },
38
+ ],
39
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wizard Feature Configs
3
+ * Pre-built configs for common generation features
4
+ */
5
+
6
+ export { TEXT_TO_IMAGE_WIZARD_CONFIG } from "./text-to-image.config";
7
+ export { TEXT_TO_VIDEO_WIZARD_CONFIG } from "./text-to-video.config";
8
+ export { IMAGE_TO_VIDEO_WIZARD_CONFIG } from "./image-to-video.config";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Text to Image Wizard Config
3
+ * Config-driven wizard steps for text-to-image generation
4
+ */
5
+
6
+ import type { WizardFeatureConfig } from "../domain/entities/wizard-config.types";
7
+
8
+ export const TEXT_TO_IMAGE_WIZARD_CONFIG: WizardFeatureConfig = {
9
+ id: "text-to-image",
10
+ name: "Text to Image",
11
+ steps: [
12
+ {
13
+ id: "prompt",
14
+ type: "text_input",
15
+ required: true,
16
+ placeholderKey: "textToImage.promptPlaceholder",
17
+ minLength: 3,
18
+ maxLength: 1000,
19
+ multiline: true,
20
+ },
21
+ {
22
+ id: "style",
23
+ type: "selection",
24
+ selectionType: "style",
25
+ options: [],
26
+ required: false,
27
+ },
28
+ ],
29
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Text to Video Wizard Config
3
+ * Config-driven wizard steps for text-to-video generation
4
+ */
5
+
6
+ import type { WizardFeatureConfig } from "../domain/entities/wizard-config.types";
7
+
8
+ export const TEXT_TO_VIDEO_WIZARD_CONFIG: WizardFeatureConfig = {
9
+ id: "text-to-video",
10
+ name: "Text to Video",
11
+ steps: [
12
+ {
13
+ id: "prompt",
14
+ type: "text_input",
15
+ required: true,
16
+ placeholderKey: "textToVideo.promptPlaceholder",
17
+ minLength: 3,
18
+ maxLength: 500,
19
+ multiline: true,
20
+ },
21
+ {
22
+ id: "duration",
23
+ type: "selection",
24
+ selectionType: "duration",
25
+ options: [
26
+ { id: "5s", label: "5 seconds", value: 5 },
27
+ { id: "10s", label: "10 seconds", value: 10 },
28
+ ],
29
+ required: true,
30
+ defaultValue: "5s",
31
+ },
32
+ ],
33
+ };
@@ -51,3 +51,6 @@ export type { GenericWizardFlowProps } from "./presentation/components";
51
51
 
52
52
  // Presentation - Screens
53
53
  export { GeneratingScreen } from "./presentation/screens";
54
+
55
+ // Feature Configs
56
+ export * from "./configs";
@@ -24,10 +24,13 @@ declare const __DEV__: boolean;
24
24
  // ============================================================================
25
25
 
26
26
  export interface ImageGenerationInput {
27
+ /** Photos are optional for text-to-image */
27
28
  readonly photos: readonly string[];
28
29
  readonly prompt: string;
29
30
  /** Optional interaction style for multi-person images */
30
31
  readonly interactionStyle?: InteractionStyle;
32
+ /** Optional style from wizard selection */
33
+ readonly style?: string;
31
34
  }
32
35
 
33
36
  export interface ImageGenerationResult {
@@ -40,29 +43,29 @@ export interface ImageGenerationResult {
40
43
 
41
44
  async function extractPhotosFromWizardData(
42
45
  wizardData: Record<string, unknown>,
43
- ): Promise<string[] | null> {
46
+ ): Promise<string[]> {
44
47
  const photoKeys = Object.keys(wizardData)
45
48
  .filter((k) => k.includes(PHOTO_KEY_PREFIX))
46
49
  .sort();
47
50
 
48
51
  if (photoKeys.length === 0) {
49
- if (typeof __DEV__ !== "undefined" && __DEV__) {
50
- console.error("[ImageStrategy] No photos found", { keys: Object.keys(wizardData) });
51
- }
52
- return null;
52
+ return [];
53
53
  }
54
54
 
55
55
  const photoUris: string[] = [];
56
56
  for (const key of photoKeys) {
57
57
  const photo = wizardData[key] as { uri?: string };
58
- if (!photo?.uri) return null;
59
- photoUris.push(photo.uri);
58
+ if (photo?.uri) {
59
+ photoUris.push(photo.uri);
60
+ }
60
61
  }
61
62
 
62
- const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
63
- const validPhotos = photosBase64.filter(Boolean) as string[];
63
+ if (photoUris.length === 0) {
64
+ return [];
65
+ }
64
66
 
65
- return validPhotos.length > 0 ? validPhotos : null;
67
+ const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
68
+ return photosBase64.filter(Boolean) as string[];
66
69
  }
67
70
 
68
71
  // ============================================================================
@@ -85,43 +88,53 @@ async function executeImageGeneration(
85
88
  const formatBase64 = (base64: string): string =>
86
89
  base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
87
90
 
88
- const imageUrls = input.photos.map(formatBase64);
89
- if (imageUrls.length === 0) {
90
- return { success: false, error: "At least one image required" };
91
- }
91
+ const hasPhotos = input.photos.length > 0;
92
+ const imageUrls = hasPhotos ? input.photos.map(formatBase64) : [];
92
93
 
93
- // Build face preservation prompt dynamically based on number of people
94
- const facePrompt = buildFacePreservationPrompt({
95
- scenarioPrompt: input.prompt,
96
- personCount: imageUrls.length,
97
- });
94
+ let finalPrompt = input.prompt;
98
95
 
99
- // Build interaction style prompt for multi-person images
100
- const interactionPrompt = buildInteractionStylePrompt({
101
- style: input.interactionStyle ?? "romantic",
102
- personCount: imageUrls.length,
103
- });
104
-
105
- // Combine prompts: face preservation + interaction style
106
- const enhancedPrompt = interactionPrompt
107
- ? `${facePrompt}\n\n${interactionPrompt}`
108
- : facePrompt;
96
+ if (hasPhotos) {
97
+ // Photo-based: Build face preservation prompt
98
+ const facePrompt = buildFacePreservationPrompt({
99
+ scenarioPrompt: input.prompt,
100
+ personCount: imageUrls.length,
101
+ });
109
102
 
110
- if (typeof __DEV__ !== "undefined" && __DEV__) {
111
- console.log("[ImageStrategy] Prompt built for", imageUrls.length, "person(s)", {
112
- interactionStyle: input.interactionStyle ?? "romantic",
103
+ const interactionPrompt = buildInteractionStylePrompt({
104
+ style: input.interactionStyle ?? "romantic",
105
+ personCount: imageUrls.length,
113
106
  });
107
+
108
+ finalPrompt = interactionPrompt
109
+ ? `${facePrompt}\n\n${interactionPrompt}`
110
+ : facePrompt;
111
+
112
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
113
+ console.log("[ImageStrategy] Photo-based generation for", imageUrls.length, "person(s)");
114
+ }
115
+ } else {
116
+ // Text-to-image: Use prompt with optional style
117
+ if (input.style && input.style !== DEFAULT_STYLE_VALUE) {
118
+ finalPrompt = `${input.prompt}. Style: ${input.style}`;
119
+ }
120
+
121
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
122
+ console.log("[ImageStrategy] Text-to-image generation");
123
+ }
114
124
  }
115
125
 
116
- const modelInput = {
117
- image_urls: imageUrls,
118
- prompt: enhancedPrompt,
126
+ const modelInput: Record<string, unknown> = {
127
+ prompt: finalPrompt,
119
128
  aspect_ratio: MODEL_INPUT_DEFAULTS.aspectRatio,
120
129
  output_format: MODEL_INPUT_DEFAULTS.outputFormat,
121
130
  num_images: MODEL_INPUT_DEFAULTS.numImages,
122
131
  enable_safety_checker: MODEL_INPUT_DEFAULTS.enableSafetyChecker,
123
132
  };
124
133
 
134
+ if (hasPhotos) {
135
+ modelInput.image_urls = imageUrls;
136
+ }
137
+
125
138
  let lastStatus = "";
126
139
  const result = await provider.subscribe(model, modelInput, {
127
140
  timeoutMs: GENERATION_TIMEOUT_MS,
@@ -151,39 +164,47 @@ export async function buildImageInput(
151
164
  scenario: WizardScenarioData,
152
165
  ): Promise<ImageGenerationInput | null> {
153
166
  const photos = await extractPhotosFromWizardData(wizardData);
154
- if (!photos) return null;
155
167
 
156
- if (!scenario.aiPrompt?.trim()) {
157
- throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
168
+ // Get prompt from wizardData (text_input step) OR scenario.aiPrompt
169
+ const userPrompt = wizardData.prompt as string | undefined;
170
+ const prompt = userPrompt?.trim() || scenario.aiPrompt?.trim();
171
+
172
+ if (!prompt) {
173
+ throw new Error("Prompt is required for image generation");
158
174
  }
159
175
 
160
- let prompt = scenario.aiPrompt;
176
+ // For photo-based generation, apply style enhancements
177
+ let finalPrompt = prompt;
178
+ if (photos.length > 0) {
179
+ const styleEnhancements: string[] = [];
161
180
 
162
- const styleEnhancements: string[] = [];
181
+ const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
182
+ if (romanticMoods?.length) {
183
+ styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
184
+ }
163
185
 
164
- const romanticMoods = wizardData.selection_romantic_mood as string[] | undefined;
165
- if (romanticMoods?.length) {
166
- styleEnhancements.push(`Mood: ${romanticMoods.join(", ")}`);
167
- }
186
+ const artStyle = wizardData.selection_art_style as string | undefined;
187
+ if (artStyle && artStyle !== DEFAULT_STYLE_VALUE) {
188
+ styleEnhancements.push(`Art style: ${artStyle}`);
189
+ }
168
190
 
169
- const artStyle = wizardData.selection_art_style as string | undefined;
170
- if (artStyle && artStyle !== DEFAULT_STYLE_VALUE) {
171
- styleEnhancements.push(`Art style: ${artStyle}`);
172
- }
191
+ const artist = wizardData.selection_artist_style as string | undefined;
192
+ if (artist && artist !== DEFAULT_STYLE_VALUE) {
193
+ styleEnhancements.push(`Artist style: ${artist}`);
194
+ }
173
195
 
174
- const artist = wizardData.selection_artist_style as string | undefined;
175
- if (artist && artist !== DEFAULT_STYLE_VALUE) {
176
- styleEnhancements.push(`Artist style: ${artist}`);
196
+ if (styleEnhancements.length > 0) {
197
+ finalPrompt = `${prompt}. ${styleEnhancements.join(", ")}`;
198
+ }
177
199
  }
178
200
 
179
- if (styleEnhancements.length > 0) {
180
- prompt = `${prompt}. ${styleEnhancements.join(", ")}`;
181
- }
201
+ // Get style from wizard selection (for text-to-image)
202
+ const style = wizardData.style as string | undefined;
182
203
 
183
204
  // Get interaction style from scenario (default: romantic for couple apps)
184
205
  const interactionStyle = (scenario.interactionStyle as InteractionStyle) ?? "romantic";
185
206
 
186
- return { photos, prompt, interactionStyle };
207
+ return { photos, prompt: finalPrompt, style, interactionStyle };
187
208
  }
188
209
 
189
210
  // ============================================================================
@@ -11,16 +11,18 @@ import type { WizardScenarioData } from "../../presentation/hooks/useWizardGener
11
11
  import type { WizardStrategy } from "./wizard-strategy.types";
12
12
  import { PHOTO_KEY_PREFIX, VIDEO_FEATURE_PATTERNS } from "./wizard-strategy.constants";
13
13
 
14
- declare const __DEV__: boolean;
15
-
16
14
  // ============================================================================
17
15
  // Types
18
16
  // ============================================================================
19
17
 
20
18
  export interface VideoGenerationInput {
21
- readonly sourceImageBase64: string;
22
- readonly targetImageBase64: string;
19
+ /** Source image (optional for text-to-video) */
20
+ readonly sourceImageBase64?: string;
21
+ /** Target image (optional, uses source if not provided) */
22
+ readonly targetImageBase64?: string;
23
23
  readonly prompt: string;
24
+ /** Video duration in seconds */
25
+ readonly duration?: number;
24
26
  }
25
27
 
26
28
  export interface VideoGenerationResult {
@@ -33,29 +35,29 @@ export interface VideoGenerationResult {
33
35
 
34
36
  async function extractPhotosFromWizardData(
35
37
  wizardData: Record<string, unknown>,
36
- ): Promise<string[] | null> {
38
+ ): Promise<string[]> {
37
39
  const photoKeys = Object.keys(wizardData)
38
40
  .filter((k) => k.includes(PHOTO_KEY_PREFIX))
39
41
  .sort();
40
42
 
41
43
  if (photoKeys.length === 0) {
42
- if (typeof __DEV__ !== "undefined" && __DEV__) {
43
- console.error("[VideoStrategy] No photos found", { keys: Object.keys(wizardData) });
44
- }
45
- return null;
44
+ return [];
46
45
  }
47
46
 
48
47
  const photoUris: string[] = [];
49
48
  for (const key of photoKeys) {
50
49
  const photo = wizardData[key] as { uri?: string };
51
- if (!photo?.uri) return null;
52
- photoUris.push(photo.uri);
50
+ if (photo?.uri) {
51
+ photoUris.push(photo.uri);
52
+ }
53
53
  }
54
54
 
55
- const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
56
- const validPhotos = photosBase64.filter(Boolean) as string[];
55
+ if (photoUris.length === 0) {
56
+ return [];
57
+ }
57
58
 
58
- return validPhotos.length > 0 ? validPhotos : null;
59
+ const photosBase64 = await Promise.all(photoUris.map((uri) => readFileAsBase64(uri)));
60
+ return photosBase64.filter(Boolean) as string[];
59
61
  }
60
62
 
61
63
  // ============================================================================
@@ -83,16 +85,24 @@ export async function buildVideoInput(
83
85
  scenario: WizardScenarioData,
84
86
  ): Promise<VideoGenerationInput | null> {
85
87
  const photos = await extractPhotosFromWizardData(wizardData);
86
- if (!photos || photos.length < 1) return null;
87
88
 
88
- if (!scenario.aiPrompt?.trim()) {
89
- throw new Error(`Scenario "${scenario.id}" must have aiPrompt field`);
89
+ // Get prompt from wizardData or scenario
90
+ const userPrompt = wizardData.prompt as string | undefined;
91
+ const motionPrompt = wizardData.motion_prompt as string | undefined;
92
+ const prompt = userPrompt?.trim() || motionPrompt?.trim() || scenario.aiPrompt?.trim();
93
+
94
+ if (!prompt) {
95
+ throw new Error("Prompt is required for video generation");
90
96
  }
91
97
 
98
+ // Get duration from wizardData
99
+ const duration = wizardData.duration as number | undefined;
100
+
92
101
  return {
93
102
  sourceImageBase64: photos[0],
94
103
  targetImageBase64: photos[1] || photos[0],
95
- prompt: scenario.aiPrompt,
104
+ prompt,
105
+ duration,
96
106
  };
97
107
  }
98
108
 
@@ -120,8 +130,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
120
130
  const result = await executeVideoFeature(
121
131
  videoFeatureType,
122
132
  {
123
- sourceImageBase64: videoInput.sourceImageBase64,
124
- targetImageBase64: videoInput.targetImageBase64,
133
+ sourceImageBase64: videoInput.sourceImageBase64 || "",
134
+ targetImageBase64: videoInput.targetImageBase64 || videoInput.sourceImageBase64 || "",
125
135
  prompt: videoInput.prompt,
126
136
  },
127
137
  { onProgress },
@@ -29,4 +29,6 @@ export const MODEL_INPUT_DEFAULTS = {
29
29
  export const VIDEO_FEATURE_PATTERNS: Record<string, VideoFeatureType> = {
30
30
  kiss: "ai-kiss",
31
31
  hug: "ai-hug",
32
+ "text-to-video": "text-to-video",
33
+ "image-to-video": "image-to-video",
32
34
  };
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * Image-to-Video Feature Hook
3
- * Provider-agnostic hook with callbacks integration
3
+ * Uses centralized useGenerationOrchestrator for consistent auth, credits, and error handling
4
4
  */
5
5
 
6
6
  import { useState, useCallback, useMemo } from "react";
7
- import { useGenerationExecution } from "./useGenerationExecution";
8
- import { validateImageToVideoGeneration } from "./useImageToVideoValidation";
7
+ import {
8
+ useGenerationOrchestrator,
9
+ type GenerationStrategy,
10
+ type AlertMessages,
11
+ } from "../../../../presentation/hooks/generation";
12
+ import { executeImageToVideo } from "../../infrastructure/services";
9
13
  import type {
10
14
  ImageToVideoFeatureState,
11
15
  ImageToVideoFeatureConfig,
@@ -16,7 +20,6 @@ import type {
16
20
 
17
21
  declare const __DEV__: boolean;
18
22
 
19
- // Initial state (inlined from constants file)
20
23
  const INITIAL_STATE: ImageToVideoFeatureState = {
21
24
  imageUri: null,
22
25
  motionPrompt: "",
@@ -27,7 +30,14 @@ const INITIAL_STATE: ImageToVideoFeatureState = {
27
30
  error: null,
28
31
  };
29
32
 
30
- // Types (inlined from types file)
33
+ const DEFAULT_ALERT_MESSAGES: AlertMessages = {
34
+ networkError: "No internet connection. Please check your network.",
35
+ policyViolation: "Content not allowed. Please try again.",
36
+ saveFailed: "Failed to save. Please try again.",
37
+ creditFailed: "Credit operation failed. Please try again.",
38
+ unknown: "An error occurred. Please try again.",
39
+ };
40
+
31
41
  export interface UseImageToVideoFeatureProps {
32
42
  config: ImageToVideoFeatureConfig;
33
43
  callbacks?: ImageToVideoFeatureCallbacks;
@@ -44,15 +54,99 @@ export interface UseImageToVideoFeatureReturn {
44
54
  canGenerate: boolean;
45
55
  }
46
56
 
57
+ interface VideoGenerationInput {
58
+ imageUri: string;
59
+ imageBase64: string;
60
+ motionPrompt: string;
61
+ options?: Omit<ImageToVideoGenerateParams, "imageUri" | "motionPrompt">;
62
+ creationId: string;
63
+ }
64
+
65
+ function generateCreationId(): string {
66
+ return `image-to-video_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
67
+ }
68
+
47
69
  export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseImageToVideoFeatureReturn {
48
70
  const { config, callbacks, userId } = props;
49
71
  const [state, setState] = useState<ImageToVideoFeatureState>(INITIAL_STATE);
50
72
 
51
- const executeGeneration = useGenerationExecution({
73
+ const strategy: GenerationStrategy<VideoGenerationInput, ImageToVideoResult> = useMemo(
74
+ () => ({
75
+ execute: async (input, onProgress) => {
76
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
77
+ console.log("[ImageToVideo] Executing generation, creationId:", input.creationId);
78
+ }
79
+
80
+ config.onProcessingStart?.();
81
+
82
+ callbacks?.onGenerationStart?.({
83
+ creationId: input.creationId,
84
+ type: "image-to-video",
85
+ imageUri: input.imageUri,
86
+ metadata: input.options as Record<string, unknown> | undefined,
87
+ }).catch(() => {});
88
+
89
+ const result = await executeImageToVideo(
90
+ {
91
+ imageUri: input.imageUri,
92
+ imageBase64: input.imageBase64,
93
+ userId,
94
+ motionPrompt: input.motionPrompt || undefined,
95
+ options: input.options,
96
+ },
97
+ {
98
+ model: config.model,
99
+ buildInput: config.buildInput,
100
+ extractResult: config.extractResult,
101
+ onProgress: (progress) => {
102
+ setState((prev) => ({ ...prev, progress }));
103
+ onProgress?.(progress);
104
+ callbacks?.onProgress?.(progress);
105
+ },
106
+ },
107
+ );
108
+
109
+ if (!result.success || !result.videoUrl) {
110
+ throw new Error(result.error || "Generation failed");
111
+ }
112
+
113
+ setState((prev) => ({
114
+ ...prev,
115
+ videoUrl: result.videoUrl ?? null,
116
+ thumbnailUrl: result.thumbnailUrl ?? null,
117
+ }));
118
+
119
+ return result;
120
+ },
121
+ getCreditCost: () => config.creditCost ?? 0,
122
+ save: async (result) => {
123
+ if (result.success && result.videoUrl && state.imageUri) {
124
+ await callbacks?.onCreationSave?.({
125
+ creationId: generateCreationId(),
126
+ type: "image-to-video",
127
+ videoUrl: result.videoUrl,
128
+ thumbnailUrl: result.thumbnailUrl,
129
+ imageUri: state.imageUri,
130
+ });
131
+ }
132
+ },
133
+ }),
134
+ [config, callbacks, userId, state.imageUri],
135
+ );
136
+
137
+ const orchestrator = useGenerationOrchestrator(strategy, {
52
138
  userId,
53
- config,
54
- callbacks,
55
- setState,
139
+ alertMessages: DEFAULT_ALERT_MESSAGES,
140
+ onCreditsExhausted: () => callbacks?.onShowPaywall?.(config.creditCost ?? 0),
141
+ onSuccess: (result) => {
142
+ const videoResult = result as ImageToVideoResult;
143
+ callbacks?.onGenerate?.(videoResult);
144
+ config.onProcessingComplete?.(videoResult);
145
+ },
146
+ onError: (err) => {
147
+ callbacks?.onError?.(err.message);
148
+ config.onError?.(err.message);
149
+ },
56
150
  });
57
151
 
58
152
  const setImageUri = useCallback(
@@ -77,23 +171,40 @@ export function useImageToVideoFeature(props: UseImageToVideoFeatureProps): UseI
77
171
  console.log("[ImageToVideoFeature] generate called, hasImage:", !!effectiveImageUri);
78
172
  }
79
173
 
174
+ if (!effectiveImageUri) {
175
+ const error = "Image is required";
176
+ setState((prev) => ({ ...prev, error }));
177
+ callbacks?.onError?.(error);
178
+ return { success: false, error };
179
+ }
180
+
80
181
  if (paramImageUri) {
81
182
  setState((prev) => ({ ...prev, imageUri: paramImageUri }));
82
183
  }
83
184
 
84
- const validation = await validateImageToVideoGeneration(
85
- effectiveImageUri,
86
- callbacks,
87
- config.creditCost,
88
- );
89
-
90
- if (!validation.shouldProceed) {
91
- return validation;
185
+ setState((prev) => ({ ...prev, isProcessing: true, error: null, progress: 0 }));
186
+
187
+ try {
188
+ const imageBase64 = await config.prepareImage(effectiveImageUri);
189
+
190
+ const input: VideoGenerationInput = {
191
+ imageUri: effectiveImageUri,
192
+ imageBase64,
193
+ motionPrompt: effectiveMotionPrompt,
194
+ options,
195
+ creationId: generateCreationId(),
196
+ };
197
+
198
+ await orchestrator.generate(input);
199
+ setState((prev) => ({ ...prev, isProcessing: false }));
200
+ return { success: true, videoUrl: state.videoUrl || undefined };
201
+ } catch (error) {
202
+ const message = error instanceof Error ? error.message : "Generation failed";
203
+ setState((prev) => ({ ...prev, isProcessing: false, error: message }));
204
+ return { success: false, error: message };
92
205
  }
93
-
94
- return executeGeneration(effectiveImageUri!, effectiveMotionPrompt, options);
95
206
  },
96
- [state.imageUri, state.motionPrompt, callbacks, config.creditCost, executeGeneration],
207
+ [state.imageUri, state.motionPrompt, state.videoUrl, config, callbacks, orchestrator],
97
208
  );
98
209
 
99
210
  const reset = useCallback(() => {
@@ -83,13 +83,6 @@ export type {
83
83
  UseTextToImageFormReturn,
84
84
  } from "./presentation";
85
85
 
86
- // Provider-based Feature Hook
87
- export { useTextToImageFeature } from "./presentation";
88
- export type {
89
- UseTextToImageFeatureProps,
90
- UseTextToImageFeatureReturn,
91
- } from "./presentation";
92
-
93
86
  // =============================================================================
94
87
  // PRESENTATION LAYER - Components
95
88
  // =============================================================================
@@ -21,10 +21,3 @@ export type {
21
21
  UseTextToImageFormOptions,
22
22
  UseTextToImageFormReturn,
23
23
  } from "./useTextToImageForm";
24
-
25
- // Provider-based Feature Hook
26
- export { useTextToImageFeature } from "./useTextToImageFeature";
27
- export type {
28
- UseTextToImageFeatureProps,
29
- UseTextToImageFeatureReturn,
30
- } from "./useTextToImageFeature";
@@ -3,14 +3,9 @@
3
3
  * Uses ONLY configured app services - no alternatives
4
4
  */
5
5
 
6
- import * as FileSystem from "expo-file-system";
6
+ import { readFileAsBase64 } from "@umituz/react-native-design-system";
7
7
  import { getAuthService, getCreditService, getPaywallService, isAppServicesConfigured } from "../config/app-services.config";
8
8
 
9
- async function readFileAsBase64(uri: string): Promise<string> {
10
- const base64 = await FileSystem.readAsStringAsync(uri, { encoding: "base64" });
11
- return `data:image/jpeg;base64,${base64}`;
12
- }
13
-
14
9
  declare const __DEV__: boolean;
15
10
 
16
11
  export type ImageSelector = () => Promise<string | null>;
@@ -1,143 +0,0 @@
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
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * Image-to-Video Validation Utilities
3
- */
4
-
5
- import type { ImageToVideoFeatureCallbacks, ImageToVideoResult } from "../../domain/types";
6
-
7
- declare const __DEV__: boolean;
8
-
9
- export interface ValidationResult extends ImageToVideoResult {
10
- shouldProceed: boolean;
11
- }
12
-
13
- export async function validateImageToVideoGeneration(
14
- effectiveImageUri: string | null,
15
- callbacks?: ImageToVideoFeatureCallbacks,
16
- creditCost?: number,
17
- ): Promise<ValidationResult> {
18
- if (!effectiveImageUri) {
19
- const error = "Image is required";
20
- if (typeof __DEV__ !== "undefined" && __DEV__) {
21
- console.log("[ImageToVideoFeature] Generate failed: Image is required");
22
- }
23
- callbacks?.onError?.(error);
24
- return { success: false, error, shouldProceed: false };
25
- }
26
-
27
- if (callbacks?.onAuthCheck && !callbacks.onAuthCheck()) {
28
- if (typeof __DEV__ !== "undefined" && __DEV__) {
29
- console.log("[ImageToVideoFeature] Generate failed: Authentication required");
30
- }
31
- return { success: false, error: "Authentication required", shouldProceed: false };
32
- }
33
-
34
- if (callbacks?.onCreditCheck && creditCost) {
35
- const hasCredits = await callbacks.onCreditCheck(creditCost);
36
- if (!hasCredits) {
37
- callbacks?.onShowPaywall?.(creditCost);
38
- if (typeof __DEV__ !== "undefined" && __DEV__) {
39
- console.log("[ImageToVideoFeature] Generate failed: Insufficient credits");
40
- }
41
- return { success: false, error: "Insufficient credits", shouldProceed: false };
42
- }
43
- }
44
-
45
- return { success: true, shouldProceed: true };
46
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * Text-to-Image Feature Hook
3
- * Provider-agnostic hook for text-to-image generation
4
- */
5
-
6
- import { useState, useCallback } from "react";
7
- import { executeTextToImage } from "../../infrastructure/services";
8
- import type {
9
- TextToImageFeatureState,
10
- TextToImageFeatureConfig,
11
- TextToImageResult,
12
- TextToImageOptions,
13
- } from "../../domain/types";
14
-
15
- export interface UseTextToImageFeatureProps {
16
- config: TextToImageFeatureConfig;
17
- userId: string;
18
- }
19
-
20
- export interface UseTextToImageFeatureReturn {
21
- state: TextToImageFeatureState;
22
- setPrompt: (prompt: string) => void;
23
- generate: (options?: TextToImageOptions) => Promise<TextToImageResult>;
24
- reset: () => void;
25
- isReady: boolean;
26
- }
27
-
28
- const initialState: TextToImageFeatureState = {
29
- prompt: "",
30
- imageUrl: null,
31
- imageUrls: [],
32
- isProcessing: false,
33
- progress: 0,
34
- error: null,
35
- };
36
-
37
- export function useTextToImageFeature(
38
- props: UseTextToImageFeatureProps,
39
- ): UseTextToImageFeatureReturn {
40
- const { config, userId } = props;
41
- const [state, setState] = useState<TextToImageFeatureState>(initialState);
42
-
43
- const setPrompt = useCallback(
44
- (prompt: string) => {
45
- setState((prev) => ({ ...prev, prompt, error: null }));
46
- config.onPromptChange?.(prompt);
47
- },
48
- [config],
49
- );
50
-
51
- const generate = useCallback(
52
- async (options?: TextToImageOptions): Promise<TextToImageResult> => {
53
- if (!state.prompt) {
54
- const error = "Prompt is required";
55
- setState((prev) => ({ ...prev, error }));
56
- return { success: false, error };
57
- }
58
-
59
- setState((prev) => ({
60
- ...prev,
61
- isProcessing: true,
62
- progress: 0,
63
- error: null,
64
- }));
65
-
66
- config.onProcessingStart?.();
67
-
68
- const result = await executeTextToImage(
69
- { prompt: state.prompt, userId, options },
70
- {
71
- model: config.model,
72
- buildInput: config.buildInput,
73
- extractResult: config.extractResult,
74
- onProgress: (progress) => {
75
- setState((prev) => ({ ...prev, progress }));
76
- },
77
- },
78
- );
79
-
80
- if (result.success && result.imageUrl) {
81
- setState((prev) => ({
82
- ...prev,
83
- imageUrl: result.imageUrl ?? null,
84
- imageUrls: result.imageUrls ?? [],
85
- isProcessing: false,
86
- progress: 100,
87
- }));
88
- } else {
89
- const error = result.error || "Generation failed";
90
- setState((prev) => ({
91
- ...prev,
92
- isProcessing: false,
93
- error,
94
- }));
95
- config.onError?.(error);
96
- }
97
-
98
- config.onProcessingComplete?.(result);
99
- return result;
100
- },
101
- [state.prompt, userId, config],
102
- );
103
-
104
- const reset = useCallback(() => {
105
- setState(initialState);
106
- }, []);
107
-
108
- const isReady = state.prompt.length > 0 && !state.isProcessing;
109
-
110
- return { state, setPrompt, generate, reset, isReady };
111
- }