@umituz/react-native-ai-generation-content 1.90.1 → 1.90.3

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.
Files changed (152) hide show
  1. package/package.json +1 -4
  2. package/src/domain/interfaces/app-services-auth.interface.ts +27 -0
  3. package/src/domain/interfaces/app-services-composite.interface.ts +29 -0
  4. package/src/domain/interfaces/app-services-optional.interface.ts +42 -0
  5. package/src/domain/interfaces/app-services.interface.ts +0 -79
  6. package/src/domains/background/infrastructure/services/job-poller-index.ts +7 -0
  7. package/src/domains/background/infrastructure/services/job-poller-utils.ts +130 -0
  8. package/src/domains/background/infrastructure/utils/polling-interval.util.ts +1 -1
  9. package/src/domains/background/presentation/hooks/use-background-generation.ts +1 -1
  10. package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts +1 -1
  11. package/src/domains/content-moderation/infrastructure/services/moderators/image.moderator.ts +34 -8
  12. package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +15 -4
  13. package/src/domains/content-moderation/infrastructure/services/moderators/video.moderator.ts +34 -8
  14. package/src/domains/content-moderation/infrastructure/services/moderators/voice.moderator.ts +19 -8
  15. package/src/domains/creations/domain/types/creation-categories.constants.ts +57 -0
  16. package/src/domains/creations/domain/types/creation-categories.helpers.ts +67 -0
  17. package/src/domains/creations/domain/types/creation-categories.ts +5 -111
  18. package/src/domains/creations/presentation/hooks/creation-validators.ts +31 -29
  19. package/src/domains/creations/presentation/hooks/job-poller-index.ts +10 -0
  20. package/src/domains/creations/presentation/hooks/job-poller-utils.filters.ts +34 -0
  21. package/src/domains/creations/presentation/hooks/job-poller-utils.logger.ts +76 -0
  22. package/src/domains/creations/presentation/hooks/job-poller-utils.stale-handlers.ts +52 -0
  23. package/src/domains/creations/presentation/hooks/job-poller-utils.ts +8 -0
  24. package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +1 -1
  25. package/src/domains/creations/presentation-exports.ts +2 -2
  26. package/src/domains/face-detection/domain/entities/FaceDetection.ts +4 -3
  27. package/src/domains/face-detection/presentation/hooks/useFaceDetection.ts +24 -21
  28. package/src/domains/generation/infrastructure/couple-generation-builder/builder-couple-preparation.ts +58 -0
  29. package/src/domains/generation/infrastructure/couple-generation-builder/builder-couple-prompt.ts +69 -0
  30. package/src/domains/generation/infrastructure/couple-generation-builder/builder-couple-resolution.ts +77 -0
  31. package/src/domains/generation/infrastructure/couple-generation-builder/builder-couple.ts +54 -0
  32. package/src/domains/generation/infrastructure/couple-generation-builder/builder-index.ts +8 -0
  33. package/src/domains/generation/infrastructure/couple-generation-builder/builder-scenario.ts +113 -0
  34. package/src/domains/generation/infrastructure/couple-generation-builder/builder.ts +7 -0
  35. package/src/domains/generation/infrastructure/couple-generation-builder/index.ts +20 -0
  36. package/src/domains/generation/infrastructure/couple-generation-builder/types.ts +44 -0
  37. package/src/domains/generation/infrastructure/couple-generation-builder/utils/builder-end-logger.ts +18 -0
  38. package/src/domains/generation/infrastructure/couple-generation-builder/utils/builder-start-logger.ts +57 -0
  39. package/src/domains/generation/infrastructure/couple-generation-builder/utils/builder-step-logger.ts +106 -0
  40. package/src/domains/generation/infrastructure/couple-generation-builder/utils/index.ts +8 -0
  41. package/src/domains/generation/infrastructure/couple-generation-builder/utils/types.ts +49 -0
  42. package/src/domains/generation/infrastructure/couple-generation-builder/utils.ts +8 -0
  43. package/src/domains/generation/infrastructure/flow/flow-store-actions.ts +105 -0
  44. package/src/domains/generation/infrastructure/flow/flow-store-initial-state.ts +26 -0
  45. package/src/domains/generation/infrastructure/flow/useFlowStore.ts +4 -116
  46. package/src/domains/generation/presentation/useAIGeneration.hook.ts +1 -1
  47. package/src/domains/generation/wizard/infrastructure/strategies/image-generation-strategy-index.ts +7 -0
  48. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts +2 -12
  49. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.types.ts +11 -0
  50. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.utils.ts +12 -0
  51. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +1 -220
  52. package/src/domains/generation/wizard/infrastructure/strategies/image-input-builder.ts +66 -0
  53. package/src/domains/generation/wizard/infrastructure/strategies/image-input-extraction.ts +88 -0
  54. package/src/domains/generation/wizard/infrastructure/strategies/image-input-prompt-builder.ts +75 -0
  55. package/src/domains/generation/wizard/infrastructure/strategies/image-input-style-enhancements.ts +35 -0
  56. package/src/domains/generation/wizard/infrastructure/strategies/image-strategy-factory.ts +42 -0
  57. package/src/domains/generation/wizard/infrastructure/strategies/video-generation-executor-index.ts +11 -0
  58. package/src/domains/generation/wizard/infrastructure/strategies/video-generation-executor.ts +76 -0
  59. package/src/domains/generation/wizard/infrastructure/strategies/video-generation-input-builder.ts +46 -0
  60. package/src/domains/generation/wizard/infrastructure/strategies/video-generation-result-types.ts +17 -0
  61. package/src/domains/generation/wizard/infrastructure/strategies/video-generation-submission.ts +62 -0
  62. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.audio-extractor.ts +27 -0
  63. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.executor.ts +2 -175
  64. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.input-builder.ts +90 -0
  65. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +3 -108
  66. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +0 -129
  67. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.validation.ts +136 -0
  68. package/src/domains/generation/wizard/presentation/hooks/photo-upload/index.ts +39 -0
  69. package/src/domains/generation/wizard/presentation/hooks/photo-upload/types.ts +37 -0
  70. package/src/domains/generation/wizard/presentation/hooks/photo-upload/usePhotoUploadStateLogic.ts +142 -0
  71. package/src/domains/generation/wizard/presentation/hooks/use-video-queue-utils.ts +103 -0
  72. package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.handlers.ts +97 -0
  73. package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.saver.ts +54 -0
  74. package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +22 -87
  75. package/src/domains/generation/wizard/presentation/hooks/usePhotoUploadState.ts +8 -177
  76. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +1 -295
  77. package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +1 -1
  78. package/src/domains/generation/wizard/presentation/hooks/video-queue/index.ts +82 -0
  79. package/src/domains/generation/wizard/presentation/hooks/video-queue/useVideoQueueGenerationCallbacks.ts +120 -0
  80. package/src/domains/generation/wizard/presentation/hooks/video-queue/useVideoQueueGenerationPolling.ts +76 -0
  81. package/src/domains/generation/wizard/presentation/hooks/video-queue/useVideoQueueGenerationRefs.ts +65 -0
  82. package/src/domains/generation/wizard/presentation/hooks/video-queue/useVideoQueueGenerationStart.ts +123 -0
  83. package/src/domains/generation/wizard/presentation/hooks/video-queue-index.ts +9 -0
  84. package/src/domains/image-to-video/domain/types/image-to-video-state.types.ts +11 -4
  85. package/src/domains/text-to-image/domain/types/text-to-image.types.ts +44 -22
  86. package/src/domains/text-to-video/domain/types/request.types.ts +33 -9
  87. package/src/domains/text-to-video/domain/types/state.types.ts +29 -9
  88. package/src/domains/text-to-video/presentation/hooks/useTextToVideoForm.handlers.ts +44 -0
  89. package/src/domains/text-to-video/presentation/hooks/useTextToVideoForm.ts +5 -51
  90. package/src/domains/text-to-video/presentation/hooks/useTextToVideoForm.types.ts +33 -0
  91. package/src/infrastructure/services/generation-orchestrator.service.ts +2 -2
  92. package/src/infrastructure/utils/couple-input-context.ts +13 -0
  93. package/src/infrastructure/utils/couple-input-index.ts +8 -0
  94. package/src/infrastructure/utils/couple-input-refiner.ts +101 -0
  95. package/src/infrastructure/utils/couple-input-resolver.ts +71 -0
  96. package/src/infrastructure/utils/couple-input-types.ts +14 -0
  97. package/src/infrastructure/utils/couple-input.util.ts +3 -176
  98. package/src/infrastructure/utils/photo-generation/photo-preparation.util.ts +1 -1
  99. package/src/infrastructure/validation/base-validator.ts +1 -27
  100. package/src/infrastructure/validation/base-validator.types.ts +32 -0
  101. package/src/presentation/hooks/generation/index.ts +1 -1
  102. package/src/presentation/hooks/generation/orchestrator-abort-logs.ts +48 -0
  103. package/src/presentation/hooks/generation/orchestrator-execution-logs.ts +67 -0
  104. package/src/presentation/hooks/generation/orchestrator-index.ts +14 -0
  105. package/src/presentation/hooks/generation/orchestrator-start-logs.ts +65 -0
  106. package/src/presentation/hooks/generation/orchestrator-state-utils.ts +17 -0
  107. package/src/presentation/hooks/generation/orchestrator-types.ts +55 -0
  108. package/src/presentation/hooks/generation/orchestrator-utils-index.ts +29 -0
  109. package/src/presentation/hooks/generation/orchestrator-utils.ts +25 -0
  110. package/src/presentation/hooks/generation/useDualImageGeneration.ts +1 -1
  111. package/src/presentation/hooks/generation/useImageGeneration.ts +1 -1
  112. package/src/presentation/hooks/generation/useVideoGeneration.ts +1 -1
  113. package/src/shared/hooks/factories/generation-hook-index.ts +12 -0
  114. package/src/shared/hooks/factories/generation-hook-types.ts +47 -0
  115. package/src/shared/hooks/factories/generation-hook-utils.ts +94 -0
  116. package/src/shared/hooks/factories/index.ts +1 -1
  117. package/src/shared/index.ts +1 -1
  118. package/src/shared/utils/calculations/aspect-ratio-calculations.ts +30 -0
  119. package/src/shared/utils/calculations/base64-calculations.ts +26 -0
  120. package/src/shared/utils/calculations/confidence-calculations.ts +21 -0
  121. package/src/shared/utils/calculations/cost-calculations-index.ts +43 -0
  122. package/src/shared/utils/calculations/cost-calculations.ts +25 -0
  123. package/src/shared/utils/calculations/credit-calculations.ts +37 -0
  124. package/src/shared/utils/calculations/index.ts +46 -0
  125. package/src/shared/utils/calculations/math-utilities.ts +32 -0
  126. package/src/shared/utils/calculations/memory-calculations.ts +33 -0
  127. package/src/shared/utils/calculations/pagination-calculations.ts +38 -0
  128. package/src/shared/utils/calculations/percentage-calculations.ts +33 -0
  129. package/src/shared/utils/calculations/time-calculations.ts +99 -0
  130. package/src/shared/utils/credit.ts +1 -1
  131. package/src/shared-kernel/application/hooks/index.ts +8 -0
  132. package/src/shared-kernel/application/hooks/use-feature-state.ts +107 -0
  133. package/src/shared-kernel/application/hooks/use-generation-handler.ts +110 -0
  134. package/src/shared-kernel/base-types/base-callbacks.types.ts +73 -0
  135. package/src/shared-kernel/base-types/base-feature-state.types.ts +77 -0
  136. package/src/shared-kernel/base-types/base-generation.types.ts +69 -0
  137. package/src/shared-kernel/base-types/index.ts +30 -0
  138. package/src/shared-kernel/domain/base-generation-strategy.ts +146 -0
  139. package/src/shared-kernel/domain/index.ts +7 -0
  140. package/src/shared-kernel/index.ts +17 -0
  141. package/src/shared-kernel/infrastructure/validation/common-validators.ts +126 -0
  142. package/src/shared-kernel/infrastructure/validation/common-validators.types.ts +33 -0
  143. package/src/shared-kernel/infrastructure/validation/error-handler.ts +52 -0
  144. package/src/shared-kernel/infrastructure/validation/error-handler.types.ts +38 -0
  145. package/src/shared-kernel/infrastructure/validation/error-handler.utils.ts +79 -0
  146. package/src/shared-kernel/infrastructure/validation/index.ts +28 -0
  147. package/src/domains/background/infrastructure/services/job-poller.service.ts +0 -234
  148. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +0 -256
  149. package/src/domains/generation/infrastructure/couple-generation-builder.ts +0 -374
  150. package/src/presentation/hooks/generation/orchestrator.ts +0 -276
  151. package/src/shared/hooks/factories/createGenerationHook.ts +0 -253
  152. package/src/shared/utils/calculations.util.ts +0 -366
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Video Generation Strategy - Input Builder
3
+ * Builds video input from wizard data
4
+ */
5
+
6
+ import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
7
+ import type { WizardVideoInput } from "./video-generation.types";
8
+ import { VIDEO_PROCESSING_PROMPTS } from "./wizard-strategy.constants";
9
+ import { extractPrompt, extractDuration, extractAspectRatio, extractResolution, extractQualityMode } from "../utils";
10
+ import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
11
+ import { extractAudioAsBase64 } from "./video-generation.audio-extractor";
12
+ import { validatePhotoCount } from "./video-generation.validation";
13
+
14
+ export async function buildVideoInput(
15
+ wizardData: Record<string, unknown>,
16
+ scenario: WizardScenarioData,
17
+ ): Promise<WizardVideoInput | null> {
18
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
19
+ console.log("[VideoStrategy] Building input", { scenarioId: scenario.id });
20
+ }
21
+
22
+ // Extract audio (shared by all code paths)
23
+ const audioUrl = await extractAudioAsBase64(wizardData);
24
+
25
+ // If a pre-generated image URL exists (e.g., two-step solo video),
26
+ // use it directly instead of extracting photos from wizard data
27
+ if (scenario.preGeneratedImageUrl) {
28
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
29
+ console.log("[VideoStrategy] Using pre-generated image URL", {
30
+ url: scenario.preGeneratedImageUrl.slice(0, 80),
31
+ hasAudio: !!audioUrl,
32
+ });
33
+ }
34
+ const finalPrompt = extractPrompt(wizardData, scenario.aiPrompt) || scenario.aiPrompt || "";
35
+ const qualityMode = extractQualityMode(wizardData);
36
+
37
+ return {
38
+ sourceImageBase64: scenario.preGeneratedImageUrl,
39
+ prompt: finalPrompt,
40
+ duration: extractDuration(wizardData),
41
+ aspectRatio: extractAspectRatio(wizardData),
42
+ resolution: extractResolution(wizardData),
43
+ audioUrl,
44
+ qualityMode,
45
+ };
46
+ }
47
+
48
+ const photos = await extractPhotosAsBase64(wizardData, true);
49
+
50
+ const validation = validatePhotoCount(photos.length, scenario.inputType);
51
+ if (!validation.isValid) {
52
+ throw new Error(validation.errorKey ?? "error.generation.invalidInput");
53
+ }
54
+
55
+ let finalPrompt = extractPrompt(wizardData, scenario.aiPrompt);
56
+
57
+ if (!finalPrompt) {
58
+ const defaultPrompt = VIDEO_PROCESSING_PROMPTS[scenario.id];
59
+ if (defaultPrompt) {
60
+ finalPrompt = defaultPrompt;
61
+ } else {
62
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
63
+ console.error("[VideoStrategy] No prompt found for scenario:", scenario.id);
64
+ console.error("[VideoStrategy] Available defaults:", Object.keys(VIDEO_PROCESSING_PROMPTS));
65
+ }
66
+ throw new Error("error.generation.promptRequired");
67
+ }
68
+ }
69
+
70
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
71
+ console.log("[VideoStrategy] Using clean prompt for video generation", {
72
+ promptLength: finalPrompt.length,
73
+ photoCount: photos.length,
74
+ hasAudio: !!audioUrl,
75
+ });
76
+ }
77
+
78
+ const qualityMode = extractQualityMode(wizardData);
79
+
80
+ return {
81
+ sourceImageBase64: photos[0] || undefined,
82
+ targetImageBase64: photos[1] || photos[0] || undefined,
83
+ prompt: finalPrompt,
84
+ duration: extractDuration(wizardData),
85
+ aspectRatio: extractAspectRatio(wizardData),
86
+ resolution: extractResolution(wizardData),
87
+ audioUrl,
88
+ qualityMode,
89
+ };
90
+ }
@@ -4,116 +4,10 @@
4
4
  * Uses direct provider calls for generation models (text-to-video, image-to-video)
