@umituz/react-native-ai-generation-content 1.84.13 → 1.84.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 +1 -1
- package/src/domains/access-control/hooks/useAIFeatureGate.ts +6 -2
- package/src/domains/access-control/types/access-control.types.ts +9 -0
- package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +1 -1
- package/src/domains/creations/presentation/screens/creations-gallery.types.ts +1 -1
- package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.executor.ts +1 -4
- package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.strategy.ts +5 -2
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts +1 -4
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +3 -0
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +1 -3
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +8 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.factory.ts +10 -2
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +1 -1
- package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +4 -2
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +1 -1
- package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.utils.ts +2 -1
- package/src/domains/generation/wizard/presentation/components/step-renderers/renderAudioPickerStep.tsx +1 -1
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +13 -6
- package/src/domains/generation/wizard/presentation/screens/AudioPickerScreen.tsx +5 -2
- package/src/domains/result-preview/presentation/components/ResultPreviewScreen.tsx +4 -4
- package/src/domains/result-preview/presentation/types/result-components.types.ts +1 -1
- package/src/domains/result-preview/presentation/types/result-screen.types.ts +2 -2
- package/src/domains/scenarios/domain/Scenario.ts +1 -1
- package/src/infrastructure/providers/generation-services.provider.tsx +1 -1
- package/src/infrastructure/utils/couple-input.util.ts +8 -4
- package/src/infrastructure/utils/image-input-preprocessor.util.ts +4 -0
- package/src/infrastructure/utils/intensity.util.ts +5 -2
- package/src/presentation/hooks/generation/useAudioGenerationExecutor.ts +2 -1
- package/src/presentation/hooks/generation/useImageGenerationExecutor.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.84.
|
|
3
|
+
"version": "1.84.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",
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* Uses `hasFirebaseUser` for auth check — anonymous users (autoAnonymousSignIn)
|
|
6
6
|
* pass the gate and go straight to credit/subscription checks.
|
|
7
7
|
* Auth modal is only shown if there is NO Firebase user at all.
|
|
8
|
+
*
|
|
9
|
+
* Paywall presentation:
|
|
10
|
+
* - Default: uses `openPaywall()` from `usePaywallVisibility` (React-based PaywallContainer)
|
|
11
|
+
* - Custom: accepts `onShowPaywall` option for native paywalls (e.g. RevenueCat `presentPaywall()`)
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import { useCallback, useMemo } from "react";
|
|
@@ -38,7 +42,7 @@ const handlePromiseResult = (
|
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateReturn {
|
|
41
|
-
const { creditCost, onNetworkError, onSuccess, onError } = options;
|
|
45
|
+
const { creditCost, onNetworkError, onSuccess, onError, onShowPaywall: customShowPaywall } = options;
|
|
42
46
|
|
|
43
47
|
const { isOffline } = useOffline();
|
|
44
48
|
const { hasFirebaseUser } = useAuth();
|
|
@@ -55,7 +59,7 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
|
|
|
55
59
|
hasSubscription: isPremium,
|
|
56
60
|
creditBalance,
|
|
57
61
|
requiredCredits: creditCost,
|
|
58
|
-
onShowPaywall: () => openPaywall(),
|
|
62
|
+
onShowPaywall: customShowPaywall ?? (() => openPaywall()),
|
|
59
63
|
isCreditsLoaded,
|
|
60
64
|
});
|
|
61
65
|
|
|
@@ -23,6 +23,15 @@ export interface AIFeatureGateOptions {
|
|
|
23
23
|
* Callback fired when feature access fails or execution errors
|
|
24
24
|
*/
|
|
25
25
|
onError?: (error: Error) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Custom paywall presentation.
|
|
29
|
+
* When provided, this is called instead of the default `openPaywall()` from
|
|
30
|
+
* `usePaywallVisibility`. Use this for apps that present the native
|
|
31
|
+
* RevenueCat paywall (e.g. `presentPaywall()`) instead of rendering a
|
|
32
|
+
* React-based PaywallContainer.
|
|
33
|
+
*/
|
|
34
|
+
onShowPaywall?: () => void;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export interface AIFeatureGateReturn {
|
|
@@ -56,7 +56,7 @@ export function GalleryResultPreview({
|
|
|
56
56
|
onSaveError: () => alert.show(AlertType.ERROR, AlertMode.TOAST, t("common.error"), t("result.saveError")),
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
const hasRating = selectedCreation.rating !== undefined
|
|
59
|
+
const hasRating = selectedCreation.rating !== undefined;
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
<>
|
|
@@ -22,7 +22,7 @@ export interface CreationsGalleryScreenProps {
|
|
|
22
22
|
/** Function to get dynamic title from creation metadata */
|
|
23
23
|
readonly getCreationTitle?: (creation: { type: string; metadata?: Record<string, unknown> }) => string;
|
|
24
24
|
/** Custom handler when a creation card is pressed. When provided, overrides the built-in preview. */
|
|
25
|
-
readonly onCreationPress?: (creation: { id: string; uri: string; type: string; originalUri?: string; output?: { imageUrl?: string; videoUrl?: string }; metadata?: Record<string, unknown> }) => void;
|
|
25
|
+
readonly onCreationPress?: (creation: { id: string; uri: string; type: string; originalUri?: string; output?: { imageUrl?: string; videoUrl?: string; audioUrl?: string }; metadata?: Record<string, unknown> }) => void;
|
|
26
26
|
/** Called when the user taps the Edit button in the creation detail view. Receives the image URL. Only shown for image creations. */
|
|
27
27
|
readonly onEdit?: (imageUrl: string) => void;
|
|
28
28
|
/** Called when the user taps the Edit button in the creation detail view. Receives the video URL. Only shown for video creations. */
|
package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.executor.ts
CHANGED
|
@@ -63,14 +63,11 @@ export async function executeAudioGeneration(
|
|
|
63
63
|
modelInput.cfg_weight = input.cfgWeight;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
let lastStatus = "";
|
|
67
66
|
addGenerationLog(sid, TAG, "Calling provider.subscribe()...");
|
|
68
67
|
const result = await provider.subscribe(model, modelInput, {
|
|
69
68
|
timeoutMs: AUDIO_GENERATION_TIMEOUT_MS,
|
|
70
69
|
onQueueUpdate: (status) => {
|
|
71
|
-
|
|
72
|
-
lastStatus = status.status;
|
|
73
|
-
}
|
|
70
|
+
addGenerationLog(sid, TAG, `Queue: ${status.status}`);
|
|
74
71
|
},
|
|
75
72
|
});
|
|
76
73
|
|
package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.strategy.ts
CHANGED
|
@@ -12,10 +12,10 @@ import { executeAudioGeneration } from "./audio-generation.executor";
|
|
|
12
12
|
// Input Builder
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export function buildAudioInput(
|
|
16
16
|
wizardData: Record<string, unknown>,
|
|
17
17
|
scenario: WizardScenarioData,
|
|
18
|
-
):
|
|
18
|
+
): WizardAudioInput {
|
|
19
19
|
// Extract text from wizard data (TEXT_INPUT step stores as "text" or "prompt")
|
|
20
20
|
const text =
|
|
21
21
|
typeof wizardData.text === "string"
|
|
@@ -79,6 +79,9 @@ export function createAudioStrategy(options: CreateAudioStrategyOptions): Wizard
|
|
|
79
79
|
|
|
80
80
|
return {
|
|
81
81
|
execute: async (input: unknown) => {
|
|
82
|
+
if (!input || typeof input !== "object") {
|
|
83
|
+
throw new Error("Invalid input: expected WizardAudioInput object");
|
|
84
|
+
}
|
|
82
85
|
const audioInput = input as WizardAudioInput;
|
|
83
86
|
const result = await executeAudioGeneration(audioInput, model, undefined, providerId);
|
|
84
87
|
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts
CHANGED
|
@@ -80,14 +80,11 @@ export async function executeImageGeneration(
|
|
|
80
80
|
modelInput.image_urls = imageUrls;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
let lastStatus = "";
|
|
84
83
|
addGenerationLog(sid, TAG, 'Calling provider.subscribe()...');
|
|
85
84
|
const result = await provider.subscribe(model, modelInput, {
|
|
86
85
|
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
87
86
|
onQueueUpdate: (status) => {
|
|
88
|
-
|
|
89
|
-
lastStatus = status.status;
|
|
90
|
-
}
|
|
87
|
+
addGenerationLog(sid, TAG, `Queue: ${status.status}`);
|
|
91
88
|
},
|
|
92
89
|
});
|
|
93
90
|
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts
CHANGED
|
@@ -88,6 +88,9 @@ export function createImageStrategy(options: CreateImageStrategyOptions): Wizard
|
|
|
88
88
|
|
|
89
89
|
return {
|
|
90
90
|
execute: async (input: unknown) => {
|
|
91
|
+
if (!input || typeof input !== "object") {
|
|
92
|
+
throw new Error("Invalid input: expected WizardImageInput object");
|
|
93
|
+
}
|
|
91
94
|
const imageInput = input as WizardImageInput;
|
|
92
95
|
const result = await executeImageGeneration(imageInput, model, undefined, providerId);
|
|
93
96
|
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -32,9 +32,7 @@ async function extractAudioAsBase64(wizardData: Record<string, unknown>): Promis
|
|
|
32
32
|
}
|
|
33
33
|
return base64;
|
|
34
34
|
} catch (error) {
|
|
35
|
-
|
|
36
|
-
console.warn("[VideoStrategy] Failed to read audio file:", error);
|
|
37
|
-
}
|
|
35
|
+
console.warn("[VideoStrategy] Failed to read audio file:", error);
|
|
38
36
|
return undefined;
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -56,6 +56,14 @@ export function validatePhotoCount(
|
|
|
56
56
|
break;
|
|
57
57
|
case "text":
|
|
58
58
|
break;
|
|
59
|
+
default: {
|
|
60
|
+
// Exhaustive check: log unexpected input types in development
|
|
61
|
+
const _exhaustive: never = effectiveInputType;
|
|
62
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
63
|
+
console.warn(`[validatePhotoCount] Unknown inputType: ${_exhaustive}`);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
return { isValid: true };
|
|
@@ -52,7 +52,11 @@ export async function buildWizardInput(
|
|
|
52
52
|
scenario: WizardScenarioData,
|
|
53
53
|
): Promise<unknown> {
|
|
54
54
|
if (scenario.outputType === "image") {
|
|
55
|
-
|
|
55
|
+
const input = await buildImageInput(wizardData, scenario);
|
|
56
|
+
if (!input) {
|
|
57
|
+
throw new Error("Failed to build image input");
|
|
58
|
+
}
|
|
59
|
+
return input;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
if (scenario.outputType === "audio") {
|
|
@@ -60,5 +64,9 @@ export async function buildWizardInput(
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
// Default to video input for video outputType or undefined
|
|
63
|
-
|
|
67
|
+
const input = await buildVideoInput(wizardData, scenario);
|
|
68
|
+
if (!input) {
|
|
69
|
+
throw new Error("Failed to build video input");
|
|
70
|
+
}
|
|
71
|
+
return input;
|
|
64
72
|
}
|
|
@@ -12,7 +12,7 @@ export interface QueueSubmissionResult {
|
|
|
12
12
|
|
|
13
13
|
export interface WizardStrategy {
|
|
14
14
|
/** Execute the generation - returns result with URLs (blocking) */
|
|
15
|
-
execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string; audioUrl?: string }>;
|
|
15
|
+
execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string; audioUrl?: string; logSessionId?: string }>;
|
|
16
16
|
/** Submit to queue for background processing - returns immediately with requestId */
|
|
17
17
|
submitToQueue?: (input: unknown) => Promise<QueueSubmissionResult>;
|
|
18
18
|
}
|
|
@@ -58,15 +58,17 @@ export async function updateToCompleted(
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const completedAt = new Date();
|
|
61
|
-
const
|
|
61
|
+
const rawDuration =
|
|
62
62
|
data.generationStartedAt !== undefined
|
|
63
63
|
? completedAt.getTime() - data.generationStartedAt
|
|
64
64
|
: undefined;
|
|
65
|
+
const durationMs = rawDuration !== undefined && rawDuration >= 0 ? rawDuration : undefined;
|
|
66
|
+
const hasOutput = Object.keys(output).length > 0;
|
|
65
67
|
|
|
66
68
|
await repository.update(userId, creationId, {
|
|
67
69
|
uri: data.uri,
|
|
68
70
|
status: "completed" as const,
|
|
69
|
-
output,
|
|
71
|
+
...(hasOutput && { output }),
|
|
70
72
|
completedAt,
|
|
71
73
|
...(durationMs !== undefined && { durationMs }),
|
|
72
74
|
} as Partial<Creation>);
|
|
@@ -61,7 +61,7 @@ export const WizardStepRenderer: React.FC<WizardStepRendererProps> = ({
|
|
|
61
61
|
const media = extractMediaUrl(generationResult);
|
|
62
62
|
if (!media) return null;
|
|
63
63
|
const isVideo = media.isVideo || getMediaTypeFromUrl(media.url) === "video";
|
|
64
|
-
const handleTryAgain = onTryAgain ?? onBack;
|
|
64
|
+
const handleTryAgain = onTryAgain ?? onBack ?? (() => {});
|
|
65
65
|
return (
|
|
66
66
|
<ResultPreviewScreen
|
|
67
67
|
imageUrl={isVideo ? undefined : media.url}
|
|
@@ -11,7 +11,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function isWizardStepConfig(value: unknown): value is WizardStepConfig {
|
|
14
|
-
|
|
14
|
+
if (!isRecord(value)) return false;
|
|
15
|
+
return typeof value.id === "string" && typeof value.type === "string";
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
function isUploadedImage(value: unknown): value is UploadedImage {
|
|
@@ -26,7 +26,7 @@ export function renderAudioPickerStep({
|
|
|
26
26
|
}: AudioPickerStepProps): React.ReactElement {
|
|
27
27
|
const audioConfig = getAudioPickerConfig(step.config);
|
|
28
28
|
const titleKey = audioConfig?.titleKey ?? `wizard.steps.${step.id}.title`;
|
|
29
|
-
const subtitleKey = audioConfig?.subtitleKey
|
|
29
|
+
const subtitleKey = audioConfig?.subtitleKey;
|
|
30
30
|
const isOptional = !(step.required ?? true);
|
|
31
31
|
|
|
32
32
|
return (
|
|
@@ -45,6 +45,7 @@ export function usePhotoBlockingGeneration(
|
|
|
45
45
|
alertMessages,
|
|
46
46
|
onSuccess,
|
|
47
47
|
onError,
|
|
48
|
+
onCreditsExhausted,
|
|
48
49
|
} = props;
|
|
49
50
|
|
|
50
51
|
const creationIdRef = useRef<string | null>(null);
|
|
@@ -53,11 +54,12 @@ export function usePhotoBlockingGeneration(
|
|
|
53
54
|
async (result: unknown) => {
|
|
54
55
|
const typedResult = result as { imageUrl?: string; videoUrl?: string; audioUrl?: string; logSessionId?: string };
|
|
55
56
|
const creationId = creationIdRef.current;
|
|
57
|
+
const resultUri = typedResult.imageUrl || typedResult.videoUrl || typedResult.audioUrl;
|
|
56
58
|
|
|
57
|
-
if (creationId && userId) {
|
|
59
|
+
if (creationId && userId && resultUri) {
|
|
58
60
|
try {
|
|
59
61
|
await persistence.updateToCompleted(userId, creationId, {
|
|
60
|
-
uri:
|
|
62
|
+
uri: resultUri,
|
|
61
63
|
imageUrl: typedResult.imageUrl,
|
|
62
64
|
videoUrl: typedResult.videoUrl,
|
|
63
65
|
audioUrl: typedResult.audioUrl,
|
|
@@ -74,16 +76,21 @@ export function usePhotoBlockingGeneration(
|
|
|
74
76
|
|
|
75
77
|
// Deduct credits after successful generation
|
|
76
78
|
if (deductCredits && creditCost) {
|
|
77
|
-
|
|
79
|
+
try {
|
|
80
|
+
const deducted = await deductCredits(creditCost);
|
|
81
|
+
if (!deducted) {
|
|
82
|
+
onCreditsExhausted?.();
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
78
85
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
86
|
console.error("[PhotoBlockingGeneration] deductCredits error:", err);
|
|
80
87
|
}
|
|
81
|
-
}
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
onSuccess?.(result);
|
|
85
92
|
},
|
|
86
|
-
[userId, persistence, deductCredits, creditCost, onSuccess],
|
|
93
|
+
[userId, persistence, deductCredits, creditCost, onSuccess, onCreditsExhausted],
|
|
87
94
|
);
|
|
88
95
|
|
|
89
96
|
const handleError = useCallback(
|
|
@@ -134,7 +141,7 @@ export function usePhotoBlockingGeneration(
|
|
|
134
141
|
resolution,
|
|
135
142
|
creditCost,
|
|
136
143
|
aspectRatio,
|
|
137
|
-
provider: "fal",
|
|
144
|
+
provider: scenario.providerId ?? "fal",
|
|
138
145
|
outputType: scenario.outputType,
|
|
139
146
|
});
|
|
140
147
|
creationIdRef.current = result.creationId;
|
|
@@ -91,7 +91,7 @@ export const AudioPickerScreen: React.FC<AudioPickerScreenProps> = ({
|
|
|
91
91
|
}, [onContinue]);
|
|
92
92
|
|
|
93
93
|
const formatFileSize = useCallback((bytes?: number) => {
|
|
94
|
-
if (
|
|
94
|
+
if (bytes === undefined || bytes === null) return "";
|
|
95
95
|
if (bytes < 1024) return `${bytes} B`;
|
|
96
96
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
97
97
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -101,7 +101,7 @@ export const AudioPickerScreen: React.FC<AudioPickerScreenProps> = ({
|
|
|
101
101
|
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
102
102
|
|
|
103
103
|
return (
|
|
104
|
-
<View style={{
|
|
104
|
+
<View style={[styles.root, { backgroundColor: tokens.colors.backgroundPrimary }]}>
|
|
105
105
|
<NavigationHeader
|
|
106
106
|
title=""
|
|
107
107
|
onBackPress={onBack}
|
|
@@ -183,6 +183,9 @@ export const AudioPickerScreen: React.FC<AudioPickerScreenProps> = ({
|
|
|
183
183
|
|
|
184
184
|
const createStyles = (tokens: DesignTokens) =>
|
|
185
185
|
StyleSheet.create({
|
|
186
|
+
root: {
|
|
187
|
+
flex: 1,
|
|
188
|
+
},
|
|
186
189
|
scrollContent: {
|
|
187
190
|
paddingHorizontal: tokens.spacing.lg,
|
|
188
191
|
paddingBottom: 40,
|
|
@@ -85,13 +85,13 @@ export const ResultPreviewScreen: React.FC<ResultPreviewScreenProps> = ({
|
|
|
85
85
|
showRating={showRating}
|
|
86
86
|
/>
|
|
87
87
|
</View>
|
|
88
|
-
{showRecent && (
|
|
88
|
+
{showRecent && recentCreations && translations.recentCreations && translations.viewAll && (
|
|
89
89
|
<RecentCreationsSection
|
|
90
|
-
recentCreations={recentCreations
|
|
90
|
+
recentCreations={recentCreations}
|
|
91
91
|
onViewAll={onViewAll}
|
|
92
92
|
onCreationPress={onCreationPress}
|
|
93
|
-
title={translations.recentCreations
|
|
94
|
-
viewAllLabel={translations.viewAll
|
|
93
|
+
title={translations.recentCreations}
|
|
94
|
+
viewAllLabel={translations.viewAll}
|
|
95
95
|
/>
|
|
96
96
|
)}
|
|
97
97
|
</View>
|
|
@@ -19,8 +19,8 @@ export interface ResultPreviewScreenProps {
|
|
|
19
19
|
/** Action callbacks */
|
|
20
20
|
onDownload: () => void;
|
|
21
21
|
onShare: () => void;
|
|
22
|
-
onTryAgain
|
|
23
|
-
onNavigateBack
|
|
22
|
+
onTryAgain?: () => void;
|
|
23
|
+
onNavigateBack?: () => void;
|
|
24
24
|
onRate?: () => void;
|
|
25
25
|
/** Edit callback — opens photo editor for the result image */
|
|
26
26
|
onEdit?: () => void;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Note: ScenarioId and ScenarioCategory should be defined in the app, not here
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export type ScenarioOutputType = "image" | "video";
|
|
7
|
+
export type ScenarioOutputType = "image" | "video" | "audio";
|
|
8
8
|
|
|
9
9
|
export type ScenarioInputType = "single" | "dual" | "text";
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ import React, {
|
|
|
12
12
|
} from "react";
|
|
13
13
|
|
|
14
14
|
export interface GenerationServicesValue {
|
|
15
|
-
/** Current authenticated user ID (
|
|
15
|
+
/** Current authenticated user ID (undefined = not authenticated) */
|
|
16
16
|
readonly userId: string | undefined;
|
|
17
17
|
/** Deduct credits. Returns true if successful, false if insufficient. */
|
|
18
18
|
readonly deductCredits: (cost: number) => Promise<boolean>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/** Target for generation: which model on which provider */
|
|
2
|
+
interface GenerationTargetLike {
|
|
3
|
+
readonly model: string;
|
|
4
|
+
readonly providerId: string;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
interface CoupleInputResult {
|
|
4
|
-
readonly target:
|
|
8
|
+
readonly target: GenerationTargetLike;
|
|
5
9
|
readonly imageUrls: string[];
|
|
6
10
|
}
|
|
7
11
|
|
|
@@ -13,8 +17,8 @@ export function resolveCoupleInput(
|
|
|
13
17
|
partner1PhotoUri: string,
|
|
14
18
|
partner2PhotoUri: string | null,
|
|
15
19
|
isCoupleMode: boolean,
|
|
16
|
-
singleTarget:
|
|
17
|
-
coupleTarget:
|
|
20
|
+
singleTarget: GenerationTargetLike,
|
|
21
|
+
coupleTarget: GenerationTargetLike,
|
|
18
22
|
): CoupleInputResult {
|
|
19
23
|
if (isCoupleMode && partner2PhotoUri) {
|
|
20
24
|
return {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Maps a 0-100 intensity slider value to AI model strength parameter.
|
|
3
3
|
* Range: 0→0.30, 50→0.61, 100→0.92
|
|
4
|
+
* Input is clamped to [0, 100].
|
|
4
5
|
*/
|
|
5
|
-
export const intensityToStrength = (intensity: number): number =>
|
|
6
|
-
|
|
6
|
+
export const intensityToStrength = (intensity: number): number => {
|
|
7
|
+
const clamped = Math.max(0, Math.min(100, intensity));
|
|
8
|
+
return 0.3 + (clamped / 100) * 0.62;
|
|
9
|
+
};
|
|
@@ -68,7 +68,7 @@ export function useAudioGenerationExecutor<P>(
|
|
|
68
68
|
|
|
69
69
|
const execute = useCallback(
|
|
70
70
|
async (params: P): Promise<string | null> => {
|
|
71
|
-
if (!userId) return null;
|
|
71
|
+
if (!userId || isLoading) return null;
|
|
72
72
|
|
|
73
73
|
setError(null);
|
|
74
74
|
setIsLoading(true);
|
|
@@ -136,6 +136,7 @@ export function useAudioGenerationExecutor<P>(
|
|
|
136
136
|
},
|
|
137
137
|
[
|
|
138
138
|
userId,
|
|
139
|
+
isLoading,
|
|
139
140
|
config,
|
|
140
141
|
deductCredits,
|
|
141
142
|
refundCredits,
|
|
@@ -74,7 +74,7 @@ export function useImageGenerationExecutor<P>(
|
|
|
74
74
|
|
|
75
75
|
const execute = useCallback(
|
|
76
76
|
async (params: P): Promise<string | null> => {
|
|
77
|
-
if (!userId) return null;
|
|
77
|
+
if (!userId || isLoading) return null;
|
|
78
78
|
|
|
79
79
|
setError(null);
|
|
80
80
|
setIsLoading(true);
|
|
@@ -142,6 +142,7 @@ export function useImageGenerationExecutor<P>(
|
|
|
142
142
|
},
|
|
143
143
|
[
|
|
144
144
|
userId,
|
|
145
|
+
isLoading,
|
|
145
146
|
config,
|
|
146
147
|
deductCredits,
|
|
147
148
|
refundCredits,
|