@umituz/react-native-ai-generation-content 1.37.11 → 1.37.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/creations/presentation/hooks/useGalleryFilters.ts +11 -12
- package/src/domains/creations/presentation/screens/CreationsGalleryScreen.tsx +3 -11
- package/src/domains/creations/presentation/screens/creations-gallery.types.ts +0 -1
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts +5 -21
- package/src/domains/generation/wizard/infrastructure/strategies/shared/unified-prompt-builder.ts +46 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +25 -75
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.37.
|
|
3
|
+
"version": "1.37.13",
|
|
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",
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { useState, useCallback, useMemo } from "react";
|
|
8
8
|
import type { Creation } from "../../domain/entities/Creation";
|
|
9
9
|
import type { FilterOption } from "../../domain/types/creation-filter";
|
|
10
|
-
import type { BackgroundJob } from "../../../../domain/entities/job.types";
|
|
11
10
|
import { useFilter } from "./useFilter";
|
|
12
11
|
import { useCreationsFilter } from "./useCreationsFilter";
|
|
13
12
|
|
|
@@ -16,14 +15,13 @@ interface UseGalleryFiltersProps {
|
|
|
16
15
|
readonly statusOptions: FilterOption[];
|
|
17
16
|
readonly mediaOptions: FilterOption[];
|
|
18
17
|
readonly t: (key: string) => string;
|
|
19
|
-
/** Pending background jobs to include in status counts */
|
|
20
|
-
readonly pendingJobs?: BackgroundJob[];
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
interface UseGalleryFiltersReturn {
|
|
24
21
|
readonly filtered: Creation[];
|
|
25
22
|
readonly isFiltered: boolean;
|
|
26
23
|
readonly activeFiltersCount: number;
|
|
24
|
+
readonly processingCount: number;
|
|
27
25
|
readonly statusFilterVisible: boolean;
|
|
28
26
|
readonly mediaFilterVisible: boolean;
|
|
29
27
|
readonly statusFilter: ReturnType<typeof useFilter>;
|
|
@@ -40,27 +38,27 @@ export function useGalleryFilters({
|
|
|
40
38
|
statusOptions,
|
|
41
39
|
mediaOptions,
|
|
42
40
|
t,
|
|
43
|
-
pendingJobs = [],
|
|
44
41
|
}: UseGalleryFiltersProps): UseGalleryFiltersReturn {
|
|
45
42
|
const [statusFilterVisible, setStatusFilterVisible] = useState(false);
|
|
46
43
|
const [mediaFilterVisible, setMediaFilterVisible] = useState(false);
|
|
47
44
|
|
|
48
|
-
// Calculate
|
|
49
|
-
const
|
|
50
|
-
return
|
|
51
|
-
|
|
45
|
+
// Calculate processing count from creations status (single source of truth)
|
|
46
|
+
const processingCount = useMemo(() => {
|
|
47
|
+
if (!creations) return 0;
|
|
48
|
+
return creations.filter(
|
|
49
|
+
(c) => c.status === "processing" || c.status === "queued",
|
|
52
50
|
).length;
|
|
53
|
-
}, [
|
|
51
|
+
}, [creations]);
|
|
54
52
|
|
|
55
53
|
// Enrich status options with dynamic counts
|
|
56
54
|
const enrichedStatusOptions = useMemo(() => {
|
|
57
55
|
return statusOptions.map((option) => {
|
|
58
|
-
if (option.id === "processing" &&
|
|
59
|
-
return { ...option, count:
|
|
56
|
+
if (option.id === "processing" && processingCount > 0) {
|
|
57
|
+
return { ...option, count: processingCount };
|
|
60
58
|
}
|
|
61
59
|
return option;
|
|
62
60
|
});
|
|
63
|
-
}, [statusOptions,
|
|
61
|
+
}, [statusOptions, processingCount]);
|
|
64
62
|
|
|
65
63
|
const statusFilter = useFilter({ options: enrichedStatusOptions, t });
|
|
66
64
|
const mediaFilter = useFilter({ options: mediaOptions, t });
|
|
@@ -85,6 +83,7 @@ export function useGalleryFilters({
|
|
|
85
83
|
filtered,
|
|
86
84
|
isFiltered,
|
|
87
85
|
activeFiltersCount,
|
|
86
|
+
processingCount,
|
|
88
87
|
statusFilterVisible,
|
|
89
88
|
mediaFilterVisible,
|
|
90
89
|
statusFilter,
|
|
@@ -12,7 +12,6 @@ import { useGalleryFilters } from "../hooks/useGalleryFilters";
|
|
|
12
12
|
import { useGalleryCallbacks } from "../hooks/useGalleryCallbacks";
|
|
13
13
|
import { GalleryHeader, CreationCard, GalleryEmptyStates } from "../components";
|
|
14
14
|
import { GalleryResultPreview } from "../components/GalleryResultPreview";
|
|
15
|
-
import { usePendingJobs } from "../../../../presentation/hooks/use-pending-jobs";
|
|
16
15
|
import { MEDIA_FILTER_OPTIONS, STATUS_FILTER_OPTIONS } from "../../domain/types/creation-filter";
|
|
17
16
|
import { getPreviewUrl } from "../../domain/utils";
|
|
18
17
|
import type { Creation } from "../../domain/entities/Creation";
|
|
@@ -30,7 +29,6 @@ export function CreationsGalleryScreen({
|
|
|
30
29
|
onEmptyAction,
|
|
31
30
|
emptyActionLabel,
|
|
32
31
|
showFilter = config.showFilter ?? true,
|
|
33
|
-
showPendingJobs = true,
|
|
34
32
|
}: CreationsGalleryScreenProps) {
|
|
35
33
|
const tokens = useAppDesignTokens();
|
|
36
34
|
const [selectedCreation, setSelectedCreation] = useState<Creation | null>(null);
|
|
@@ -38,7 +36,6 @@ export function CreationsGalleryScreen({
|
|
|
38
36
|
const hasAutoSelectedRef = useRef(false);
|
|
39
37
|
|
|
40
38
|
const { data: creations, isLoading, refetch } = useCreations({ userId, repository });
|
|
41
|
-
const { jobs: pendingJobs } = usePendingJobs();
|
|
42
39
|
const deleteMutation = useDeleteCreation({ userId, repository });
|
|
43
40
|
|
|
44
41
|
useEffect(() => {
|
|
@@ -68,7 +65,7 @@ export function CreationsGalleryScreen({
|
|
|
68
65
|
const showStatusFilter = config.filterConfig?.showStatusFilter ?? true;
|
|
69
66
|
const showMediaFilter = config.filterConfig?.showMediaFilter ?? true;
|
|
70
67
|
|
|
71
|
-
const filters = useGalleryFilters({ creations, statusOptions, mediaOptions, t
|
|
68
|
+
const filters = useGalleryFilters({ creations, statusOptions, mediaOptions, t });
|
|
72
69
|
|
|
73
70
|
useAppFocusEffect(useCallback(() => { void refetch(); }, [refetch]));
|
|
74
71
|
|
|
@@ -113,11 +110,6 @@ export function CreationsGalleryScreen({
|
|
|
113
110
|
/>
|
|
114
111
|
), [callbacks, getScenarioTitle]);
|
|
115
112
|
|
|
116
|
-
const activePendingCount = useMemo(() => {
|
|
117
|
-
if (!showPendingJobs) return 0;
|
|
118
|
-
return pendingJobs.filter((j) => j.status === "processing" || j.status === "queued").length;
|
|
119
|
-
}, [showPendingJobs, pendingJobs]);
|
|
120
|
-
|
|
121
113
|
const renderHeader = useMemo(() => {
|
|
122
114
|
if (!creations?.length && !isLoading) return null;
|
|
123
115
|
return (
|
|
@@ -128,12 +120,12 @@ export function CreationsGalleryScreen({
|
|
|
128
120
|
countLabel={t(config.translations.photoCount)}
|
|
129
121
|
showFilter={showFilter}
|
|
130
122
|
filterButtons={filterButtons}
|
|
131
|
-
pendingCount={
|
|
123
|
+
pendingCount={filters.processingCount}
|
|
132
124
|
pendingLabel={t("creations.processing")}
|
|
133
125
|
/>
|
|
134
126
|
</View>
|
|
135
127
|
);
|
|
136
|
-
}, [creations, isLoading, filters.filtered.length, showFilter, filterButtons, t, config, tokens
|
|
128
|
+
}, [creations, isLoading, filters.filtered.length, filters.processingCount, showFilter, filterButtons, t, config, tokens]);
|
|
137
129
|
|
|
138
130
|
const renderEmpty = useMemo(() => (
|
|
139
131
|
<GalleryEmptyStates
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* Handles the actual image generation execution
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { buildInteractionStylePrompt } from "../../../../prompts/infrastructure/builders/interaction-style-builder";
|
|
6
|
+
import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
|
|
8
7
|
import type { WizardImageInput } from "./image-generation.types";
|
|
9
8
|
import {
|
|
10
9
|
GENERATION_TIMEOUT_MS,
|
|
@@ -21,31 +20,19 @@ interface ExecutionResult {
|
|
|
21
20
|
error?: string;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* Formats base64 string with proper data URI prefix
|
|
26
|
-
*/
|
|
27
23
|
function formatBase64(base64: string): string {
|
|
28
24
|
return base64.startsWith("data:") ? base64 : `${BASE64_IMAGE_PREFIX}${base64}`;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
/**
|
|
32
|
-
* Builds the final prompt based on input type (photo-based or text-to-image)
|
|
33
|
-
*/
|
|
34
27
|
function buildFinalPrompt(input: WizardImageInput, imageUrls: string[]): string {
|
|
35
28
|
const hasPhotos = imageUrls.length > 0;
|
|
36
29
|
|
|
37
30
|
if (hasPhotos) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
return buildUnifiedPrompt({
|
|
32
|
+
basePrompt: input.prompt,
|
|
33
|
+
photoCount: imageUrls.length,
|
|
34
|
+
interactionStyle: input.interactionStyle,
|
|
41
35
|
});
|
|
42
|
-
|
|
43
|
-
const interactionPrompt = buildInteractionStylePrompt({
|
|
44
|
-
style: input.interactionStyle ?? "romantic",
|
|
45
|
-
personCount: imageUrls.length,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return interactionPrompt ? `${facePrompt}\n\n${interactionPrompt}` : facePrompt;
|
|
49
36
|
}
|
|
50
37
|
|
|
51
38
|
// Text-to-image with optional style
|
|
@@ -56,9 +43,6 @@ function buildFinalPrompt(input: WizardImageInput, imageUrls: string[]): string
|
|
|
56
43
|
return input.prompt;
|
|
57
44
|
}
|
|
58
45
|
|
|
59
|
-
/**
|
|
60
|
-
* Executes image generation using the AI provider
|
|
61
|
-
*/
|
|
62
46
|
export async function executeImageGeneration(
|
|
63
47
|
input: WizardImageInput,
|
|
64
48
|
model: string,
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/unified-prompt-builder.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Prompt Builder
|
|
3
|
+
* Single prompt building logic for ALL generation types (image & video)
|
|
4
|
+
* Uses MultiPersonPromptStructure for photo-based scenarios
|
|
5
|
+
* Uses createEnhancedPrompt for text-only scenarios
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createMultiPersonPrompt } from "../../../../../prompts/domain/entities/MultiPersonPromptStructure";
|
|
9
|
+
import { createEnhancedPrompt } from "../../../../../prompts/domain/entities/BasePromptStructure";
|
|
10
|
+
|
|
11
|
+
export interface BuildPromptOptions {
|
|
12
|
+
/** Base scenario prompt (aiPrompt from scenario config) */
|
|
13
|
+
readonly basePrompt: string;
|
|
14
|
+
/** Number of photos/people in generation */
|
|
15
|
+
readonly photoCount: number;
|
|
16
|
+
/** Interaction style from scenario (optional - only if scenario specifies it) */
|
|
17
|
+
readonly interactionStyle?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build unified prompt for any generation type
|
|
22
|
+
* - Photo-based: Uses createMultiPersonPrompt with @image1, @image2 references
|
|
23
|
+
* - Text-only: Uses createEnhancedPrompt with identity preservation
|
|
24
|
+
*/
|
|
25
|
+
export function buildUnifiedPrompt(options: BuildPromptOptions): string {
|
|
26
|
+
const { basePrompt, photoCount, interactionStyle } = options;
|
|
27
|
+
|
|
28
|
+
// Text-only generation (no photos)
|
|
29
|
+
if (photoCount === 0) {
|
|
30
|
+
return createEnhancedPrompt(basePrompt, {
|
|
31
|
+
includeIdentityPreservation: false,
|
|
32
|
+
includePhotoRealism: true,
|
|
33
|
+
includePoseGuidelines: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Photo-based generation - use multi-person prompt with @imageN references
|
|
38
|
+
let finalPrompt = createMultiPersonPrompt(basePrompt, photoCount);
|
|
39
|
+
|
|
40
|
+
// Add interaction style if specified by scenario (no defaults)
|
|
41
|
+
if (interactionStyle) {
|
|
42
|
+
finalPrompt = `${finalPrompt}\n\nINTERACTION STYLE: ${interactionStyle}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return finalPrompt;
|
|
46
|
+
}
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -5,54 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
import { executeVideoFeature } from "../../../../../infrastructure/services/video-feature-executor.service";
|
|
7
7
|
import { createCreationsRepository } from "../../../../creations/infrastructure/adapters";
|
|
8
|
+
import { buildUnifiedPrompt } from "./shared/unified-prompt-builder";
|
|
8
9
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
9
|
-
import type { ScenarioInputType } from "../../../../scenarios/domain/Scenario";
|
|
10
10
|
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
11
11
|
import { VIDEO_PROCESSING_PROMPTS } from "./wizard-strategy.constants";
|
|
12
12
|
import { extractPrompt, extractDuration, extractAspectRatio, extractResolution } from "../utils";
|
|
13
13
|
import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
|
|
14
14
|
import { getVideoFeatureType } from "./video-generation.utils";
|
|
15
15
|
import type { WizardVideoInput, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
16
|
+
import { validatePhotoCount } from "./video-generation.types";
|
|
16
17
|
|
|
17
18
|
declare const __DEV__: boolean;
|
|
18
19
|
|
|
19
20
|
export type { WizardVideoInput, WizardVideoResult, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
20
21
|
|
|
21
|
-
interface PhotoValidationResult {
|
|
22
|
-
isValid: boolean;
|
|
23
|
-
errorKey?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function validatePhotoCount(
|
|
27
|
-
photoCount: number,
|
|
28
|
-
inputType: ScenarioInputType | undefined,
|
|
29
|
-
): PhotoValidationResult {
|
|
30
|
-
const effectiveInputType = inputType ?? "single";
|
|
31
|
-
|
|
32
|
-
switch (effectiveInputType) {
|
|
33
|
-
case "dual":
|
|
34
|
-
if (photoCount < 2) {
|
|
35
|
-
return {
|
|
36
|
-
isValid: false,
|
|
37
|
-
errorKey: "error.generation.dualPhotosRequired",
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
break;
|
|
41
|
-
case "single":
|
|
42
|
-
if (photoCount < 1) {
|
|
43
|
-
return {
|
|
44
|
-
isValid: false,
|
|
45
|
-
errorKey: "error.generation.photoRequired",
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
break;
|
|
49
|
-
case "text":
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { isValid: true };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
22
|
export async function buildVideoInput(
|
|
57
23
|
wizardData: Record<string, unknown>,
|
|
58
24
|
scenario: WizardScenarioData,
|
|
@@ -65,61 +31,50 @@ export async function buildVideoInput(
|
|
|
65
31
|
|
|
66
32
|
const validation = validatePhotoCount(photos.length, scenario.inputType);
|
|
67
33
|
if (!validation.isValid) {
|
|
68
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
|
-
console.log("[VideoStrategy] Validation failed", {
|
|
70
|
-
scenarioId: scenario.id,
|
|
71
|
-
inputType: scenario.inputType,
|
|
72
|
-
photoCount: photos.length,
|
|
73
|
-
errorKey: validation.errorKey,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
34
|
throw new Error(validation.errorKey ?? "error.generation.invalidInput");
|
|
77
35
|
}
|
|
78
36
|
|
|
79
|
-
let
|
|
37
|
+
let basePrompt = extractPrompt(wizardData, scenario.aiPrompt);
|
|
80
38
|
|
|
81
|
-
if (!
|
|
39
|
+
if (!basePrompt) {
|
|
82
40
|
const defaultPrompt = VIDEO_PROCESSING_PROMPTS[scenario.id];
|
|
83
41
|
if (defaultPrompt) {
|
|
84
|
-
|
|
42
|
+
basePrompt = defaultPrompt;
|
|
85
43
|
} else {
|
|
86
44
|
throw new Error("error.generation.promptRequired");
|
|
87
45
|
}
|
|
88
46
|
}
|
|
89
47
|
|
|
90
|
-
|
|
48
|
+
// Build unified prompt with face preservation
|
|
49
|
+
const finalPrompt = buildUnifiedPrompt({
|
|
50
|
+
basePrompt,
|
|
51
|
+
photoCount: photos.length,
|
|
52
|
+
interactionStyle: scenario.interactionStyle as string | undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
+
console.log("[VideoStrategy] Prompt built", {
|
|
57
|
+
baseLength: basePrompt.length,
|
|
58
|
+
finalLength: finalPrompt.length,
|
|
59
|
+
photoCount: photos.length,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
91
64
|
sourceImageBase64: photos[0],
|
|
92
65
|
targetImageBase64: photos[1] || photos[0],
|
|
93
|
-
prompt,
|
|
66
|
+
prompt: finalPrompt,
|
|
94
67
|
duration: extractDuration(wizardData),
|
|
95
68
|
aspectRatio: extractAspectRatio(wizardData),
|
|
96
69
|
resolution: extractResolution(wizardData),
|
|
97
70
|
};
|
|
98
|
-
|
|
99
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
|
-
console.log("[VideoStrategy] Input built", {
|
|
101
|
-
hasSource: !!input.sourceImageBase64,
|
|
102
|
-
hasTarget: !!input.targetImageBase64,
|
|
103
|
-
duration: input.duration,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return input;
|
|
108
71
|
}
|
|
109
72
|
|
|
110
|
-
// ============================================================================
|
|
111
|
-
// Strategy Factory
|
|
112
|
-
// ============================================================================
|
|
113
|
-
|
|
114
73
|
export function createVideoStrategy(options: CreateVideoStrategyOptions): WizardStrategy {
|
|
115
74
|
const { scenario, collectionName = "creations" } = options;
|
|
116
75
|
const repository = createCreationsRepository(collectionName);
|
|
117
76
|
const videoFeatureType = getVideoFeatureType(scenario.id);
|
|
118
77
|
|
|
119
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
-
console.log("[VideoStrategy] Created", { scenarioId: scenario.id, videoFeatureType });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
78
|
let lastInputRef: WizardVideoInput | null = null;
|
|
124
79
|
|
|
125
80
|
return {
|
|
@@ -152,7 +107,7 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
152
107
|
const videoResult = result as { videoUrl?: string };
|
|
153
108
|
if (!input || !scenario?.id || !videoResult.videoUrl) return;
|
|
154
109
|
|
|
155
|
-
|
|
110
|
+
await repository.create(uid, {
|
|
156
111
|
id: `${scenario.id}_${Date.now()}`,
|
|
157
112
|
uri: videoResult.videoUrl,
|
|
158
113
|
type: scenario.id,
|
|
@@ -161,14 +116,9 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
161
116
|
createdAt: new Date(),
|
|
162
117
|
isShared: false,
|
|
163
118
|
isFavorite: false,
|
|
164
|
-
metadata: {
|
|
165
|
-
scenarioId: scenario.id,
|
|
166
|
-
scenarioTitle: scenario.title,
|
|
167
|
-
},
|
|
119
|
+
metadata: { scenarioId: scenario.id, scenarioTitle: scenario.title },
|
|
168
120
|
output: { videoUrl: videoResult.videoUrl },
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
await repository.create(uid, creation);
|
|
121
|
+
});
|
|
172
122
|
},
|
|
173
123
|
};
|
|
174
124
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
7
|
+
import type { ScenarioInputType } from "../../../../scenarios/domain/Scenario";
|
|
7
8
|
|
|
8
9
|
export interface WizardVideoInput {
|
|
9
10
|
/** Source image (optional for text-to-video) */
|
|
@@ -27,3 +28,32 @@ export interface CreateVideoStrategyOptions {
|
|
|
27
28
|
readonly scenario: WizardScenarioData;
|
|
28
29
|
readonly collectionName?: string;
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
export interface PhotoValidationResult {
|
|
33
|
+
isValid: boolean;
|
|
34
|
+
errorKey?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validatePhotoCount(
|
|
38
|
+
photoCount: number,
|
|
39
|
+
inputType: ScenarioInputType | undefined,
|
|
40
|
+
): PhotoValidationResult {
|
|
41
|
+
const effectiveInputType = inputType ?? "single";
|
|
42
|
+
|
|
43
|
+
switch (effectiveInputType) {
|
|
44
|
+
case "dual":
|
|
45
|
+
if (photoCount < 2) {
|
|
46
|
+
return { isValid: false, errorKey: "error.generation.dualPhotosRequired" };
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case "single":
|
|
50
|
+
if (photoCount < 1) {
|
|
51
|
+
return { isValid: false, errorKey: "error.generation.photoRequired" };
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "text":
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { isValid: true };
|
|
59
|
+
}
|