@umituz/react-native-ai-generation-content 1.84.11 → 1.84.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/domain/entities/Creation.ts +1 -1
- package/src/domains/generation/index.ts +5 -0
- package/src/domains/generation/wizard/index.ts +5 -0
- package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.executor.ts +116 -0
- package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.strategy.ts +94 -0
- package/src/domains/generation/wizard/infrastructure/strategies/audio-generation.types.ts +26 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.factory.ts +9 -0
- package/src/domains/generation/wizard/infrastructure/strategies/wizard-strategy.types.ts +1 -1
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.types.ts +1 -0
- package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +1 -0
- package/src/domains/generation/wizard/presentation/hooks/usePhotoBlockingGeneration.ts +3 -2
- package/src/domains/generation/wizard/presentation/hooks/wizard-generation.types.ts +1 -1
- package/src/exports/presentation.ts +2 -1
- package/src/index.ts +4 -1
- package/src/infrastructure/utils/image-input-preprocessor.util.ts +83 -0
- package/src/presentation/components/display/VideoResultPlayer.tsx +7 -23
- package/src/presentation/hooks/generation/index.ts +8 -0
- package/src/presentation/hooks/generation/useAudioGenerationExecutor.ts +148 -0
- package/src/presentation/hooks/generation/useImageGenerationExecutor.ts +3 -1
- package/src/presentation/hooks/index.ts +4 -0
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.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",
|
|
@@ -88,7 +88,7 @@ export function mapDocumentToCreation(
|
|
|
88
88
|
): Creation {
|
|
89
89
|
const creationDate = toDate(data.createdAt) ?? new Date();
|
|
90
90
|
|
|
91
|
-
const uri = data.output?.imageUrl || data.output?.videoUrl || data.uri || "";
|
|
91
|
+
const uri = data.output?.imageUrl || data.output?.videoUrl || data.output?.audioUrl || data.uri || "";
|
|
92
92
|
|
|
93
93
|
return {
|
|
94
94
|
id,
|
|
@@ -54,6 +54,7 @@ export type {
|
|
|
54
54
|
UseWizardGenerationReturn,
|
|
55
55
|
WizardScenarioData,
|
|
56
56
|
WizardOutputType,
|
|
57
|
+
WizardAudioInput,
|
|
57
58
|
GenericWizardFlowProps,
|
|
58
59
|
TextInputScreenTranslations,
|
|
59
60
|
TextInputScreenConfig,
|
|
@@ -75,6 +76,10 @@ export {
|
|
|
75
76
|
TEXT_TO_IMAGE_WIZARD_CONFIG,
|
|
76
77
|
TEXT_TO_VIDEO_WIZARD_CONFIG,
|
|
77
78
|
IMAGE_TO_VIDEO_WIZARD_CONFIG,
|
|
79
|
+
// Audio generation
|
|
80
|
+
buildAudioInput,
|
|
81
|
+
createAudioStrategy,
|
|
82
|
+
executeAudioGeneration,
|
|
78
83
|
} from "./wizard";
|
|
79
84
|
|
|
80
85
|
// Flow Infrastructure
|
|
@@ -80,6 +80,11 @@ export type {
|
|
|
80
80
|
AudioPickerScreenProps,
|
|
81
81
|
} from "./presentation/screens";
|
|
82
82
|
|
|
83
|
+
// Audio Generation Types
|
|
84
|
+
export type { WizardAudioInput } from "./infrastructure/strategies/audio-generation.types";
|
|
85
|
+
export { buildAudioInput, createAudioStrategy } from "./infrastructure/strategies/audio-generation.strategy";
|
|
86
|
+
export { executeAudioGeneration } from "./infrastructure/strategies/audio-generation.executor";
|
|
87
|
+
|
|
83
88
|
// Feature Configs
|
|
84
89
|
export {
|
|
85
90
|
TEXT_TO_IMAGE_WIZARD_CONFIG,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Generation Executor
|
|
3
|
+
* Handles TTS generation via the AI provider (model-agnostic).
|
|
4
|
+
* The model endpoint (e.g., Chatterbox) is configured in the main app.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WizardAudioInput } from "./audio-generation.types";
|
|
8
|
+
import { addGenerationLogs, addGenerationLog, startGenerationLogSession } from "../../../../../infrastructure/services/generation-log-store";
|
|
9
|
+
|
|
10
|
+
/** Audio generation timeout — TTS is typically fast (10-30s) */
|
|
11
|
+
const AUDIO_GENERATION_TIMEOUT_MS = 60_000;
|
|
12
|
+
|
|
13
|
+
interface AudioExecutionResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
audioUrl?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
logSessionId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function executeAudioGeneration(
|
|
21
|
+
input: WizardAudioInput,
|
|
22
|
+
model: string,
|
|
23
|
+
onProgress?: (progress: number) => void,
|
|
24
|
+
providerId?: string,
|
|
25
|
+
): Promise<AudioExecutionResult> {
|
|
26
|
+
const TAG = "AudioExecutor";
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
const sid = startGenerationLogSession();
|
|
29
|
+
const { resolveProvider } = await import("../../../../../infrastructure/services/provider-resolver");
|
|
30
|
+
|
|
31
|
+
let provider;
|
|
32
|
+
try {
|
|
33
|
+
provider = resolveProvider(providerId);
|
|
34
|
+
} catch {
|
|
35
|
+
addGenerationLog(sid, TAG, "Provider not initialized!", "error");
|
|
36
|
+
return { success: false, error: "AI provider not initialized", logSessionId: sid };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
addGenerationLog(sid, TAG, "TTS generation starting", "info", {
|
|
41
|
+
model,
|
|
42
|
+
textLength: input.text.length,
|
|
43
|
+
voice: input.voice,
|
|
44
|
+
language: input.language,
|
|
45
|
+
timeout: AUDIO_GENERATION_TIMEOUT_MS,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Build model input — generic structure, model-specific params come from app config
|
|
49
|
+
const modelInput: Record<string, unknown> = {
|
|
50
|
+
text: input.text,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (input.voice) {
|
|
54
|
+
modelInput.voice = input.voice;
|
|
55
|
+
}
|
|
56
|
+
if (input.language) {
|
|
57
|
+
modelInput.language = input.language;
|
|
58
|
+
}
|
|
59
|
+
if (input.exaggeration !== undefined) {
|
|
60
|
+
modelInput.exaggeration = input.exaggeration;
|
|
61
|
+
}
|
|
62
|
+
if (input.cfgWeight !== undefined) {
|
|
63
|
+
modelInput.cfg_weight = input.cfgWeight;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let lastStatus = "";
|
|
67
|
+
addGenerationLog(sid, TAG, "Calling provider.subscribe()...");
|
|
68
|
+
const result = await provider.subscribe(model, modelInput, {
|
|
69
|
+
timeoutMs: AUDIO_GENERATION_TIMEOUT_MS,
|
|
70
|
+
onQueueUpdate: (status) => {
|
|
71
|
+
if (status.status !== lastStatus) {
|
|
72
|
+
lastStatus = status.status;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Collect provider logs
|
|
78
|
+
const providerSessionId = (result as { __providerSessionId?: string })?.__providerSessionId;
|
|
79
|
+
const providerLogs = provider.endLogSession?.(providerSessionId) ?? provider.getSessionLogs?.(providerSessionId) ?? [];
|
|
80
|
+
addGenerationLogs(sid, providerLogs);
|
|
81
|
+
|
|
82
|
+
// Extract audio URL from response
|
|
83
|
+
// FAL TTS models return: { audio: { url } } or { audio_url } or { url }
|
|
84
|
+
const rawResult = result as Record<string, unknown>;
|
|
85
|
+
const data = (rawResult?.data ?? rawResult) as {
|
|
86
|
+
audio?: { url: string };
|
|
87
|
+
audio_url?: string;
|
|
88
|
+
url?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const audioUrl = data?.audio?.url ?? data?.audio_url ?? data?.url;
|
|
92
|
+
|
|
93
|
+
const elapsed = Date.now() - startTime;
|
|
94
|
+
onProgress?.(100);
|
|
95
|
+
|
|
96
|
+
if (audioUrl) {
|
|
97
|
+
addGenerationLog(sid, TAG, `TTS SUCCESS in ${elapsed}ms`, "info", { audioUrl, elapsed });
|
|
98
|
+
return { success: true, audioUrl, logSessionId: sid };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
addGenerationLog(sid, TAG, `No audio in response after ${elapsed}ms`, "error", {
|
|
102
|
+
responseKeys: Object.keys(data || {}),
|
|
103
|
+
elapsed,
|
|
104
|
+
});
|
|
105
|
+
return { success: false, error: "No audio generated", logSessionId: sid };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Collect provider logs even on failure
|
|
108
|
+
const providerLogs = provider.endLogSession?.() ?? [];
|
|
109
|
+
addGenerationLogs(sid, providerLogs);
|
|
110
|
+
|
|
111
|
+
const elapsed = Date.now() - startTime;
|
|
112
|
+
const errorMsg = error instanceof Error ? error.message : "Audio generation failed";
|
|
113
|
+
addGenerationLog(sid, TAG, `TTS FAILED after ${elapsed}ms: ${errorMsg}`, "error", { elapsed });
|
|
114
|
+
return { success: false, error: errorMsg, logSessionId: sid };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Generation Strategy
|
|
3
|
+
* Handles TTS-specific generation logic (execution only)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
7
|
+
import type { WizardStrategy } from "./wizard-strategy.types";
|
|
8
|
+
import type { WizardAudioInput, CreateAudioStrategyOptions } from "./audio-generation.types";
|
|
9
|
+
import { executeAudioGeneration } from "./audio-generation.executor";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Input Builder
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export async function buildAudioInput(
|
|
16
|
+
wizardData: Record<string, unknown>,
|
|
17
|
+
scenario: WizardScenarioData,
|
|
18
|
+
): Promise<WizardAudioInput | null> {
|
|
19
|
+
// Extract text from wizard data (TEXT_INPUT step stores as "text" or "prompt")
|
|
20
|
+
const text =
|
|
21
|
+
typeof wizardData.text === "string"
|
|
22
|
+
? wizardData.text
|
|
23
|
+
: typeof wizardData.prompt === "string"
|
|
24
|
+
? wizardData.prompt
|
|
25
|
+
: "";
|
|
26
|
+
|
|
27
|
+
if (!text.trim()) {
|
|
28
|
+
throw new Error("Text is required for audio generation");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract voice selection from wizard data
|
|
32
|
+
const voice =
|
|
33
|
+
typeof wizardData.voice === "string"
|
|
34
|
+
? wizardData.voice
|
|
35
|
+
: typeof wizardData.selectedVoice === "string"
|
|
36
|
+
? wizardData.selectedVoice
|
|
37
|
+
: typeof scenario.voice === "string"
|
|
38
|
+
? scenario.voice
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
// Extract optional parameters
|
|
42
|
+
const language =
|
|
43
|
+
typeof wizardData.language === "string"
|
|
44
|
+
? wizardData.language
|
|
45
|
+
: typeof scenario.language === "string"
|
|
46
|
+
? scenario.language
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const exaggeration =
|
|
50
|
+
typeof wizardData.exaggeration === "number"
|
|
51
|
+
? wizardData.exaggeration
|
|
52
|
+
: typeof scenario.exaggeration === "number"
|
|
53
|
+
? scenario.exaggeration
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
const cfgWeight =
|
|
57
|
+
typeof wizardData.cfgWeight === "number"
|
|
58
|
+
? wizardData.cfgWeight
|
|
59
|
+
: typeof scenario.cfgWeight === "number"
|
|
60
|
+
? scenario.cfgWeight
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
63
|
+
return { text, voice, language, exaggeration, cfgWeight };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Strategy Factory
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
export function createAudioStrategy(options: CreateAudioStrategyOptions): WizardStrategy {
|
|
71
|
+
const { scenario } = options;
|
|
72
|
+
|
|
73
|
+
if (!scenario.model) {
|
|
74
|
+
throw new Error("Model is required for audio generation");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const model = scenario.model;
|
|
78
|
+
const providerId = scenario.providerId;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
execute: async (input: unknown) => {
|
|
82
|
+
const audioInput = input as WizardAudioInput;
|
|
83
|
+
const result = await executeAudioGeneration(audioInput, model, undefined, providerId);
|
|
84
|
+
|
|
85
|
+
if (!result.success || !result.audioUrl) {
|
|
86
|
+
const error = new Error(result.error || "Audio generation failed");
|
|
87
|
+
(error as Error & { logSessionId?: string }).logSessionId = result.logSessionId;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { audioUrl: result.audioUrl, logSessionId: result.logSessionId };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard Audio Generation Types
|
|
3
|
+
* Type definitions for wizard audio generation strategy (TTS)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WizardScenarioData } from "../../presentation/hooks/useWizardGeneration";
|
|
7
|
+
|
|
8
|
+
export interface WizardAudioInput {
|
|
9
|
+
/** Text content to convert to speech */
|
|
10
|
+
readonly text: string;
|
|
11
|
+
/** Voice preset name (model-specific) */
|
|
12
|
+
readonly voice?: string;
|
|
13
|
+
/** Language code (e.g., "en", "es") */
|
|
14
|
+
readonly language?: string;
|
|
15
|
+
/** Exaggeration factor for voice expressiveness (0.0 - 1.0) */
|
|
16
|
+
readonly exaggeration?: number;
|
|
17
|
+
/** CFG/pace control weight */
|
|
18
|
+
readonly cfgWeight?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CreateAudioStrategyOptions {
|
|
22
|
+
readonly scenario: WizardScenarioData;
|
|
23
|
+
readonly collectionName?: string;
|
|
24
|
+
/** Credit cost for this generation - REQUIRED, determined by the app */
|
|
25
|
+
readonly creditCost: number;
|
|
26
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { WizardStrategy } from "./wizard-strategy.types";
|
|
|
9
9
|
import type { VideoModelConfig } from "../../../../../domain/interfaces/video-model-config.types";
|
|
10
10
|
import { createImageStrategy, buildImageInput } from "./image-generation.strategy";
|
|
11
11
|
import { createVideoStrategy, buildVideoInput } from "./video-generation.strategy";
|
|
12
|
+
import { createAudioStrategy, buildAudioInput } from "./audio-generation.strategy";
|
|
12
13
|
|
|
13
14
|
// ============================================================================
|
|
14
15
|
// Types
|
|
@@ -34,6 +35,10 @@ export function createWizardStrategy(options: CreateWizardStrategyOptions): Wiza
|
|
|
34
35
|
return createImageStrategy({ scenario, collectionName, creditCost });
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
if (scenario.outputType === "audio") {
|
|
39
|
+
return createAudioStrategy({ scenario, collectionName, creditCost });
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
// Default to video strategy for video outputType or undefined
|
|
38
43
|
return createVideoStrategy({ scenario, modelConfig, collectionName, creditCost });
|
|
39
44
|
}
|
|
@@ -50,6 +55,10 @@ export async function buildWizardInput(
|
|
|
50
55
|
return buildImageInput(wizardData, scenario);
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
if (scenario.outputType === "audio") {
|
|
59
|
+
return buildAudioInput(wizardData, scenario);
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
// Default to video input for video outputType or undefined
|
|
54
63
|
return buildVideoInput(wizardData, scenario);
|
|
55
64
|
}
|
|
@@ -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 }>;
|
|
15
|
+
execute: (input: unknown) => Promise<{ imageUrl?: string; videoUrl?: string; audioUrl?: string }>;
|
|
16
16
|
/** Submit to queue for background processing - returns immediately with requestId */
|
|
17
17
|
submitToQueue?: (input: unknown) => Promise<QueueSubmissionResult>;
|
|
18
18
|
}
|
|
@@ -21,6 +21,7 @@ export interface CompletedCreationData {
|
|
|
21
21
|
readonly uri: string;
|
|
22
22
|
readonly imageUrl?: string;
|
|
23
23
|
readonly videoUrl?: string;
|
|
24
|
+
readonly audioUrl?: string;
|
|
24
25
|
readonly thumbnailUrl?: string;
|
|
25
26
|
/** Unix timestamp (ms) when generation was submitted; used to compute durationMs */
|
|
26
27
|
readonly generationStartedAt?: number;
|
|
@@ -53,6 +53,7 @@ export async function updateToCompleted(
|
|
|
53
53
|
const output: CreationOutput = {
|
|
54
54
|
...(data.imageUrl && { imageUrl: data.imageUrl }),
|
|
55
55
|
...(data.videoUrl && { videoUrl: data.videoUrl }),
|
|
56
|
+
...(data.audioUrl && { audioUrl: data.audioUrl }),
|
|
56
57
|
...(data.thumbnailUrl && { thumbnailUrl: data.thumbnailUrl }),
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -51,15 +51,16 @@ export function usePhotoBlockingGeneration(
|
|
|
51
51
|
|
|
52
52
|
const handleSuccess = useCallback(
|
|
53
53
|
async (result: unknown) => {
|
|
54
|
-
const typedResult = result as { imageUrl?: string; videoUrl?: string; logSessionId?: string };
|
|
54
|
+
const typedResult = result as { imageUrl?: string; videoUrl?: string; audioUrl?: string; logSessionId?: string };
|
|
55
55
|
const creationId = creationIdRef.current;
|
|
56
56
|
|
|
57
57
|
if (creationId && userId) {
|
|
58
58
|
try {
|
|
59
59
|
await persistence.updateToCompleted(userId, creationId, {
|
|
60
|
-
uri: typedResult.imageUrl || typedResult.videoUrl || "",
|
|
60
|
+
uri: typedResult.imageUrl || typedResult.videoUrl || typedResult.audioUrl || "",
|
|
61
61
|
imageUrl: typedResult.imageUrl,
|
|
62
62
|
videoUrl: typedResult.videoUrl,
|
|
63
|
+
audioUrl: typedResult.audioUrl,
|
|
63
64
|
logSessionId: typedResult.logSessionId,
|
|
64
65
|
});
|
|
65
66
|
} catch (err) {
|
|
@@ -7,7 +7,7 @@ import type { AlertMessages } from "../../../../../presentation/hooks/generation
|
|
|
7
7
|
import type { ScenarioInputType } from "../../../../scenarios/domain/Scenario";
|
|
8
8
|
import type { VideoModelConfig } from "../../../../../domain/interfaces/video-model-config.types";
|
|
9
9
|
|
|
10
|
-
export type WizardOutputType = "image" | "video";
|
|
10
|
+
export type WizardOutputType = "image" | "video" | "audio";
|
|
11
11
|
|
|
12
12
|
export interface WizardScenarioData {
|
|
13
13
|
readonly id: string;
|
|
@@ -9,7 +9,7 @@ export {
|
|
|
9
9
|
useGenerationFlow, useAIFeatureCallbacks,
|
|
10
10
|
useAIGenerateState, AIGenerateStep,
|
|
11
11
|
useGenerationOrchestrator, useImageGeneration, useVideoGeneration, useDualImageGeneration,
|
|
12
|
-
useImagePicker, useImageGenerationExecutor,
|
|
12
|
+
useImagePicker, useImageGenerationExecutor, useAudioGenerationExecutor,
|
|
13
13
|
createGenerationError, getAlertMessage, parseError,
|
|
14
14
|
} from "../presentation/hooks";
|
|
15
15
|
export type {
|
|
@@ -26,6 +26,7 @@ export type {
|
|
|
26
26
|
UploadedImage,
|
|
27
27
|
GenerationTarget, GenerationInput, AIImageResult,
|
|
28
28
|
ImageGenerationExecutorConfig, ImageGenerationExecutorReturn,
|
|
29
|
+
AudioGenerationInput, AudioGenerationExecutorConfig, AudioGenerationExecutorReturn,
|
|
29
30
|
} from "../presentation/hooks";
|
|
30
31
|
|
|
31
32
|
// Components
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,10 @@ export {
|
|
|
31
31
|
IMAGE_TO_VIDEO_WIZARD_CONFIG,
|
|
32
32
|
SOLO_VIDEO_WIZARD_CONFIG,
|
|
33
33
|
} from "./domains/generation/wizard";
|
|
34
|
-
export type { WizardScenarioData } from "./domains/generation/wizard";
|
|
34
|
+
export type { WizardScenarioData, WizardAudioInput } from "./domains/generation/wizard";
|
|
35
|
+
|
|
36
|
+
// Audio Generation (TTS)
|
|
37
|
+
export { executeAudioGeneration, buildAudioInput, createAudioStrategy } from "./domains/generation/wizard";
|
|
35
38
|
|
|
36
39
|
// Wizard Validators and Credit Utilities
|
|
37
40
|
export {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Input Preprocessor
|
|
3
|
+
*
|
|
4
|
+
* Converts local file URIs (file://, ph://, content://) to base64 strings
|
|
5
|
+
* before passing params to any AI provider. This ensures providers only
|
|
6
|
+
* receive base64 or HTTPS URLs — never platform-specific file URIs.
|
|
7
|
+
*
|
|
8
|
+
* Uses @umituz/react-native-design-system/filesystem for file I/O.
|
|
9
|
+
* Used by useImageGenerationExecutor to preprocess params automatically.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileAsBase64 } from "@umituz/react-native-design-system/filesystem";
|
|
13
|
+
|
|
14
|
+
const LOCAL_URI_PREFIXES = ["file://", "ph://", "content://"];
|
|
15
|
+
|
|
16
|
+
const isLocalFileUri = (value: unknown): value is string =>
|
|
17
|
+
typeof value === "string" &&
|
|
18
|
+
LOCAL_URI_PREFIXES.some((p) => value.startsWith(p));
|
|
19
|
+
|
|
20
|
+
/** Known field names that may contain image URIs */
|
|
21
|
+
const IMAGE_FIELDS = [
|
|
22
|
+
"image",
|
|
23
|
+
"image_url",
|
|
24
|
+
"image_urls",
|
|
25
|
+
"images",
|
|
26
|
+
"reference_image",
|
|
27
|
+
"source_image",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a local file URI as base64.
|
|
32
|
+
*/
|
|
33
|
+
async function readLocalUri(uri: string): Promise<string> {
|
|
34
|
+
const result = await readFileAsBase64(uri);
|
|
35
|
+
if (!result) throw new Error(`Failed to read file: ${uri}`);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a single value: convert local file URI to base64, pass others through.
|
|
41
|
+
*/
|
|
42
|
+
async function resolveValue(value: string): Promise<string> {
|
|
43
|
+
return isLocalFileUri(value) ? readLocalUri(value) : value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Preprocess generation params: scan known image fields and convert
|
|
48
|
+
* local file URIs to base64. Returns a new object (never mutates input).
|
|
49
|
+
*/
|
|
50
|
+
export async function preprocessImageInputs(
|
|
51
|
+
params: Record<string, unknown>,
|
|
52
|
+
): Promise<Record<string, unknown>> {
|
|
53
|
+
const result = { ...params };
|
|
54
|
+
let converted = 0;
|
|
55
|
+
|
|
56
|
+
for (const field of IMAGE_FIELDS) {
|
|
57
|
+
const value = result[field];
|
|
58
|
+
if (value === undefined || value === null) continue;
|
|
59
|
+
|
|
60
|
+
if (typeof value === "string" && isLocalFileUri(value)) {
|
|
61
|
+
result[field] = await resolveValue(value);
|
|
62
|
+
converted++;
|
|
63
|
+
} else if (Array.isArray(value)) {
|
|
64
|
+
const hasLocalUri = value.some(isLocalFileUri);
|
|
65
|
+
if (hasLocalUri) {
|
|
66
|
+
result[field] = await Promise.all(
|
|
67
|
+
value.map((v) =>
|
|
68
|
+
typeof v === "string" ? resolveValue(v) : v,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
converted++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (converted > 0 && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
77
|
+
console.log(
|
|
78
|
+
`[ImagePreprocessor] Converted ${converted} field(s) from file URI to base64`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
|
-
// expo-video is optional — module-level lazy require with null stubs
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
let useVideoPlayer: (...args: any[]) => any = () => null;
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
let VideoView: React.ComponentType<any> = () => null;
|
|
8
|
-
try {
|
|
9
|
-
const expoVideo = require("expo-video");
|
|
10
|
-
useVideoPlayer = expoVideo.useVideoPlayer;
|
|
11
|
-
VideoView = expoVideo.VideoView;
|
|
12
|
-
} catch {
|
|
13
|
-
// expo-video not installed in consuming app
|
|
14
|
-
}
|
|
2
|
+
import { VideoPlayer } from "@umituz/react-native-video-editor/player";
|
|
15
3
|
|
|
16
4
|
interface VideoResultPlayerProps {
|
|
17
5
|
uri: string;
|
|
18
6
|
}
|
|
19
7
|
|
|
20
8
|
export const VideoResultPlayer: React.FC<VideoResultPlayerProps> = ({ uri }) => {
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
-
const player = useVideoPlayer(uri, (player: any) => {
|
|
23
|
-
player.loop = true;
|
|
24
|
-
player.play();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
9
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
10
|
+
<VideoPlayer
|
|
11
|
+
source={uri}
|
|
12
|
+
loop
|
|
13
|
+
autoPlay
|
|
14
|
+
showControls
|
|
31
15
|
contentFit="cover"
|
|
32
|
-
|
|
16
|
+
style={{ width: "100%", aspectRatio: 2 / 3, borderRadius: 16 }}
|
|
33
17
|
/>
|
|
34
18
|
);
|
|
35
19
|
};
|
|
@@ -57,6 +57,14 @@ export type {
|
|
|
57
57
|
ImageGenerationExecutorReturn,
|
|
58
58
|
} from "./useImageGenerationExecutor";
|
|
59
59
|
|
|
60
|
+
// Standalone audio generation executor (Template Method Pattern)
|
|
61
|
+
export { useAudioGenerationExecutor } from "./useAudioGenerationExecutor";
|
|
62
|
+
export type {
|
|
63
|
+
AudioGenerationInput,
|
|
64
|
+
AudioGenerationExecutorConfig,
|
|
65
|
+
AudioGenerationExecutorReturn,
|
|
66
|
+
} from "./useAudioGenerationExecutor";
|
|
67
|
+
|
|
60
68
|
// Error utilities
|
|
61
69
|
export {
|
|
62
70
|
createGenerationError,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Generation Executor Hook (Template Method Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Mirrors useImageGenerationExecutor but for TTS/audio generation.
|
|
5
|
+
* Encapsulates: auth → credit deduction → provider call → creation save → error/refund
|
|
6
|
+
*
|
|
7
|
+
* Consumers provide domain-specific logic via config:
|
|
8
|
+
* - buildInput: text, voice, target (model + provider)
|
|
9
|
+
* - buildMetadata: creation metadata
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback, useMemo } from "react";
|
|
13
|
+
import { useGenerationServices } from "../../../infrastructure/providers/generation-services.provider";
|
|
14
|
+
import { resolveProvider } from "../../../infrastructure/services/provider-resolver";
|
|
15
|
+
import { createCreationsRepository } from "../../../domains/creations/infrastructure/adapters";
|
|
16
|
+
import type { GenerationTarget } from "./useImageGenerationExecutor";
|
|
17
|
+
|
|
18
|
+
/** Domain-specific audio generation input returned by buildInput */
|
|
19
|
+
export interface AudioGenerationInput {
|
|
20
|
+
/** Which model + provider to use */
|
|
21
|
+
readonly target: GenerationTarget;
|
|
22
|
+
/** The text being converted to speech (stored in creation record) */
|
|
23
|
+
readonly prompt: string;
|
|
24
|
+
/** Full params passed to provider.subscribe() */
|
|
25
|
+
readonly params: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Provider-agnostic AI audio result */
|
|
29
|
+
interface AIAudioResult {
|
|
30
|
+
readonly audio?: { readonly url: string };
|
|
31
|
+
readonly audio_url?: string;
|
|
32
|
+
readonly url?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Default credit cost for audio generation */
|
|
36
|
+
const DEFAULT_AUDIO_CREDIT_COST = 1;
|
|
37
|
+
|
|
38
|
+
export interface AudioGenerationExecutorConfig<P> {
|
|
39
|
+
/** Creation type stored in Firestore (e.g. "voice-generator") */
|
|
40
|
+
readonly type: string;
|
|
41
|
+
/** Credit cost per generation. Defaults to 1. */
|
|
42
|
+
readonly creditCost?: number;
|
|
43
|
+
/** Whether to call onGenerationSuccess after completion */
|
|
44
|
+
readonly trackRating?: boolean;
|
|
45
|
+
/** Build AI generation input from domain-specific params. May be async. */
|
|
46
|
+
readonly buildInput: (params: P) => Promise<AudioGenerationInput> | AudioGenerationInput;
|
|
47
|
+
/** Build domain-specific metadata for the creation record */
|
|
48
|
+
readonly buildMetadata?: (params: P) => Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AudioGenerationExecutorReturn<P> {
|
|
52
|
+
readonly execute: (params: P) => Promise<string | null>;
|
|
53
|
+
readonly isLoading: boolean;
|
|
54
|
+
readonly error: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useAudioGenerationExecutor<P>(
|
|
58
|
+
config: AudioGenerationExecutorConfig<P>,
|
|
59
|
+
): AudioGenerationExecutorReturn<P> {
|
|
60
|
+
const { userId, deductCredits, refundCredits, onGenerationSuccess } =
|
|
61
|
+
useGenerationServices();
|
|
62
|
+
const repository = useMemo(
|
|
63
|
+
() => createCreationsRepository("creations"),
|
|
64
|
+
[],
|
|
65
|
+
);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
68
|
+
|
|
69
|
+
const execute = useCallback(
|
|
70
|
+
async (params: P): Promise<string | null> => {
|
|
71
|
+
if (!userId) return null;
|
|
72
|
+
|
|
73
|
+
setError(null);
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
let deducted = false;
|
|
76
|
+
const cost = config.creditCost ?? DEFAULT_AUDIO_CREDIT_COST;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const creditSuccess = await deductCredits(cost);
|
|
80
|
+
if (!creditSuccess) {
|
|
81
|
+
setError("Insufficient credits");
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
deducted = true;
|
|
85
|
+
|
|
86
|
+
const input = await config.buildInput(params);
|
|
87
|
+
const provider = resolveProvider(input.target.providerId);
|
|
88
|
+
const result = (await provider.subscribe(
|
|
89
|
+
input.target.model,
|
|
90
|
+
input.params,
|
|
91
|
+
)) as AIAudioResult;
|
|
92
|
+
|
|
93
|
+
// Extract audio URL — FAL returns { audio: { url } }, some models { audio_url } or { url }
|
|
94
|
+
const audioUrl = result?.audio?.url ?? result?.audio_url ?? result?.url;
|
|
95
|
+
if (!audioUrl) throw new Error("No audio returned");
|
|
96
|
+
|
|
97
|
+
await repository.create(userId, {
|
|
98
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
99
|
+
type: config.type,
|
|
100
|
+
uri: audioUrl,
|
|
101
|
+
createdAt: new Date(),
|
|
102
|
+
isShared: false,
|
|
103
|
+
isFavorite: false,
|
|
104
|
+
status: "completed",
|
|
105
|
+
output: { audioUrl },
|
|
106
|
+
model: input.target.model,
|
|
107
|
+
prompt: input.prompt,
|
|
108
|
+
metadata: config.buildMetadata?.(params) ?? {},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (config.trackRating && onGenerationSuccess) {
|
|
112
|
+
void onGenerationSuccess();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return audioUrl;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message =
|
|
118
|
+
err instanceof Error ? err.message : "Generation failed";
|
|
119
|
+
setError(message);
|
|
120
|
+
if (deducted) {
|
|
121
|
+
try {
|
|
122
|
+
await refundCredits(cost);
|
|
123
|
+
} catch {
|
|
124
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
125
|
+
console.error(`[${config.type}] Refund failed`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
130
|
+
console.error(`[${config.type}]`, err);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
} finally {
|
|
134
|
+
setIsLoading(false);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[
|
|
138
|
+
userId,
|
|
139
|
+
config,
|
|
140
|
+
deductCredits,
|
|
141
|
+
refundCredits,
|
|
142
|
+
repository,
|
|
143
|
+
onGenerationSuccess,
|
|
144
|
+
],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return { execute, isLoading, error };
|
|
148
|
+
}
|
|
@@ -15,6 +15,7 @@ import { useState, useCallback, useMemo } from "react";
|
|
|
15
15
|
import { useGenerationServices } from "../../../infrastructure/providers/generation-services.provider";
|
|
16
16
|
import { resolveProvider } from "../../../infrastructure/services/provider-resolver";
|
|
17
17
|
import { createCreationsRepository } from "../../../domains/creations/infrastructure/adapters";
|
|
18
|
+
import { preprocessImageInputs } from "../../../infrastructure/utils/image-input-preprocessor.util";
|
|
18
19
|
|
|
19
20
|
/** Target for generation: which model on which provider */
|
|
20
21
|
export interface GenerationTarget {
|
|
@@ -90,9 +91,10 @@ export function useImageGenerationExecutor<P>(
|
|
|
90
91
|
|
|
91
92
|
const input = await config.buildInput(params);
|
|
92
93
|
const provider = resolveProvider(input.target.providerId);
|
|
94
|
+
const processedParams = await preprocessImageInputs(input.params);
|
|
93
95
|
const result = (await provider.subscribe(
|
|
94
96
|
input.target.model,
|
|
95
|
-
|
|
97
|
+
processedParams,
|
|
96
98
|
)) as AIImageResult;
|
|
97
99
|
|
|
98
100
|
const imageUrl = result?.images?.[0]?.url;
|
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
useDualImageGeneration,
|
|
11
11
|
useImagePicker,
|
|
12
12
|
useImageGenerationExecutor,
|
|
13
|
+
useAudioGenerationExecutor,
|
|
13
14
|
createGenerationError,
|
|
14
15
|
getAlertMessage,
|
|
15
16
|
parseError,
|
|
@@ -39,6 +40,9 @@ export type {
|
|
|
39
40
|
AIImageResult,
|
|
40
41
|
ImageGenerationExecutorConfig,
|
|
41
42
|
ImageGenerationExecutorReturn,
|
|
43
|
+
AudioGenerationInput,
|
|
44
|
+
AudioGenerationExecutorConfig,
|
|
45
|
+
AudioGenerationExecutorReturn,
|
|
42
46
|
} from "./generation";
|
|
43
47
|
|
|
44
48
|
export { useGeneration } from "./use-generation";
|