@umituz/react-native-ai-pruna-provider 1.0.63 → 1.0.65
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 -4
- package/src/application/dto/pruna.dto.ts +58 -0
- package/src/application/services/pruna-service.ts +81 -0
- package/src/application/use-cases/generate-image-edit.use-case.ts +165 -0
- package/src/application/use-cases/generate-image.use-case.ts +125 -0
- package/src/application/use-cases/generate-video.use-case.ts +147 -0
- package/src/domain/services/error-mapper.domain-service.ts +204 -0
- package/src/domain/services/validation.domain-service.ts +93 -0
- package/src/domain/value-objects/api-key.value.ts +30 -0
- package/src/domain/value-objects/model-id.value.ts +41 -0
- package/src/domain/value-objects/session-id.value.ts +22 -0
- package/src/index.new.ts +65 -0
- package/src/infrastructure/api/http-client.ts +111 -0
- package/src/infrastructure/logging/pruna-logger.ts +100 -0
- package/src/infrastructure/storage/file-storage.ts +97 -0
- package/src/infrastructure/utils/calculation.utils.ts +2 -55
- package/src/presentation/hooks/use-pruna-generation.new.ts +182 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Storage Infrastructure
|
|
3
|
+
* Handles file uploads to Pruna's storage service
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { httpClient } from "../api/http-client";
|
|
7
|
+
import { logger } from "../logging/pruna-logger";
|
|
8
|
+
import { PRUNA_FILES_URL, UPLOAD_CONFIG } from "../../services/pruna-provider.constants";
|
|
9
|
+
|
|
10
|
+
export class FileStorageService {
|
|
11
|
+
async uploadFile(
|
|
12
|
+
base64Data: string,
|
|
13
|
+
apiKey: string,
|
|
14
|
+
sessionId: string
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const log = logger;
|
|
17
|
+
|
|
18
|
+
// Validation
|
|
19
|
+
if (!base64Data?.trim()) {
|
|
20
|
+
log.error(sessionId, 'file-storage', 'Empty file data');
|
|
21
|
+
throw new Error("File data is empty. Provide a base64 string or URL.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Already a URL
|
|
25
|
+
if (base64Data.startsWith('http')) {
|
|
26
|
+
log.info(sessionId, 'file-storage', 'File already a URL', {
|
|
27
|
+
url: base64Data.substring(0, 80) + '...',
|
|
28
|
+
});
|
|
29
|
+
return base64Data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Process base64
|
|
33
|
+
const rawBase64 = this.extractBase64(base64Data);
|
|
34
|
+
const dataUri = this.createDataUri(rawBase64);
|
|
35
|
+
const formData = this.createFormData(dataUri);
|
|
36
|
+
|
|
37
|
+
log.info(sessionId, 'file-storage', 'Uploading file', {
|
|
38
|
+
size: Math.round(rawBase64.length / 1024) + 'KB',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await httpClient.request<{ urls?: { get?: string }; id?: string }>(
|
|
43
|
+
{
|
|
44
|
+
url: PRUNA_FILES_URL,
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { apikey: apiKey },
|
|
47
|
+
body: formData,
|
|
48
|
+
timeout: UPLOAD_CONFIG.timeoutMs,
|
|
49
|
+
},
|
|
50
|
+
sessionId,
|
|
51
|
+
'file-storage'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const fileUrl = response.data.urls?.get ||
|
|
55
|
+
(response.data.id ? `${PRUNA_FILES_URL}/${response.data.id}` : PRUNA_FILES_URL);
|
|
56
|
+
|
|
57
|
+
log.info(sessionId, 'file-storage', 'Upload complete', {
|
|
58
|
+
url: fileUrl.substring(0, 80) + '...',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return fileUrl;
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
log.error(sessionId, 'file-storage', 'Upload failed', {
|
|
65
|
+
error: error instanceof Error ? error.message : String(error),
|
|
66
|
+
});
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private extractBase64(data: string): string {
|
|
72
|
+
const base64Index = data.indexOf('base64,');
|
|
73
|
+
return base64Index !== -1 ? data.substring(base64Index + 7) : data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private createDataUri(base64: string): string {
|
|
77
|
+
return `data:image/jpeg;base64,${base64}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private createFormData(dataUri: string): FormData {
|
|
81
|
+
const formData = new FormData();
|
|
82
|
+
const timestamp = Date.now();
|
|
83
|
+
const randomId = Math.random().toString(36).substring(2, 8);
|
|
84
|
+
const fileName = `vivoim_${timestamp}_${randomId}.jpg`;
|
|
85
|
+
|
|
86
|
+
(formData as unknown as { append: (name: string, value: { uri: string; type: string; name: string }) => void })
|
|
87
|
+
.append('content', {
|
|
88
|
+
uri: dataUri,
|
|
89
|
+
type: 'image/jpeg',
|
|
90
|
+
name: fileName,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return formData;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const fileStorageService = new FileStorageService();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Calculation Utilities
|
|
3
|
-
*
|
|
3
|
+
* Internal calculation functions for Pruna provider
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { DEFAULT_PRUNA_CONFIG } from "../services/pruna-provider.constants";
|
|
@@ -21,7 +21,7 @@ export function bytesToKB(bytes: number): number {
|
|
|
21
21
|
/**
|
|
22
22
|
* Converts bytes to megabytes (MB)
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
function bytesToMB(bytes: number): number {
|
|
25
25
|
return Math.round(bytes / 1024 / 1024);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -32,17 +32,6 @@ export function calculateElapsedMs(startTime: number): number {
|
|
|
32
32
|
return Date.now() - startTime;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Formats elapsed time in human-readable format
|
|
37
|
-
*/
|
|
38
|
-
export function formatElapsedMs(ms: number): string {
|
|
39
|
-
if (ms < 1000) return `${ms}ms`;
|
|
40
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
41
|
-
const minutes = Math.floor(ms / 60000);
|
|
42
|
-
const seconds = Math.floor((ms % 60000) / 1000);
|
|
43
|
-
return `${minutes}m ${seconds}s`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
35
|
/**
|
|
47
36
|
* Creates a preview of a string by truncating and adding ellipsis
|
|
48
37
|
*/
|
|
@@ -51,15 +40,6 @@ export function createStringPreview(str: string, maxLength: number = DEFAULT_MAX
|
|
|
51
40
|
return `${str.substring(0, maxLength)}...`;
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
/**
|
|
55
|
-
* Creates a size preview (e.g., "2.5 MB", "1024 KB")
|
|
56
|
-
*/
|
|
57
|
-
export function formatSize(bytes: number): string {
|
|
58
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
59
|
-
if (bytes < 1024 * 1024) return `${bytesToKB(bytes)} KB`;
|
|
60
|
-
return `${bytesToMB(bytes)} MB`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
43
|
/**
|
|
64
44
|
* Calculates percentage with specified precision
|
|
65
45
|
*/
|
|
@@ -68,25 +48,6 @@ export function calculatePercentage(value: number, total: number, precision: num
|
|
|
68
48
|
return Number(((value / total) * 100).toFixed(precision));
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
/**
|
|
72
|
-
* Clamps a number between min and max values
|
|
73
|
-
*/
|
|
74
|
-
export function clamp(value: number, min: number, max: number): number {
|
|
75
|
-
return Math.min(Math.max(value, min), max);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Calculates retry delay with exponential backoff
|
|
80
|
-
*/
|
|
81
|
-
export function calculateRetryDelay(
|
|
82
|
-
attempt: number,
|
|
83
|
-
baseDelayMs: number,
|
|
84
|
-
maxDelayMs: number
|
|
85
|
-
): number {
|
|
86
|
-
const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
87
|
-
return Math.round(delay);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
51
|
/**
|
|
91
52
|
* Validates if a timeout value is within acceptable range
|
|
92
53
|
*/
|
|
@@ -97,17 +58,3 @@ export function isValidTimeout(timeoutMs: number): boolean {
|
|
|
97
58
|
timeoutMs <= MAX_TIMEOUT_MS
|
|
98
59
|
);
|
|
99
60
|
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Calculates hash of a string for request deduplication
|
|
103
|
-
* Uses base64 encoding for collision resistance
|
|
104
|
-
*/
|
|
105
|
-
export function calculateRequestHash(input: string): string {
|
|
106
|
-
// Replace non-alphanumeric chars with underscores for safe string representation
|
|
107
|
-
const safeInput = input.replace(/[^a-zA-Z0-9]/g, '_');
|
|
108
|
-
|
|
109
|
-
// Use first 64 chars + last 64 chars to keep key length manageable
|
|
110
|
-
if (safeInput.length <= 128) return safeInput;
|
|
111
|
-
|
|
112
|
-
return `${safeInput.substring(0, 64)}...${safeInput.slice(-64)}`;
|
|
113
|
-
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna Generation Hook (Refactored)
|
|
3
|
+
* Clean React hook using new DDD architecture
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef } from 'react';
|
|
7
|
+
import { prunaService } from '../../application/services/pruna-service';
|
|
8
|
+
import type {
|
|
9
|
+
PrunaImageGenerationRequest,
|
|
10
|
+
PrunaVideoGenerationRequest,
|
|
11
|
+
PrunaImageEditRequest,
|
|
12
|
+
PrunaGenerationResponse,
|
|
13
|
+
PrunaGenerationError,
|
|
14
|
+
} from '../../application/dto/pruna.dto';
|
|
15
|
+
|
|
16
|
+
export interface PrunaGenerationState {
|
|
17
|
+
isGenerating: boolean;
|
|
18
|
+
progress: number;
|
|
19
|
+
status: string;
|
|
20
|
+
error: PrunaGenerationError | null;
|
|
21
|
+
result: PrunaGenerationResponse | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function usePrunaGeneration() {
|
|
25
|
+
const [state, setState] = useState<PrunaGenerationState>({
|
|
26
|
+
isGenerating: false,
|
|
27
|
+
progress: 0,
|
|
28
|
+
status: 'idle',
|
|
29
|
+
error: null,
|
|
30
|
+
result: null,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
34
|
+
|
|
35
|
+
const generateImage = useCallback(async (request: PrunaImageGenerationRequest) => {
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
abortControllerRef.current = controller;
|
|
38
|
+
|
|
39
|
+
setState({
|
|
40
|
+
isGenerating: true,
|
|
41
|
+
progress: 0,
|
|
42
|
+
status: 'starting',
|
|
43
|
+
error: null,
|
|
44
|
+
result: null,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await prunaService.generateImage(request, controller.signal);
|
|
49
|
+
setState({
|
|
50
|
+
isGenerating: false,
|
|
51
|
+
progress: 100,
|
|
52
|
+
status: 'completed',
|
|
53
|
+
error: null,
|
|
54
|
+
result: response,
|
|
55
|
+
});
|
|
56
|
+
return response;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const errorObj: PrunaGenerationError = {
|
|
59
|
+
type: 'unknown',
|
|
60
|
+
message: error instanceof Error ? error.message : String(error),
|
|
61
|
+
retryable: false,
|
|
62
|
+
originalError: error instanceof Error ? error.stack : undefined,
|
|
63
|
+
};
|
|
64
|
+
setState({
|
|
65
|
+
isGenerating: false,
|
|
66
|
+
progress: 0,
|
|
67
|
+
status: 'error',
|
|
68
|
+
error: errorObj,
|
|
69
|
+
result: null,
|
|
70
|
+
});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const generateVideo = useCallback(async (request: PrunaVideoGenerationRequest) => {
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
abortControllerRef.current = controller;
|
|
78
|
+
|
|
79
|
+
setState({
|
|
80
|
+
isGenerating: true,
|
|
81
|
+
progress: 0,
|
|
82
|
+
status: 'starting',
|
|
83
|
+
error: null,
|
|
84
|
+
result: null,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await prunaService.generateVideo(request, controller.signal);
|
|
89
|
+
setState({
|
|
90
|
+
isGenerating: false,
|
|
91
|
+
progress: 100,
|
|
92
|
+
status: 'completed',
|
|
93
|
+
error: null,
|
|
94
|
+
result: response,
|
|
95
|
+
});
|
|
96
|
+
return response;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const errorObj: PrunaGenerationError = {
|
|
99
|
+
type: 'unknown',
|
|
100
|
+
message: error instanceof Error ? error.message : String(error),
|
|
101
|
+
retryable: false,
|
|
102
|
+
originalError: error instanceof Error ? error.stack : undefined,
|
|
103
|
+
};
|
|
104
|
+
setState({
|
|
105
|
+
isGenerating: false,
|
|
106
|
+
progress: 0,
|
|
107
|
+
status: 'error',
|
|
108
|
+
error: errorObj,
|
|
109
|
+
result: null,
|
|
110
|
+
});
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const generateImageEdit = useCallback(async (request: PrunaImageEditRequest) => {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
abortControllerRef.current = controller;
|
|
118
|
+
|
|
119
|
+
setState({
|
|
120
|
+
isGenerating: true,
|
|
121
|
+
progress: 0,
|
|
122
|
+
status: 'starting',
|
|
123
|
+
error: null,
|
|
124
|
+
result: null,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const response = await prunaService.generateImageEdit(request, controller.signal);
|
|
129
|
+
setState({
|
|
130
|
+
isGenerating: false,
|
|
131
|
+
progress: 100,
|
|
132
|
+
status: 'completed',
|
|
133
|
+
error: null,
|
|
134
|
+
result: response,
|
|
135
|
+
});
|
|
136
|
+
return response;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const errorObj: PrunaGenerationError = {
|
|
139
|
+
type: 'unknown',
|
|
140
|
+
message: error instanceof Error ? error.message : String(error),
|
|
141
|
+
retryable: false,
|
|
142
|
+
originalError: error instanceof Error ? error.stack : undefined,
|
|
143
|
+
};
|
|
144
|
+
setState({
|
|
145
|
+
isGenerating: false,
|
|
146
|
+
progress: 0,
|
|
147
|
+
status: 'error',
|
|
148
|
+
error: errorObj,
|
|
149
|
+
result: null,
|
|
150
|
+
});
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const cancel = useCallback(() => {
|
|
156
|
+
abortControllerRef.current?.abort();
|
|
157
|
+
setState(prev => ({
|
|
158
|
+
...prev,
|
|
159
|
+
isGenerating: false,
|
|
160
|
+
status: 'cancelled',
|
|
161
|
+
}));
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const reset = useCallback(() => {
|
|
165
|
+
setState({
|
|
166
|
+
isGenerating: false,
|
|
167
|
+
progress: 0,
|
|
168
|
+
status: 'idle',
|
|
169
|
+
error: null,
|
|
170
|
+
result: null,
|
|
171
|
+
});
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
state,
|
|
176
|
+
generateImage,
|
|
177
|
+
generateVideo,
|
|
178
|
+
generateImageEdit,
|
|
179
|
+
cancel,
|
|
180
|
+
reset,
|
|
181
|
+
};
|
|
182
|
+
}
|