5
5
  */
6
6
 
7
- import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
8
7
  import type { WizardStrategy } from "./wizard-strategy.types";
9
- import { VIDEO_PROCESSING_PROMPTS } from "./wizard-strategy.constants";
10
- import { extractPrompt, extractDuration, extractAspectRatio, extractResolution, extractQualityMode } from "../utils";
11
- import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
12
- import type { WizardVideoInput, CreateVideoStrategyOptions } from "./video-generation.types";
13
- import { validatePhotoCount, validateWizardVideoInput } from "./video-generation.types";
8
+ import type { CreateVideoStrategyOptions } from "./video-generation.types";
9
+ import { validateWizardVideoInput } from "./video-generation.validation";
14
10
  import { executeVideoGeneration, submitVideoGenerationToQueue } from "./video-generation.executor";
15
- import { readFileAsBase64 } from "@umituz/react-native-design-system/filesystem";
16
-
17
-
18
-
19
- /**
20
- * Extract audio from wizardData and read as base64.
21
- * Audio step stores data as { uri: "file:///..." }.
22
- */
23
- async function extractAudioAsBase64(wizardData: Record<string, unknown>): Promise<string | undefined> {
24
- const audioData = wizardData.background_audio as { uri?: string } | undefined;
25
- if (!audioData?.uri) return undefined;
26
-
27
- try {
28
- const base64 = await readFileAsBase64(audioData.uri);
29
- if (!base64) return undefined;
30
- if (typeof __DEV__ !== "undefined" && __DEV__) {
31
- console.log("[VideoStrategy] Audio extracted as base64", { length: base64.length });
32
- }
33
- return base64;
34
- } catch (error) {
35
- console.warn("[VideoStrategy] Failed to read audio file:", error);
36
- return undefined;
37
- }
38
- }
39
-
40
- export async function buildVideoInput(
41
- wizardData: Record<string, unknown>,
42
- scenario: WizardScenarioData,
43
- ): Promise<WizardVideoInput | null> {
44
- if (typeof __DEV__ !== "undefined" && __DEV__) {
45
- console.log("[VideoStrategy] Building input", { scenarioId: scenario.id });
46
- }
47
-
48
- // Extract audio (shared by all code paths)
49
- const audioUrl = await extractAudioAsBase64(wizardData);
50
-
51
- // If a pre-generated image URL exists (e.g., two-step solo video),
52
- // use it directly instead of extracting photos from wizard data
53
- if (scenario.preGeneratedImageUrl) {
54
- if (typeof __DEV__ !== "undefined" && __DEV__) {
55
- console.log("[VideoStrategy] Using pre-generated image URL", {
56
- url: scenario.preGeneratedImageUrl.slice(0, 80),
57
- hasAudio: !!audioUrl,
58
- });
59
- }
60
- const finalPrompt = extractPrompt(wizardData, scenario.aiPrompt) || scenario.aiPrompt || "";
61
- const qualityMode = extractQualityMode(wizardData);
62
-
63
- return {
64
- sourceImageBase64: scenario.preGeneratedImageUrl,
65
- prompt: finalPrompt,
66
- duration: extractDuration(wizardData),
67
- aspectRatio: extractAspectRatio(wizardData),
68
- resolution: extractResolution(wizardData),
69
- audioUrl,
70
- qualityMode,
71
- };
72
- }
73
-
74
- const photos = await extractPhotosAsBase64(wizardData, true);
75
-
76
- const validation = validatePhotoCount(photos.length, scenario.inputType);
77
- if (!validation.isValid) {
78
- throw new Error(validation.errorKey ?? "error.generation.invalidInput");
79
- }
80
-
81
- let finalPrompt = extractPrompt(wizardData, scenario.aiPrompt);
82
-
83
- if (!finalPrompt) {
84
- const defaultPrompt = VIDEO_PROCESSING_PROMPTS[scenario.id];
85
- if (defaultPrompt) {
86
- finalPrompt = defaultPrompt;
87
- } else {
88
- if (typeof __DEV__ !== "undefined" && __DEV__) {
89
- console.error("[VideoStrategy] No prompt found for scenario:", scenario.id);
90
- console.error("[VideoStrategy] Available defaults:", Object.keys(VIDEO_PROCESSING_PROMPTS));
91
- }
92
- throw new Error("error.generation.promptRequired");
93
- }
94
- }
95
-
96
- if (typeof __DEV__ !== "undefined" && __DEV__) {
97
- console.log("[VideoStrategy] Using clean prompt for video generation", {
98
- promptLength: finalPrompt.length,
99
- photoCount: photos.length,
100
- hasAudio: !!audioUrl,
101
- });
102
- }
103
-
104
- const qualityMode = extractQualityMode(wizardData);
105
-
106
- return {
107
- sourceImageBase64: photos[0] || undefined,
108
- targetImageBase64: photos[1] || photos[0] || undefined,
109
- prompt: finalPrompt,
110
- duration: extractDuration(wizardData),
111
- aspectRatio: extractAspectRatio(wizardData),
112
- resolution: extractResolution(wizardData),
113
- audioUrl,
114
- qualityMode,
115
- };
116
- }
117
11
 
