@umituz/react-native-ai-generation-content 1.41.12 → 1.42.0
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/domain/types/index.ts +21 -0
- package/src/domain/types/result.types.ts +105 -0
- package/src/domains/access-control/hooks/useAIFeatureGate.ts +1 -1
- package/src/features/image-to-video/infrastructure/services/image-to-video-executor.ts +123 -95
- package/src/features/text-to-image/infrastructure/services/text-to-image-executor.ts +92 -47
- package/src/features/text-to-video/infrastructure/services/text-to-video-executor.ts +95 -69
- package/src/index.ts +12 -0
- package/src/infrastructure/executors/base-executor.ts +169 -0
- package/src/infrastructure/executors/index.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.42.0",
|
|
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",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Types
|
|
3
|
+
* Core type definitions for the AI generation content package
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
Result,
|
|
8
|
+
Success,
|
|
9
|
+
Failure,
|
|
10
|
+
} from "./result.types";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
success,
|
|
14
|
+
failure,
|
|
15
|
+
isSuccess,
|
|
16
|
+
isFailure,
|
|
17
|
+
mapResult,
|
|
18
|
+
andThen,
|
|
19
|
+
unwrap,
|
|
20
|
+
unwrapOr,
|
|
21
|
+
} from "./result.types";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Type Pattern for Functional Error Handling
|
|
3
|
+
* Inspired by Rust's Result<T, E> type
|
|
4
|
+
*
|
|
5
|
+
* @see https://arg-software.medium.com/functional-error-handling-in-typescript-with-the-result-pattern-5b96a5abb6d3
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Success result containing a value of type T
|
|
10
|
+
*/
|
|
11
|
+
export interface Success<T> {
|
|
12
|
+
success: true;
|
|
13
|
+
value: T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Failure result containing an error of type E
|
|
18
|
+
*/
|
|
19
|
+
export interface Failure<E> {
|
|
20
|
+
success: false;
|
|
21
|
+
error: E;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Result type that can be either Success or Failure
|
|
26
|
+
* Forces explicit error handling at compile time
|
|
27
|
+
*/
|
|
28
|
+
export type Result<T, E = string> = Success<T> | Failure<E>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a successful result
|
|
32
|
+
*/
|
|
33
|
+
export function success<T>(value: T): Success<T> {
|
|
34
|
+
return { success: true, value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a failed result
|
|
39
|
+
*/
|
|
40
|
+
export function failure<E>(error: E): Failure<E> {
|
|
41
|
+
return { success: false, error };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Type guard to check if result is successful
|
|
46
|
+
*/
|
|
47
|
+
export function isSuccess<T, E>(result: Result<T, E>): result is Success<T> {
|
|
48
|
+
return result.success === true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Type guard to check if result is a failure
|
|
53
|
+
*/
|
|
54
|
+
export function isFailure<T, E>(result: Result<T, E>): result is Failure<E> {
|
|
55
|
+
return result.success === false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Map a successful result to a new value
|
|
60
|
+
* If result is failure, returns the failure unchanged
|
|
61
|
+
*/
|
|
62
|
+
export function mapResult<T, U, E>(
|
|
63
|
+
result: Result<T, E>,
|
|
64
|
+
fn: (value: T) => U,
|
|
65
|
+
): Result<U, E> {
|
|
66
|
+
if (isSuccess(result)) {
|
|
67
|
+
return success(fn(result.value));
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Chain async operations on Result types
|
|
74
|
+
* Similar to Promise.then() but for Result
|
|
75
|
+
*/
|
|
76
|
+
export async function andThen<T, U, E>(
|
|
77
|
+
result: Result<T, E>,
|
|
78
|
+
fn: (value: T) => Promise<Result<U, E>>,
|
|
79
|
+
): Promise<Result<U, E>> {
|
|
80
|
+
if (isSuccess(result)) {
|
|
81
|
+
return fn(result.value);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Unwrap a result, throwing if it's a failure
|
|
88
|
+
* Use only when you're certain the result is successful
|
|
89
|
+
*/
|
|
90
|
+
export function unwrap<T, E>(result: Result<T, E>): T {
|
|
91
|
+
if (isSuccess(result)) {
|
|
92
|
+
return result.value;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Called unwrap on a failure: ${String(result.error)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Unwrap a result or return a default value
|
|
99
|
+
*/
|
|
100
|
+
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
|
|
101
|
+
if (isSuccess(result)) {
|
|
102
|
+
return result.value;
|
|
103
|
+
}
|
|
104
|
+
return defaultValue;
|
|
105
|
+
}
|
|
@@ -41,7 +41,7 @@ import type {
|
|
|
41
41
|
export function useAIFeatureGate(
|
|
42
42
|
options: AIFeatureGateOptions,
|
|
43
43
|
): AIFeatureGateReturn {
|
|
44
|
-
const { creditCost,
|
|
44
|
+
const { creditCost, onSuccess, onError } = options;
|
|
45
45
|
|
|
46
46
|
// Auth state
|
|
47
47
|
const { isAuthenticated: rawIsAuthenticated, isAnonymous } = useAuth();
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Image-to-Video Executor
|
|
3
|
-
* Provider-agnostic image-to-video execution using
|
|
4
|
-
* Uses progress mapper for consistent progress reporting
|
|
3
|
+
* Provider-agnostic image-to-video execution using Template Method pattern
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
/** Map job status to progress percentage */
|
|
10
|
-
const getProgressFromJobStatus = (status: string): number => {
|
|
11
|
-
switch (status.toLowerCase()) {
|
|
12
|
-
case "queued": return 10;
|
|
13
|
-
case "in_queue": return 15;
|
|
14
|
-
case "processing": return 50;
|
|
15
|
-
case "in_progress": return 60;
|
|
16
|
-
case "completed": return 100;
|
|
17
|
-
default: return 30;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
6
|
+
import { BaseExecutor } from "../../../../infrastructure/executors/base-executor";
|
|
7
|
+
import { isSuccess, type Result } from "../../../../domain/types/result.types";
|
|
20
8
|
import type {
|
|
21
9
|
ImageToVideoRequest,
|
|
22
10
|
ImageToVideoResult,
|
|
@@ -24,8 +12,9 @@ import type {
|
|
|
24
12
|
ImageToVideoResultExtractor,
|
|
25
13
|
} from "../../domain/types";
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Options for image-to-video execution
|
|
17
|
+
*/
|
|
29
18
|
export interface ExecuteImageToVideoOptions {
|
|
30
19
|
model: string;
|
|
31
20
|
buildInput: ImageToVideoInputBuilder;
|
|
@@ -33,9 +22,40 @@ export interface ExecuteImageToVideoOptions {
|
|
|
33
22
|
onProgress?: (progress: number) => void;
|
|
34
23
|
}
|
|
35
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Extracted result structure from provider response
|
|
27
|
+
*/
|
|
28
|
+
interface ExtractedVideoResult {
|
|
29
|
+
videoUrl?: string;
|
|
30
|
+
thumbnailUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Map job status to progress percentage
|
|
35
|
+
*/
|
|
36
|
+
const getProgressFromJobStatus = (status: string): number => {
|
|
37
|
+
switch (status.toLowerCase()) {
|
|
38
|
+
case "queued":
|
|
39
|
+
return 10;
|
|
40
|
+
case "in_queue":
|
|
41
|
+
return 15;
|
|
42
|
+
case "processing":
|
|
43
|
+
return 50;
|
|
44
|
+
case "in_progress":
|
|
45
|
+
return 60;
|
|
46
|
+
case "completed":
|
|
47
|
+
return 100;
|
|
48
|
+
default:
|
|
49
|
+
return 30;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Default extractor for image-to-video results
|
|
55
|
+
*/
|
|
36
56
|
function defaultExtractResult(
|
|
37
57
|
result: unknown,
|
|
38
|
-
):
|
|
58
|
+
): ExtractedVideoResult | undefined {
|
|
39
59
|
if (typeof result !== "object" || result === null) return undefined;
|
|
40
60
|
|
|
41
61
|
const r = result as Record<string, unknown>;
|
|
@@ -58,108 +78,116 @@ function defaultExtractResult(
|
|
|
58
78
|
return undefined;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!provider) {
|
|
73
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
74
|
-
|
|
75
|
-
console.error("[ImageToVideoExecutor] No AI provider configured");
|
|
76
|
-
}
|
|
77
|
-
return { success: false, error: "No AI provider configured" };
|
|
81
|
+
/**
|
|
82
|
+
* Image-to-Video Executor using Template Method pattern
|
|
83
|
+
* Eliminates code duplication through BaseExecutor
|
|
84
|
+
*/
|
|
85
|
+
class ImageToVideoExecutor extends BaseExecutor<
|
|
86
|
+
ImageToVideoRequest,
|
|
87
|
+
ImageToVideoResult,
|
|
88
|
+
ExtractedVideoResult
|
|
89
|
+
> {
|
|
90
|
+
constructor() {
|
|
91
|
+
super("ImageToVideo");
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
protected validateRequest(
|
|
95
|
+
request: ImageToVideoRequest,
|
|
96
|
+
): string | undefined {
|
|
97
|
+
if (!request.imageBase64) {
|
|
98
|
+
return "Image base64 is required";
|
|
84
99
|
}
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!request.imageBase64) {
|
|
89
|
-
return { success: false, error: "Image base64 is required" };
|
|
100
|
+
return undefined;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
onProgress?.(5);
|
|
101
|
-
|
|
102
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
103
|
-
|
|
104
|
-
console.log("[ImageToVideoExecutor] Starting provider.subscribe()...");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Build input directly - let buildInput handle base64 format
|
|
108
|
-
const input = buildInput(request.imageBase64, request.motionPrompt, request.options);
|
|
103
|
+
protected async executeProvider(
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
provider: any,
|
|
106
|
+
model: string,
|
|
107
|
+
input: unknown,
|
|
108
|
+
onProgress?: (progress: number) => void,
|
|
109
|
+
): Promise<unknown> {
|
|
110
|
+
this.logInfo("Starting provider.subscribe()...");
|
|
109
111
|
|
|
110
112
|
// Use subscribe for video generation (long-running operation with queue)
|
|
111
|
-
//
|
|
113
|
+
// Provider reports real progress via onQueueUpdate callback
|
|
112
114
|
const result = await provider.subscribe(model, input, {
|
|
113
|
-
onQueueUpdate: (status) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Map provider status to progress using centralized mapper
|
|
115
|
+
onQueueUpdate: (status: { status: string; queuePosition?: number }) => {
|
|
116
|
+
this.logInfo(
|
|
117
|
+
`Queue status: ${status.status}, position: ${status.queuePosition}`,
|
|
118
|
+
);
|
|
119
|
+
// Map provider status to progress
|
|
119
120
|
const progress = getProgressFromJobStatus(status.status);
|
|
120
121
|
onProgress?.(progress);
|
|
121
122
|
},
|
|
122
123
|
timeoutMs: 300000, // 5 minutes timeout for video generation
|
|
123
124
|
});
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
131
|
-
|
|
132
|
-
console.log("[ImageToVideoExecutor] provider.subscribe() completed");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const extractor = extractResult || defaultExtractResult;
|
|
136
|
-
const extracted = extractor(result);
|
|
137
|
-
onProgress?.(100);
|
|
126
|
+
this.logInfo(
|
|
127
|
+
`Subscribe resolved, result keys: ${result ? Object.keys(result as object) : "null"}`,
|
|
128
|
+
);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
138
131
|
|
|
132
|
+
protected validateExtractedResult(
|
|
133
|
+
extracted: ExtractedVideoResult | undefined,
|
|
134
|
+
): string | undefined {
|
|
139
135
|
if (!extracted?.videoUrl) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
console.error("[ImageToVideoExecutor] No video URL in response");
|
|
143
|
-
}
|
|
144
|
-
return { success: false, error: "No video in response" };
|
|
136
|
+
return "No video in response";
|
|
145
137
|
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
146
140
|
|
|
141
|
+
protected transformResult(
|
|
142
|
+
extracted: ExtractedVideoResult,
|
|
143
|
+
): ImageToVideoResult {
|
|
147
144
|
return {
|
|
148
145
|
success: true,
|
|
149
146
|
videoUrl: extracted.videoUrl,
|
|
150
147
|
thumbnailUrl: extracted.thumbnailUrl,
|
|
151
148
|
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return { success: false, error: message };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected getDefaultExtractor(): (
|
|
152
|
+
result: unknown,
|
|
153
|
+
) => ExtractedVideoResult | undefined {
|
|
154
|
+
return defaultExtractResult;
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
157
|
|
|
158
|
+
// Singleton instance
|
|
159
|
+
const executor = new ImageToVideoExecutor();
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Execute image-to-video generation
|
|
163
|
+
* Public API maintained for backwards compatibility
|
|
164
|
+
*/
|
|
165
|
+
export async function executeImageToVideo(
|
|
166
|
+
request: ImageToVideoRequest,
|
|
167
|
+
options: ExecuteImageToVideoOptions,
|
|
168
|
+
): Promise<ImageToVideoResult> {
|
|
169
|
+
const result: Result<ImageToVideoResult, string> = await executor.execute(
|
|
170
|
+
request,
|
|
171
|
+
{
|
|
172
|
+
model: options.model,
|
|
173
|
+
buildInput: (req) =>
|
|
174
|
+
options.buildInput(req.imageBase64!, req.motionPrompt, req.options),
|
|
175
|
+
extractResult: options.extractResult,
|
|
176
|
+
onProgress: options.onProgress,
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Convert Result<T, E> back to legacy format for backwards compatibility
|
|
181
|
+
if (isSuccess(result)) {
|
|
182
|
+
return result.value;
|
|
183
|
+
}
|
|
184
|
+
return { success: false, error: result.error };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if image-to-video is supported
|
|
189
|
+
* Public API maintained for backwards compatibility
|
|
190
|
+
*/
|
|
162
191
|
export function hasImageToVideoSupport(): boolean {
|
|
163
|
-
|
|
164
|
-
return provider !== null && provider.isInitialized();
|
|
192
|
+
return executor.hasSupport();
|
|
165
193
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Text-to-Image Executor
|
|
3
|
-
* Provider-agnostic text-to-image execution using
|
|
3
|
+
* Provider-agnostic text-to-image execution using Template Method pattern
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { BaseExecutor } from "../../../../infrastructure/executors/base-executor";
|
|
7
|
+
import { isSuccess, type Result } from "../../../../domain/types/result.types";
|
|
7
8
|
import type {
|
|
8
9
|
TextToImageRequest,
|
|
9
10
|
TextToImageResult,
|
|
@@ -11,8 +12,9 @@ import type {
|
|
|
11
12
|
TextToImageResultExtractor,
|
|
12
13
|
} from "../../domain/types";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Options for text-to-image execution
|
|
17
|
+
*/
|
|
16
18
|
export interface ExecuteTextToImageOptions {
|
|
17
19
|
model: string;
|
|
18
20
|
buildInput: TextToImageInputBuilder;
|
|
@@ -20,6 +22,17 @@ export interface ExecuteTextToImageOptions {
|
|
|
20
22
|
onProgress?: (progress: number) => void;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Extracted result structure from provider response
|
|
27
|
+
*/
|
|
28
|
+
interface ExtractedImageResult {
|
|
29
|
+
imageUrl?: string;
|
|
30
|
+
imageUrls?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract images from provider response object
|
|
35
|
+
*/
|
|
23
36
|
function extractImagesFromObject(
|
|
24
37
|
obj: Record<string, unknown>,
|
|
25
38
|
): string[] | null {
|
|
@@ -40,9 +53,12 @@ function extractImagesFromObject(
|
|
|
40
53
|
return null;
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Default extractor for text-to-image results
|
|
58
|
+
*/
|
|
43
59
|
function defaultExtractResult(
|
|
44
60
|
result: unknown,
|
|
45
|
-
):
|
|
61
|
+
): ExtractedImageResult | undefined {
|
|
46
62
|
if (typeof result !== "object" || result === null) {
|
|
47
63
|
return undefined;
|
|
48
64
|
}
|
|
@@ -84,64 +100,93 @@ function defaultExtractResult(
|
|
|
84
100
|
return undefined;
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return { success: false, error: "AI provider not initialized" };
|
|
103
|
+
/**
|
|
104
|
+
* Text-to-Image Executor using Template Method pattern
|
|
105
|
+
* Eliminates code duplication through BaseExecutor
|
|
106
|
+
*/
|
|
107
|
+
class TextToImageExecutor extends BaseExecutor<
|
|
108
|
+
TextToImageRequest,
|
|
109
|
+
TextToImageResult,
|
|
110
|
+
ExtractedImageResult
|
|
111
|
+
> {
|
|
112
|
+
constructor() {
|
|
113
|
+
super("TextToImage");
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
protected validateRequest(request: TextToImageRequest): string | undefined {
|
|
117
|
+
if (!request.prompt) {
|
|
118
|
+
return "Prompt is required";
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
protected async executeProvider(
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
provider: any,
|
|
126
|
+
model: string,
|
|
127
|
+
input: unknown,
|
|
128
|
+
): Promise<unknown> {
|
|
129
|
+
return provider.run(model, input);
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const input = buildInput(request.prompt, request.options);
|
|
116
|
-
onProgress?.(20);
|
|
117
|
-
|
|
118
|
-
const result = await provider.run(model, input);
|
|
119
|
-
onProgress?.(90);
|
|
120
|
-
|
|
121
|
-
const extractor = extractResult || defaultExtractResult;
|
|
122
|
-
const extracted = extractor(result);
|
|
123
|
-
onProgress?.(100);
|
|
124
|
-
|
|
132
|
+
protected validateExtractedResult(
|
|
133
|
+
extracted: ExtractedImageResult | undefined,
|
|
134
|
+
): string | undefined {
|
|
125
135
|
if (!extracted?.imageUrl) {
|
|
126
|
-
return
|
|
136
|
+
return "No image in response";
|
|
127
137
|
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
128
140
|
|
|
141
|
+
protected transformResult(
|
|
142
|
+
extracted: ExtractedImageResult,
|
|
143
|
+
): TextToImageResult {
|
|
129
144
|
return {
|
|
130
145
|
success: true,
|
|
131
146
|
imageUrl: extracted.imageUrl,
|
|
132
147
|
imageUrls: extracted.imageUrls,
|
|
133
148
|
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return { success: false, error: message };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected getDefaultExtractor(): (
|
|
152
|
+
result: unknown,
|
|
153
|
+
) => ExtractedImageResult | undefined {
|
|
154
|
+
return defaultExtractResult;
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
|
|
158
|
+
// Singleton instance
|
|
159
|
+
const executor = new TextToImageExecutor();
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Execute text-to-image generation
|
|
163
|
+
* Public API maintained for backwards compatibility
|
|
164
|
+
*/
|
|
165
|
+
export async function executeTextToImage(
|
|
166
|
+
request: TextToImageRequest,
|
|
167
|
+
options: ExecuteTextToImageOptions,
|
|
168
|
+
): Promise<TextToImageResult> {
|
|
169
|
+
const result: Result<TextToImageResult, string> = await executor.execute(
|
|
170
|
+
request,
|
|
171
|
+
{
|
|
172
|
+
model: options.model,
|
|
173
|
+
buildInput: (req) => options.buildInput(req.prompt, req.options),
|
|
174
|
+
extractResult: options.extractResult,
|
|
175
|
+
onProgress: options.onProgress,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Convert Result<T, E> back to legacy format for backwards compatibility
|
|
180
|
+
if (isSuccess(result)) {
|
|
181
|
+
return result.value;
|
|
182
|
+
}
|
|
183
|
+
return { success: false, error: result.error };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if text-to-image is supported
|
|
188
|
+
* Public API maintained for backwards compatibility
|
|
189
|
+
*/
|
|
144
190
|
export function hasTextToImageSupport(): boolean {
|
|
145
|
-
|
|
146
|
-
return provider !== null && provider.isInitialized();
|
|
191
|
+
return executor.hasSupport();
|
|
147
192
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Text-to-Video Executor
|
|
3
|
-
*
|
|
3
|
+
* Provider-agnostic text-to-video execution using Template Method pattern
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { BaseExecutor } from "../../../../infrastructure/executors/base-executor";
|
|
7
|
+
import { isSuccess, type Result } from "../../../../domain/types/result.types";
|
|
7
8
|
import type {
|
|
8
9
|
TextToVideoRequest,
|
|
9
10
|
TextToVideoResult,
|
|
@@ -13,6 +14,9 @@ import type {
|
|
|
13
14
|
|
|
14
15
|
declare const __DEV__: boolean;
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Options for text-to-video execution
|
|
19
|
+
*/
|
|
16
20
|
export interface ExecuteTextToVideoOptions {
|
|
17
21
|
model: string;
|
|
18
22
|
buildInput: TextToVideoInputBuilder;
|
|
@@ -20,9 +24,20 @@ export interface ExecuteTextToVideoOptions {
|
|
|
20
24
|
onProgress?: (progress: number) => void;
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Extracted result structure from provider response
|
|
29
|
+
*/
|
|
30
|
+
interface ExtractedVideoResult {
|
|
31
|
+
videoUrl?: string;
|
|
32
|
+
thumbnailUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default extractor for text-to-video results
|
|
37
|
+
*/
|
|
23
38
|
function defaultExtractResult(
|
|
24
39
|
result: unknown,
|
|
25
|
-
):
|
|
40
|
+
): ExtractedVideoResult | undefined {
|
|
26
41
|
if (typeof result !== "object" || result === null) return undefined;
|
|
27
42
|
|
|
28
43
|
const r = result as Record<string, unknown>;
|
|
@@ -45,97 +60,108 @@ function defaultExtractResult(
|
|
|
45
60
|
return undefined;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!provider) {
|
|
60
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
|
|
62
|
-
console.error("[TextToVideoExecutor] No AI provider configured");
|
|
63
|
-
}
|
|
64
|
-
return { success: false, error: "No AI provider configured" };
|
|
63
|
+
/**
|
|
64
|
+
* Text-to-Video Executor using Template Method pattern
|
|
65
|
+
* Eliminates code duplication through BaseExecutor
|
|
66
|
+
*/
|
|
67
|
+
class TextToVideoExecutor extends BaseExecutor<
|
|
68
|
+
TextToVideoRequest,
|
|
69
|
+
TextToVideoResult,
|
|
70
|
+
ExtractedVideoResult
|
|
71
|
+
> {
|
|
72
|
+
constructor() {
|
|
73
|
+
super("TextToVideo");
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
console.error("[TextToVideoExecutor] AI provider not initialized");
|
|
76
|
+
protected validateRequest(request: TextToVideoRequest): string | undefined {
|
|
77
|
+
if (!request.prompt) {
|
|
78
|
+
return "Prompt is required";
|
|
71
79
|
}
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!request.prompt) {
|
|
76
|
-
return { success: false, error: "Prompt is required" };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const { model, buildInput, extractResult, onProgress } = options;
|
|
80
|
-
|
|
81
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
82
|
-
|
|
83
|
-
console.log(`[TextToVideoExecutor] Provider: ${provider.providerId}, Model: ${model}`);
|
|
80
|
+
return undefined;
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
protected async executeProvider(
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
provider: any,
|
|
86
|
+
model: string,
|
|
87
|
+
input: unknown,
|
|
88
|
+
onProgress?: (progress: number) => void,
|
|
89
|
+
): Promise<unknown> {
|
|
90
|
+
this.logInfo("Starting provider.run()...");
|
|
94
91
|
|
|
92
|
+
// Provider reports real progress via callback
|
|
95
93
|
const result = await provider.run(model, input, {
|
|
96
|
-
onProgress: (progressInfo) => {
|
|
94
|
+
onProgress: (progressInfo: { progress: number }) => {
|
|
97
95
|
const progressValue = progressInfo.progress;
|
|
98
96
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
99
|
-
|
|
100
|
-
console.log("[TextToVideoExecutor] Progress:", progressValue);
|
|
97
|
+
console.log("[TextToVideo] Progress:", progressValue);
|
|
101
98
|
}
|
|
102
99
|
onProgress?.(progressValue);
|
|
103
100
|
},
|
|
104
101
|
});
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const extractor = extractResult || defaultExtractResult;
|
|
112
|
-
const extracted = extractor(result);
|
|
113
|
-
onProgress?.(100);
|
|
103
|
+
this.logInfo("provider.run() completed");
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
114
106
|
|
|
107
|
+
protected validateExtractedResult(
|
|
108
|
+
extracted: ExtractedVideoResult | undefined,
|
|
109
|
+
): string | undefined {
|
|
115
110
|
if (!extracted?.videoUrl) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.error("[TextToVideoExecutor] No video URL in response");
|
|
119
|
-
}
|
|
120
|
-
return { success: false, error: "No video in response" };
|
|
111
|
+
return "No video in response";
|
|
121
112
|
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
122
115
|
|
|
116
|
+
protected transformResult(
|
|
117
|
+
extracted: ExtractedVideoResult,
|
|
118
|
+
): TextToVideoResult {
|
|
123
119
|
return {
|
|
124
120
|
success: true,
|
|
125
121
|
videoUrl: extracted.videoUrl,
|
|
126
122
|
thumbnailUrl: extracted.thumbnailUrl,
|
|
127
123
|
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return { success: false, error: message };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected getDefaultExtractor(): (
|
|
127
|
+
result: unknown,
|
|
128
|
+
) => ExtractedVideoResult | undefined {
|
|
129
|
+
return defaultExtractResult;
|
|
135
130
|
}
|
|
136
131
|
}
|
|
137
132
|
|
|
133
|
+
// Singleton instance
|
|
134
|
+
const executor = new TextToVideoExecutor();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute text-to-video generation
|
|
138
|
+
* Public API maintained for backwards compatibility
|
|
139
|
+
*/
|
|
140
|
+
export async function executeTextToVideo(
|
|
141
|
+
request: TextToVideoRequest,
|
|
142
|
+
options: ExecuteTextToVideoOptions,
|
|
143
|
+
): Promise<TextToVideoResult> {
|
|
144
|
+
const result: Result<TextToVideoResult, string> = await executor.execute(
|
|
145
|
+
request,
|
|
146
|
+
{
|
|
147
|
+
model: options.model,
|
|
148
|
+
buildInput: (req) => options.buildInput(req.prompt, req.options),
|
|
149
|
+
extractResult: options.extractResult,
|
|
150
|
+
onProgress: options.onProgress,
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Convert Result<T, E> back to legacy format for backwards compatibility
|
|
155
|
+
if (isSuccess(result)) {
|
|
156
|
+
return result.value;
|
|
157
|
+
}
|
|
158
|
+
return { success: false, error: result.error };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if text-to-video is supported
|
|
163
|
+
* Public API maintained for backwards compatibility
|
|
164
|
+
*/
|
|
138
165
|
export function hasTextToVideoSupport(): boolean {
|
|
139
|
-
|
|
140
|
-
return provider !== null && provider.isInitialized();
|
|
166
|
+
return executor.hasSupport();
|
|
141
167
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] @umituz/react-native-ai-generation-content/index.ts - Module loading");
|
|
7
7
|
|
|
8
|
+
// Result Type Pattern - Functional error handling
|
|
9
|
+
export type {
|
|
10
|
+
Result, Success, Failure,
|
|
11
|
+
} from "./domain/types";
|
|
12
|
+
export {
|
|
13
|
+
success, failure, isSuccess, isFailure, mapResult, andThen, unwrap, unwrapOr,
|
|
14
|
+
} from "./domain/types";
|
|
15
|
+
|
|
16
|
+
// Base Executor - Template Method Pattern
|
|
17
|
+
export { BaseExecutor } from "./infrastructure/executors";
|
|
18
|
+
export type { BaseExecutorOptions } from "./infrastructure/executors";
|
|
19
|
+
|
|
8
20
|
export type {
|
|
9
21
|
AIProviderConfig, IAIProvider, JobSubmission, JobStatus, AIJobStatusType, AILogEntry,
|
|
10
22
|
SubscribeOptions, RunOptions, ImageFeatureType, VideoFeatureType, ImageFeatureInputData,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseExecutor - Template Method Pattern
|
|
3
|
+
* Eliminates code duplication across 8+ executor implementations
|
|
4
|
+
*
|
|
5
|
+
* @see https://refactoring.guru/design-patterns/template-method/typescript/example
|
|
6
|
+
*
|
|
7
|
+
* Template Method Pattern defines skeleton of algorithm in base class,
|
|
8
|
+
* letting subclasses override specific steps without changing structure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { providerRegistry } from "../services";
|
|
12
|
+
import { failure, success, type Result } from "../../domain/types/result.types";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base options that all executors share
|
|
18
|
+
*/
|
|
19
|
+
export interface BaseExecutorOptions<TInput, TOutput> {
|
|
20
|
+
model: string;
|
|
21
|
+
buildInput: (request: TInput) => unknown;
|
|
22
|
+
extractResult?: (result: unknown) => TOutput | undefined;
|
|
23
|
+
onProgress?: (progress: number) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Abstract base class for all AI feature executors
|
|
28
|
+
* Implements Template Method pattern for execution flow
|
|
29
|
+
*/
|
|
30
|
+
export abstract class BaseExecutor<TRequest, TResult, TOutput> {
|
|
31
|
+
protected readonly logPrefix: string;
|
|
32
|
+
|
|
33
|
+
constructor(logPrefix: string) {
|
|
34
|
+
this.logPrefix = logPrefix;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Template Method - defines the execution algorithm skeleton
|
|
39
|
+
* This is the main method that orchestrates the execution flow
|
|
40
|
+
*/
|
|
41
|
+
public async execute(
|
|
42
|
+
request: TRequest,
|
|
43
|
+
options: BaseExecutorOptions<TRequest, TOutput>,
|
|
44
|
+
): Promise<Result<TResult, string>> {
|
|
45
|
+
// Step 1: Get provider
|
|
46
|
+
const provider = providerRegistry.getActiveProvider();
|
|
47
|
+
if (!provider) {
|
|
48
|
+
this.logError("No AI provider configured");
|
|
49
|
+
return failure("No AI provider configured");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Step 2: Check initialization
|
|
53
|
+
if (!provider.isInitialized()) {
|
|
54
|
+
this.logError("AI provider not initialized");
|
|
55
|
+
return failure("AI provider not initialized");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Step 3: Validate request (subclass-specific)
|
|
59
|
+
const validationError = this.validateRequest(request);
|
|
60
|
+
if (validationError) {
|
|
61
|
+
this.logError(validationError);
|
|
62
|
+
return failure(validationError);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 4: Log execution start
|
|
66
|
+
this.logInfo(`Provider: ${provider.providerId}, Model: ${options.model}`);
|
|
67
|
+
|
|
68
|
+
// Step 5: Execute with error handling
|
|
69
|
+
try {
|
|
70
|
+
// Build input
|
|
71
|
+
const input = options.buildInput(request);
|
|
72
|
+
|
|
73
|
+
// Execute provider call (subclass-specific)
|
|
74
|
+
// Provider handles all progress reporting via callbacks
|
|
75
|
+
const providerResult = await this.executeProvider(
|
|
76
|
+
provider,
|
|
77
|
+
options.model,
|
|
78
|
+
input,
|
|
79
|
+
options.onProgress,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Extract result
|
|
83
|
+
const extractor = options.extractResult || this.getDefaultExtractor();
|
|
84
|
+
const extracted = extractor(providerResult);
|
|
85
|
+
|
|
86
|
+
// Validate extracted result (subclass-specific)
|
|
87
|
+
const resultValidationError = this.validateExtractedResult(extracted);
|
|
88
|
+
if (resultValidationError) {
|
|
89
|
+
this.logError(resultValidationError);
|
|
90
|
+
return failure(resultValidationError);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Transform to final result (subclass-specific)
|
|
94
|
+
return success(this.transformResult(extracted as TOutput));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
this.logError(`Error: ${message}`);
|
|
98
|
+
return failure(message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if feature is supported by current provider
|
|
104
|
+
*/
|
|
105
|
+
public hasSupport(): boolean {
|
|
106
|
+
const provider = providerRegistry.getActiveProvider();
|
|
107
|
+
return provider !== null && provider.isInitialized();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ==================== Abstract Methods (Must Override) ====================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate the incoming request
|
|
114
|
+
* Return error message if invalid, undefined if valid
|
|
115
|
+
*/
|
|
116
|
+
protected abstract validateRequest(request: TRequest): string | undefined;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute the provider call (run or subscribe)
|
|
120
|
+
* Subclasses implement their specific provider interaction
|
|
121
|
+
*/
|
|
122
|
+
protected abstract executeProvider(
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
provider: any,
|
|
125
|
+
model: string,
|
|
126
|
+
input: unknown,
|
|
127
|
+
onProgress?: (progress: number) => void,
|
|
128
|
+
): Promise<unknown>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate the extracted result
|
|
132
|
+
* Return error message if invalid, undefined if valid
|
|
133
|
+
*/
|
|
134
|
+
protected abstract validateExtractedResult(
|
|
135
|
+
extracted: TOutput | undefined,
|
|
136
|
+
): string | undefined;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Transform extracted output to final result type
|
|
140
|
+
*/
|
|
141
|
+
protected abstract transformResult(extracted: TOutput): TResult;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get default result extractor for this executor type
|
|
145
|
+
*/
|
|
146
|
+
protected abstract getDefaultExtractor(): (
|
|
147
|
+
result: unknown,
|
|
148
|
+
) => TOutput | undefined;
|
|
149
|
+
|
|
150
|
+
// ==================== Utility Methods ====================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Log info message (development only)
|
|
154
|
+
*/
|
|
155
|
+
protected logInfo(message: string): void {
|
|
156
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
157
|
+
console.log(`[${this.logPrefix}] ${message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Log error message (development only)
|
|
163
|
+
*/
|
|
164
|
+
protected logError(message: string): void {
|
|
165
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
166
|
+
console.error(`[${this.logPrefix}] ${message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|