@umituz/react-native-ai-generation-content 1.84.13 → 1.84.14

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/domains/creations/presentation/components/GalleryResultPreview.tsx +1 -1
  3. package/src/domains/creations/presentation/screens/creations-gallery.types.ts +1 -1
  4. package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.executor.ts +1 -4
  5. package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.strategy.ts +5 -2
  6. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.executor.ts +1 -4
  7. package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +3 -0
  8. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +1 -3
  9. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +8 -0
  10. package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.factory.ts +10 -2
  11. package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +1 -1
  12. package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +4 -2
  13. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.tsx +1 -1
  14. package/src/domains/generation/wizard/presentation/components/WizardStepRenderer.utils.ts +2 -1
  15. package/src/domains/generation/wizard/presentation/components/step-renderers/renderAudioPickerStep.tsx +1 -1
  16. package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +13 -6
  17. package/src/domains/generation/wizard/presentation/screens/AudioPickerScreen.tsx +5 -2
  18. package/src/domains/result-preview/presentation/components/ResultPreviewScreen.tsx +4 -4
  19. package/src/domains/result-preview/presentation/types/result-components.types.ts +1 -1
  20. package/src/domains/result-preview/presentation/types/result-screen.types.ts +2 -2
  21. package/src/domains/scenarios/domain/Scenario.ts +1 -1
  22. package/src/infrastructure/providers/generation-services.provider.tsx +1 -1
  23. package/src/infrastructure/utils/couple-input.util.ts +8 -4
  24. package/src/infrastructure/utils/image-input-preprocessor.util.ts +4 -0
  25. package/src/infrastructure/utils/intensity.util.ts +5 -2
  26. package/src/presentation/hooks/generation/useAudioGenerationExecutor.ts +2 -1
  27. 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.13",
3
+ "version": "1.84.14",
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",
@@ -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 && selectedCreation.rating !== null;
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. */
@@ -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
- if (status.status !== lastStatus) {
72
- lastStatus = status.status;
73
- }
70
+ addGenerationLog(sid, TAG, `Queue: ${status.status}`);
74
71
  },
75
72
  });
76
73
 
@@ -12,10 +12,10 @@ import { executeAudioGeneration } from "./audio-generation.executor";
12
12
  // Input Builder
13
13
  // ============================================================================
14
14
 
15
- export async function buildAudioInput(
15
+ export function buildAudioInput(
16
16
  wizardData: Record<string, unknown>,
17
17
  scenario: WizardScenarioData,
18
- ): Promise<WizardAudioInput | null> {
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
 
@@ -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
- if (status.status !== lastStatus) {
89
- lastStatus = status.status;
90
- }
87
+ addGenerationLog(sid, TAG, `Queue: ${status.status}`);
91
88
  },
92
89
  });
93
90
 
@@ -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
 
@@ -32,9 +32,7 @@ async function extractAudioAsBase64(wizardData: Record<string, unknown>): Promis
32
32
  }
33
33
  return base64;
34
34
  } catch (error) {
35
- if (typeof __DEV__ !== "undefined" && __DEV__) {
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
- return buildImageInput(wizardData, scenario);
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
- return buildVideoInput(wizardData, scenario);
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 durationMs =
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
- return isRecord(value);
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 ?? `wizard.steps.${step.id}.subtitle`;
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: typedResult.imageUrl || typedResult.videoUrl || typedResult.audioUrl || "",
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
- await deductCredits(creditCost).catch((err) => {
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 (!bytes) return "";
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={{ flex: 1, backgroundColor: tokens.colors.backgroundPrimary }}>
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>
@@ -29,7 +29,7 @@ export interface ResultActionBarProps {
29
29
  /** Share callback */
30
30
  onShare: () => void;
31
31
  /** Try again callback */
32
- onTryAgain: () => void;
32
+ onTryAgain?: () => void;
33
33
  /** Rate callback */
34
34
  onRate?: () => void;
35
35
  /** Save button text */
@@ -19,8 +19,8 @@ export interface ResultPreviewScreenProps {
19
19
  /** Action callbacks */
20
20
  onDownload: () => void;
21
21
  onShare: () => void;
22
- onTryAgain: () => void;
23
- onNavigateBack: () => void;
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 (null = not authenticated) */
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
- import type { GenerationTarget } from "../../presentation/hooks/generation/useImageGenerationExecutor";
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: GenerationTarget;
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: GenerationTarget,
17
- coupleTarget: GenerationTarget,
20
+ singleTarget: GenerationTargetLike,
21
+ coupleTarget: GenerationTargetLike,
18
22
  ): CoupleInputResult {
19
23
  if (isCoupleMode && partner2PhotoUri) {
20
24
  return {
@@ -25,6 +25,10 @@ const IMAGE_FIELDS = [
25
25
  "images",
26
26
  "reference_image",
27
27
  "source_image",
28
+ "target_image",
29
+ "driver_image_url",
30
+ "face_image",
31
+ "mask_image",
28
32
  ] as const;
29
33
 
30
34
  /**
@@ -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
- 0.3 + (intensity / 100) * 0.62;
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,