118
12
  export function createVideoStrategy(options: CreateVideoStrategyOptions): WizardStrategy {
119
13
  const { scenario, modelConfig } = options;
@@ -153,3 +47,4 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
153
47
  },
154
48
  };
155
49
  }
50
+
@@ -33,132 +33,3 @@ export interface CreateVideoStrategyOptions {
33
33
  /** Credit cost for this generation - REQUIRED, determined by the app */
34
34
  readonly creditCost: number;
35
35
  }
36
-
37
- interface PhotoValidationResult {
38
- isValid: boolean;
39
- errorKey?: string;
40
- }
41
-
42
- export function validatePhotoCount(
43
- photoCount: number,
44
- inputType: ScenarioInputType | undefined,
45
- ): PhotoValidationResult {
46
- const effectiveInputType = inputType ?? "single";
47
-
48
- switch (effectiveInputType) {
49
- case "dual":
50
- if (photoCount < 2) {
51
- return { isValid: false, errorKey: "error.generation.dualPhotosRequired" };
52
- }
53
- break;
54
- case "single":
55
- if (photoCount < 1) {
56
- return { isValid: false, errorKey: "error.generation.photoRequired" };
57
- }
58
- break;
59
- case "text":
60
- break;
61
- default: {
62
- // Exhaustive check: log unexpected input types in development
63
- const _exhaustive: never = effectiveInputType;
64
- if (typeof __DEV__ !== "undefined" && __DEV__) {
65
- console.warn(`[validatePhotoCount] Unknown inputType: ${_exhaustive}`);
66
- }
67
- break;
68
- }
69
- }
70
-
71
- return { isValid: true };
72
- }
73
-
74
- /**
75
- * Type guard for WizardVideoInput
76
- * Validates runtime input and provides type safety
77
- */
78
- function isWizardVideoInput(input: unknown): input is WizardVideoInput {
79
- if (!input || typeof input !== "object") {
80
- return false;
81
- }
82
-
83
- const obj = input as Record<string, unknown>;
84
-
85
- // prompt is required
86
- if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
87
- return false;
88
- }
89
-
90
- // Optional fields validation
91
- if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
92
- return false;
93
- }
94
-
95
- if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
96
- return false;
97
- }
98
-
99
- if (obj.duration !== undefined && typeof obj.duration !== "number") {
100
- return false;
101
- }
102
-
103
- if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
104
- return false;
105
- }
106
-
107
- if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
108
- return false;
109
- }
110
-
111
- if (obj.audioUrl !== undefined && typeof obj.audioUrl !== "string") {
112
- return false;
113
- }
114
-
115
- return true;
116
- }
117
-
118
- /**
119
- * Validates and casts input to WizardVideoInput
120
- * Throws descriptive error if validation fails
121
- */
122
- export function validateWizardVideoInput(input: unknown): WizardVideoInput {
123
- if (!isWizardVideoInput(input)) {
124
- const errors: string[] = [];
125
-
126
- if (!input || typeof input !== "object") {
127
- throw new Error("Invalid input: expected object");
128
- }
129
-
130
- const obj = input as Record<string, unknown>;
131
-
132
- if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
133
- errors.push("prompt (string, required)");
134
- }
135
-
136
- if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
137
- errors.push("sourceImageBase64 (string, optional)");
138
- }
139
-
140
- if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
141
- errors.push("targetImageBase64 (string, optional)");
142
- }
143
-
144
- if (obj.duration !== undefined && typeof obj.duration !== "number") {
145
- errors.push("duration (number, optional)");
146
- }
147
-
148
- if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
149
- errors.push("aspectRatio (string, optional)");
150
- }
151
-
152
- if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
153
- errors.push("resolution (string, optional)");
154
- }
155
-
156
- if (obj.audioUrl !== undefined && typeof obj.audioUrl !== "string") {
157
- errors.push("audioUrl (string, optional)");
158
- }
159
-
160
- throw new Error(`Invalid WizardVideoInput: ${errors.join(", ")}`);
161
- }
162
-
163
- return input;
164
- }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Wizard Video Generation Validation
3
+ * Validation functions for video generation input
4
+ */
5
+
6
+ import type { WizardVideoInput } from "./video-generation.types";
7
+ import type { ScenarioInputType } from "../../../../scenarios/domain/Scenario";
8
+
9
+ interface PhotoValidationResult {
10
+ isValid: boolean;
11
+ errorKey?: string;
12
+ }
13
+
14
+ export function validatePhotoCount(
15
+ photoCount: number,
16
+ inputType: ScenarioInputType | undefined,
17
+ ): PhotoValidationResult {
18
+ const effectiveInputType = inputType ?? "single";
19
+
20
+ switch (effectiveInputType) {
21
+ case "dual":
22
+ if (photoCount < 2) {
23
+ return { isValid: false, errorKey: "error.generation.dualPhotosRequired" };
24
+ }
25
+ break;
26
+ case "single":
27
+ if (photoCount < 1) {
28
+ return { isValid: false, errorKey: "error.generation.photoRequired" };
29
+ }
30
+ break;
31
+ case "text":
32
+ break;
33
+ default: {
34
+ // Exhaustive check: log unexpected input types in development
35
+ const _exhaustive: never = effectiveInputType;
36
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
37
+ console.warn(`[validatePhotoCount] Unknown inputType: ${_exhaustive}`);
38
+ }
39
+ break;
40
+ }
41
+ }
42
+
43
+ return { isValid: true };
44
+ }
45
+
46
+ /**
47
+ * Type guard for WizardVideoInput
48
+ * Validates runtime input and provides type safety
49
+ */
50
+ function isWizardVideoInput(input: unknown): input is WizardVideoInput {
51
+ if (!input || typeof input !== "object") {
52
+ return false;
53
+ }
54
+
55
+ const obj = input as Record<string, unknown>;
56
+
57
+ // prompt is required
58
+ if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
59
+ return false;
60
+ }
61
+
62
+ // Optional fields validation
63
+ if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
64
+ return false;
65
+ }
66
+
67
+ if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
68
+ return false;
69
+ }
70
+
71
+ if (obj.duration !== undefined && typeof obj.duration !== "number") {
72
+ return false;
73
+ }
74
+
75
+ if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
76
+ return false;
77
+ }
78
+
79
+ if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
80
+ return false;
81
+ }
82
+
83
+ if (obj.audioUrl !== undefined && typeof obj.audioUrl !== "string") {
84
+ return false;
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * Validates and casts input to WizardVideoInput
92
+ * Throws descriptive error if validation fails
93
+ */
94
+ export function validateWizardVideoInput(input: unknown): WizardVideoInput {
95
+ if (!isWizardVideoInput(input)) {
96
+ const errors: string[] = [];
97
+
98
+ if (!input || typeof input !== "object") {
99
+ throw new Error("Invalid input: expected object");
100
+ }
101
+
102
+ const obj = input as Record<string, unknown>;
103
+
104
+ if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
105
+ errors.push("prompt (string, required)");
106
+ }
107
+
108
+ if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
109
+ errors.push("sourceImageBase64 (string, optional)");
110
+ }
111
+
112
+ if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
113
+ errors.push("targetImageBase64 (string, optional)");
114
+ }
115
+
116
+ if (obj.duration !== undefined && typeof obj.duration !== "number") {
117
+ errors.push("duration (number, optional)");
118
+ }
119
+
120
+ if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
121
+ errors.push("aspectRatio (string, optional)");
122
+ }
123
+
124
+ if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
125
+ errors.push("resolution (string, optional)");
126
+ }
127
+
128
+ if (obj.audioUrl !== undefined && typeof obj.audioUrl !== "string") {
129
+ errors.push("audioUrl (string, optional)");
130
+ }
131
+
132
+ throw new Error(`Invalid WizardVideoInput: ${errors.join(", ")}`);
133
+ }
134
+
135
+ return input;
136
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Generic Photo Upload State Hook
3
+ * Manages photo upload state for wizard steps
4
+ * Uses design system's useMedia hook for media picking with built-in validation
5
+ */
6
+
7
+ import { useState } from "react";
8
+ import type { UsePhotoUploadStateProps, UsePhotoUploadStateReturn } from "./types";
9
+ import { usePhotoUploadStateLogic } from "./usePhotoUploadStateLogic";
10
+
11
+ export const usePhotoUploadState = ({
12
+ config,
13
+ translations,
14
+ initialImage,
15
+ stepId,
16
+ onError,
17
+ }: UsePhotoUploadStateProps): UsePhotoUploadStateReturn => {
18
+ const [image, setImage] = useState<UploadedImage | null>(initialImage || null);
19
+
20
+ const { handlePickImage, clearImage, isLoading } = usePhotoUploadStateLogic(
21
+ config,
22
+ translations,
23
+ initialImage,
24
+ stepId,
25
+ onError,
26
+ setImage,
27
+ );
28
+
29
+ const canContinue = image !== null && !isLoading;
30
+
31
+ return {
32
+ image,
33
+ handlePickImage,
34
+ canContinue,
35
+ clearImage,
36
+ };
37
+ };
38
+
39
+ export type { PhotoUploadConfig, PhotoUploadTranslations, PhotoUploadError, UsePhotoUploadStateProps, UsePhotoUploadStateReturn } from "./types";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Generic Photo Upload State Hook - Type Definitions
3
+ */
4
+
5
+ import type { UploadedImage } from "../../../../presentation/hooks/generation/useAIGenerateState";
6
+
7
+ export interface PhotoUploadConfig {
8
+ readonly maxFileSizeMB?: number;
9
+ }
10
+
11
+ export interface PhotoUploadTranslations {
12
+ readonly fileTooLarge: string;
13
+ readonly maxFileSize: string;
14
+ readonly error: string;
15
+ readonly uploadFailed: string;
16
+ readonly permissionDenied?: string;
17
+ }
18
+
19
+ export interface PhotoUploadError {
20
+ readonly title: string;
21
+ readonly message: string;
22
+ }
23
+
24
+ export interface UsePhotoUploadStateProps {
25
+ readonly config?: PhotoUploadConfig;
26
+ readonly translations: PhotoUploadTranslations;
27
+ readonly initialImage?: UploadedImage;
28
+ readonly stepId?: string;
29
+ readonly onError?: (error: PhotoUploadError) => void;
30
+ }
31
+
32
+ export interface UsePhotoUploadStateReturn {
33
+ readonly image: UploadedImage | null;
34
+ readonly handlePickImage: () => Promise<void>;
35
+ readonly canContinue: boolean;
36
+ readonly clearImage: () => void;
37
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Generic Photo Upload State Hook - Core Logic
3
+ */
4
+
5
+ import { useCallback, useRef, useEffect } from "react";
6
+ import { useMedia, MediaQuality, MediaValidationError, MEDIA_CONSTANTS } from "@umituz/react-native-design-system/media";
7
+ import type { UploadedImage } from "../../../../presentation/hooks/generation/useAIGenerateState";
8
+ import type { PhotoUploadConfig, PhotoUploadError, PhotoUploadTranslations } from "./types";
9
+
10
+ export function usePhotoUploadStateLogic(
11
+ config: PhotoUploadConfig | undefined,
12
+ translations: PhotoUploadTranslations,
13
+ initialImage: UploadedImage | undefined,
14
+ stepId: string | undefined,
15
+ onError: ((error: PhotoUploadError) => void) | undefined,
16
+ setImage: (image: UploadedImage | null) => void,
17
+ ) {
18
+ const { pickImage, isLoading } = useMedia();
19
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
20
+
21
+ // Use refs to avoid effect re-runs on callback changes
22
+ const onErrorRef = useRef(onError);
23
+ const translationsRef = useRef(translations);
24
+
25
+ useEffect(() => {
26
+ onErrorRef.current = onError;
27
+ translationsRef.current = translations;
28
+ }, [onError, translations]);
29
+
30
+ const maxFileSizeMB = config?.maxFileSizeMB ?? MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB;
31
+
32
+ useEffect(() => {
33
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
34
+ console.log("[usePhotoUploadState] Step changed, resetting image", { stepId, hasInitialImage: !!initialImage });
35
+ }
36
+ setImage(initialImage || null);
37
+ }, [stepId, initialImage, setImage]);
38
+
39
+ useEffect(() => {
40
+ // Clear any existing timeout first
41
+ if (timeoutRef.current) {
42
+ clearTimeout(timeoutRef.current);
43
+ timeoutRef.current = undefined;
44
+ }
45
+
46
+ if (isLoading) {
47
+ timeoutRef.current = setTimeout(() => {
48
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
49
+ console.warn("[usePhotoUploadState] Image picker timeout - possible stuck state (DEV warning only)");
50
+ }
51
+ // NOTE: Do NOT call onError here — the picker may still complete successfully.
52
+ // Showing a modal alert while the picker is open blocks the UI.
53
+ }, 30000);
54
+ }
55
+
56
+ return () => {
57
+ if (timeoutRef.current) {
58
+ clearTimeout(timeoutRef.current);
59
+ timeoutRef.current = undefined;
60
+ }
61
+ };
62
+ }, [isLoading]);
63
+
64
+ const clearImage = useCallback(() => {
65
+ setImage(null);
66
+ }, [setImage]);
67
+
68
+ const handlePickImage = useCallback(async () => {
69
+ try {
70
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
71
+ console.log("[usePhotoUploadState] Starting image pick");
72
+ }
73
+
74
+ const result = await pickImage({
75
+ allowsEditing: true,
76
+ aspect: [1, 1],
77
+ quality: MediaQuality.MEDIUM,
78
+ maxFileSizeMB,
79
+ });
80
+
81
+ if (result.error) {
82
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
83
+ console.log("[usePhotoUploadState] Validation error", result.error);
84
+ }
85
+
86
+ if (result.error === MediaValidationError.FILE_TOO_LARGE) {
87
+ onErrorRef.current?.({
88
+ title: translationsRef.current.fileTooLarge,
89
+ message: translationsRef.current.maxFileSize.replace("{size}", maxFileSizeMB.toString()),
90
+ });
91
+ } else if (result.error === MediaValidationError.PERMISSION_DENIED) {
92
+ onErrorRef.current?.({
93
+ title: translationsRef.current.error,
94
+ message: translationsRef.current.permissionDenied ?? "Permission to access media library is required",
95
+ });
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (result.canceled || !result.assets || result.assets.length === 0) {
101
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
102
+ console.log("[usePhotoUploadState] Image pick canceled");
103
+ }
104
+ return;
105
+ }
106
+
107
+ const selectedAsset = result.assets[0];
108
+ if (!selectedAsset) {
109
+ return;
110
+ }
111
+
112
+ const uploadedImage: UploadedImage = {
113
+ uri: selectedAsset.uri,
114
+ previewUrl: selectedAsset.uri,
115
+ width: selectedAsset.width,
116
+ height: selectedAsset.height,
117
+ fileSize: selectedAsset.fileSize,
118
+ };
119
+
120
+ setImage(uploadedImage);
121
+
122
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
123
+ const fileSizeMB = (selectedAsset.fileSize ?? 0) / (1024 * 1024);
124
+ console.log("[usePhotoUploadState] Image selected", {
125
+ width: uploadedImage.width,
126
+ height: uploadedImage.height,
127
+ fileSizeMB: fileSizeMB.toFixed(2),
128
+ });
129
+ }
130
+ } catch (error) {
131
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
132
+ console.error("[usePhotoUploadState] Error picking image", error);
133
+ }
134
+ onErrorRef.current?.({
135
+ title: translationsRef.current.error,
136
+ message: translationsRef.current.uploadFailed,
137
+ });
138
+ }
139
+ }, [pickImage, maxFileSizeMB, setImage]);
140
+
141
+ return { handlePickImage, clearImage, isLoading };
142
+ }