@umituz/react-native-ai-pruna-provider 1.0.64 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-pruna-provider",
3
- "version": "1.0.64",
3
+ "version": "1.0.65",
4
4
  "description": "Pruna AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Pruna DTOs
3
+ * Data transfer objects for Pruna operations
4
+ */
5
+
6
+ export interface PrunaImageGenerationRequest {
7
+ prompt: string;
8
+ aspectRatio?: '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
9
+ seed?: number;
10
+ disableSafetyChecker?: boolean;
11
+ width?: number;
12
+ height?: number;
13
+ }
14
+
15
+ export interface PrunaVideoGenerationRequest {
16
+ image: string;
17
+ prompt: string;
18
+ aspectRatio?: '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
19
+ duration?: number;
20
+ resolution?: '720p' | '1080p';
21
+ fps?: number;
22
+ draft?: boolean;
23
+ promptUpsampling?: boolean;
24
+ audio?: string;
25
+ disableSafetyChecker?: boolean;
26
+ }
27
+
28
+ export interface PrunaImageEditRequest {
29
+ images?: string[];
30
+ image?: string;
31
+ imageUrl?: string;
32
+ imageUrls?: string[];
33
+ prompt: string;
34
+ aspectRatio?: '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
35
+ seed?: number;
36
+ disableSafetyChecker?: boolean;
37
+ width?: number;
38
+ height?: number;
39
+ }
40
+
41
+ export interface PrunaGenerationResponse {
42
+ url: string;
43
+ requestId: string;
44
+ sessionId?: string;
45
+ }
46
+
47
+ export interface PrunaGenerationOptions {
48
+ timeout?: number;
49
+ onProgress?: (progress: number, status: string) => void;
50
+ onQueueUpdate?: (update: { status: string; requestId: string }) => void;
51
+ }
52
+
53
+ export interface PrunaGenerationError {
54
+ type: string;
55
+ message: string;
56
+ retryable: boolean;
57
+ originalError?: string;
58
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Pruna Application Service
3
+ * Orchestrates use cases and provides high-level API
4
+ */
5
+
6
+ import { ApiKey } from "../../domain/value-objects/api-key.value";
7
+ import { ModelId } from "../../domain/value-objects/model-id.value";
8
+ import { ValidationService } from "../../domain/services/validation.domain-service";
9
+ import { logger } from "../../infrastructure/logging/pruna-logger";
10
+ import { generateImageUseCase, GenerateImageInput } from "../use-cases/generate-image.use-case";
11
+ import { generateVideoUseCase, GenerateVideoInput } from "../use-cases/generate-video.use-case";
12
+ import { generateImageEditUseCase, GenerateImageEditInput } from "../use-cases/generate-image-edit.use-case";
13
+
14
+ export interface PrunaConfig {
15
+ apiKey: string;
16
+ maxTimeoutMs?: number;
17
+ }
18
+
19
+ export type PrunaModel = 'p-image' | 'p-image-edit' | 'p-video';
20
+
21
+ export class PrunaService {
22
+ private apiKey: ApiKey | null = null;
23
+ private initialized = false;
24
+
25
+ initialize(config: PrunaConfig): void {
26
+ const validation = ValidationService.validateApiKey(config.apiKey);
27
+ if (!validation.isValid) {
28
+ throw new Error(validation.error);
29
+ }
30
+
31
+ this.apiKey = ApiKey.create(config.apiKey);
32
+ this.initialized = true;
33
+ }
34
+
35
+ isInitialized(): boolean {
36
+ return this.initialized;
37
+ }
38
+
39
+ private ensureInitialized(): ApiKey {
40
+ if (!this.apiKey || !this.initialized) {
41
+ throw new Error("Pruna service not initialized. Call initialize() first.");
42
+ }
43
+ return this.apiKey;
44
+ }
45
+
46
+ async generateImage(
47
+ input: GenerateImageInput,
48
+ signal?: AbortSignal
49
+ ): Promise<{ imageUrl: string; requestId: string }> {
50
+ const apiKey = this.ensureInitialized();
51
+ return generateImageUseCase.execute(input, apiKey.toString(), signal);
52
+ }
53
+
54
+ async generateVideo(
55
+ input: GenerateVideoInput,
56
+ signal?: AbortSignal
57
+ ): Promise<{ videoUrl: string; requestId: string }> {
58
+ const apiKey = this.ensureInitialized();
59
+ return generateVideoUseCase.execute(input, apiKey.toString(), signal);
60
+ }
61
+
62
+ async generateImageEdit(
63
+ input: GenerateImageEditInput,
64
+ signal?: AbortSignal
65
+ ): Promise<{ imageUrl: string; requestId: string }> {
66
+ const apiKey = this.ensureInitialized();
67
+ return generateImageEditUseCase.execute(input, apiKey.toString(), signal);
68
+ }
69
+
70
+ reset(): void {
71
+ this.apiKey = null;
72
+ this.initialized = false;
73
+ }
74
+
75
+ getSessionLogs(sessionId: string): unknown[] {
76
+ // Return logs for debugging
77
+ return [];
78
+ }
79
+ }
80
+
81
+ export const prunaService = new PrunaService();
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Generate Image Edit Use Case
3
+ * Handles image-to-image editing using Pruna p-image-edit model
4
+ */
5
+
6
+ import { ModelId } from "../../domain/value-objects/model-id.value";
7
+ import { ValidationService } from "../../domain/services/validation.domain-service";
8
+ import { logger } from "../../infrastructure/logging/pruna-logger";
9
+ import { fileStorageService } from "../../infrastructure/storage/file-storage";
10
+ import { PRUNA_PREDICTIONS_URL } from "../../infrastructure/services/pruna-provider.constants";
11
+ import { httpClient } from "../../infrastructure/api/http-client";
12
+
13
+ export interface GenerateImageEditInput {
14
+ images?: string[];
15
+ image?: string;
16
+ imageUrl?: string;
17
+ imageUrls?: string[];
18
+ prompt: string;
19
+ aspectRatio?: string;
20
+ seed?: number;
21
+ disableSafetyChecker?: boolean;
22
+ width?: number;
23
+ height?: number;
24
+ }
25
+
26
+ export interface GenerateImageEditOutput {
27
+ imageUrl: string;
28
+ requestId: string;
29
+ }
30
+
31
+ export class GenerateImageEditUseCase {
32
+ async execute(
33
+ input: GenerateImageEditInput,
34
+ apiKey: string,
35
+ signal?: AbortSignal
36
+ ): Promise<GenerateImageEditOutput> {
37
+ const sessionId = logger.createSession();
38
+ const log = logger;
39
+
40
+ try {
41
+ // Validate input
42
+ const promptValidation = ValidationService.validatePrompt(input.prompt);
43
+ if (!promptValidation.isValid) {
44
+ throw new Error(promptValidation.error);
45
+ }
46
+
47
+ // Extract images
48
+ const rawImages = this.extractImages(input);
49
+ const imageValidation = ValidationService.validateImageData(rawImages);
50
+ if (!imageValidation.isValid) {
51
+ throw new Error(imageValidation.error);
52
+ }
53
+
54
+ log.info(sessionId, 'image-edit', 'Starting image edit', {
55
+ imageCount: rawImages.length,
56
+ promptLength: input.prompt.length,
57
+ });
58
+
59
+ // Upload images in parallel
60
+ const imageUrls = await Promise.all(
61
+ rawImages.map(img => fileStorageService.uploadFile(img, apiKey, sessionId))
62
+ );
63
+
64
+ log.info(sessionId, 'image-edit', 'All images uploaded', {
65
+ count: imageUrls.length,
66
+ });
67
+
68
+ // Build payload
69
+ const payload = this.buildPayload(input, imageUrls);
70
+ const modelId = ModelId.create('p-image-edit');
71
+
72
+ // Submit prediction
73
+ const response = await httpClient.request<{
74
+ generation_url?: string;
75
+ output?: { url?: string } | string;
76
+ get_url?: string;
77
+ status_url?: string;
78
+ }>(
79
+ {
80
+ url: PRUNA_PREDICTIONS_URL,
81
+ method: 'POST',
82
+ headers: {
83
+ apikey: apiKey,
84
+ Model: modelId.toString(),
85
+ 'Try-Sync': 'true',
86
+ 'Content-Type': 'application/json',
87
+ },
88
+ body: JSON.stringify({ input: payload }),
89
+ signal,
90
+ },
91
+ sessionId,
92
+ 'image-edit'
93
+ );
94
+
95
+ const imageUrl = this.extractResultUrl(response.data);
96
+ const requestId = `edit_${Date.now()}`;
97
+
98
+ log.info(sessionId, 'image-edit', 'Edit complete', {
99
+ requestId,
100
+ imageUrl: imageUrl.substring(0, 80) + '...',
101
+ });
102
+
103
+ return { imageUrl, requestId };
104
+
105
+ } finally {
106
+ logger.endSession(sessionId);
107
+ }
108
+ }
109
+
110
+ private extractImages(input: GenerateImageEditInput): string[] {
111
+ if (Array.isArray(input.images) && input.images.length > 0) {
112
+ return input.images.filter(img => typeof img === 'string' && img.trim().length > 0);
113
+ }
114
+
115
+ if (typeof input.image === 'string' && input.image.trim().length > 0) {
116
+ return [input.image];
117
+ }
118
+
119
+ if (typeof input.imageUrl === 'string' && input.imageUrl.trim().length > 0) {
120
+ return [input.imageUrl];
121
+ }
122
+
123
+ if (Array.isArray(input.imageUrls) && input.imageUrls.length > 0) {
124
+ return input.imageUrls.filter(url => typeof url === 'string' && url.trim().length > 0);
125
+ }
126
+
127
+ throw new Error("No valid images provided. Use 'image', 'images', 'imageUrl', or 'imageUrls'.");
128
+ }
129
+
130
+ private buildPayload(
131
+ input: GenerateImageEditInput,
132
+ imageUrls: string[]
133
+ ): Record<string, unknown> {
134
+ const payload: Record<string, unknown> = {
135
+ images: imageUrls,
136
+ prompt: input.prompt,
137
+ aspect_ratio: input.aspectRatio || '1:1',
138
+ };
139
+
140
+ // Add optional parameters
141
+ if (input.seed !== undefined) payload.seed = input.seed;
142
+ if (input.disableSafetyChecker !== undefined) {
143
+ payload.disable_safety_checker = input.disableSafetyChecker;
144
+ }
145
+ if (input.width !== undefined) payload.width = input.width;
146
+ if (input.height !== undefined) payload.height = input.height;
147
+
148
+ return payload;
149
+ }
150
+
151
+ private extractResultUrl(data: Record<string, unknown>): string {
152
+ const url = data.generation_url ||
153
+ (data.output && typeof data.output === 'object' ? (data.output as { url?: string }).url : null) ||
154
+ (typeof data.output === 'string' ? data.output : null) ||
155
+ data.data;
156
+
157
+ if (!url || typeof url !== 'string') {
158
+ throw new Error('No image URL in response');
159
+ }
160
+
161
+ return url.startsWith('/') ? `https://api.pruna.ai${url}` : url;
162
+ }
163
+ }
164
+
165
+ export const generateImageEditUseCase = new GenerateImageEditUseCase();
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Generate Image Use Case
3
+ * Handles text-to-image generation using Pruna p-image model
4
+ */
5
+
6
+ import { ModelId } from "../../domain/value-objects/model-id.value";
7
+ import { ValidationService } from "../../domain/services/validation.domain-service";
8
+ import { logger } from "../../infrastructure/logging/pruna-logger";
9
+ import { PRUNA_PREDICTIONS_URL } from "../../infrastructure/services/pruna-provider.constants";
10
+ import { httpClient } from "../../infrastructure/api/http-client";
11
+
12
+ export interface GenerateImageInput {
13
+ prompt: string;
14
+ aspectRatio?: string;
15
+ seed?: number;
16
+ disableSafetyChecker?: boolean;
17
+ width?: number;
18
+ height?: number;
19
+ }
20
+
21
+ export interface GenerateImageOutput {
22
+ imageUrl: string;
23
+ requestId: string;
24
+ }
25
+
26
+ export class GenerateImageUseCase {
27
+ async execute(
28
+ input: GenerateImageInput,
29
+ apiKey: string,
30
+ signal?: AbortSignal
31
+ ): Promise<GenerateImageOutput> {
32
+ // Create session for logging
33
+ const sessionId = logger.createSession();
34
+ const log = logger;
35
+
36
+ try {
37
+ // Validate input
38
+ const promptValidation = ValidationService.validatePrompt(input.prompt);
39
+ if (!promptValidation.isValid) {
40
+ log.error(sessionId, 'generate-image', 'Validation failed', {
41
+ error: promptValidation.error,
42
+ });
43
+ throw new Error(promptValidation.error);
44
+ }
45
+
46
+ // Build request payload
47
+ const payload = this.buildPayload(input);
48
+ const modelId = ModelId.create('p-image');
49
+
50
+ log.info(sessionId, 'generate-image', 'Starting image generation', {
51
+ promptLength: input.prompt.length,
52
+ aspectRatio: input.aspectRatio,
53
+ });
54
+
55
+ // Submit prediction
56
+ const response = await httpClient.request<{
57
+ generation_url?: string;
58
+ output?: { url?: string } | string;
59
+ get_url?: string;
60
+ status_url?: string;
61
+ }>(
62
+ {
63
+ url: PRUNA_PREDICTIONS_URL,
64
+ method: 'POST',
65
+ headers: {
66
+ apikey: apiKey,
67
+ Model: modelId.toString(),
68
+ 'Try-Sync': 'true',
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify({ input: payload }),
72
+ signal,
73
+ },
74
+ sessionId,
75
+ 'generate-image'
76
+ );
77
+
78
+ // Extract result URL
79
+ const imageUrl = this.extractResultUrl(response.data);
80
+ const requestId = `img_${Date.now()}`;
81
+
82
+ log.info(sessionId, 'generate-image', 'Generation complete', {
83
+ requestId,
84
+ imageUrl: imageUrl.substring(0, 80) + '...',
85
+ });
86
+
87
+ return { imageUrl, requestId };
88
+
89
+ } finally {
90
+ logger.endSession(sessionId);
91
+ }
92
+ }
93
+
94
+ private buildPayload(input: GenerateImageInput): Record<string, unknown> {
95
+ const payload: Record<string, unknown> = {
96
+ prompt: input.prompt,
97
+ aspect_ratio: input.aspectRatio || '1:1',
98
+ };
99
+
100
+ // Add optional parameters
101
+ if (input.seed !== undefined) payload.seed = input.seed;
102
+ if (input.disableSafetyChecker !== undefined) {
103
+ payload.disable_safety_checker = input.disableSafetyChecker;
104
+ }
105
+ if (input.width !== undefined) payload.width = input.width;
106
+ if (input.height !== undefined) payload.height = input.height;
107
+
108
+ return payload;
109
+ }
110
+
111
+ private extractResultUrl(data: Record<string, unknown>): string {
112
+ const url = data.generation_url ||
113
+ (data.output && typeof data.output === 'object' ? (data.output as { url?: string }).url : null) ||
114
+ (typeof data.output === 'string' ? data.output : null) ||
115
+ data.data;
116
+
117
+ if (!url || typeof url !== 'string') {
118
+ throw new Error('No image URL in response');
119
+ }
120
+
121
+ return url.startsWith('/') ? `https://api.pruna.ai${url}` : url;
122
+ }
123
+ }
124
+
125
+ export const generateImageUseCase = new GenerateImageUseCase();
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Generate Video Use Case
3
+ * Handles image-to-video generation using Pruna p-video model
4
+ */
5
+
6
+ import { ModelId } from "../../domain/value-objects/model-id.value";
7
+ import { ValidationService } from "../../domain/services/validation.domain-service";
8
+ import { logger } from "../../infrastructure/logging/pruna-logger";
9
+ import { fileStorageService } from "../../infrastructure/storage/file-storage";
10
+ import { PRUNA_PREDICTIONS_URL, P_VIDEO_DEFAULTS } from "../../infrastructure/services/pruna-provider.constants";
11
+ import { httpClient } from "../../infrastructure/api/http-client";
12
+
13
+ export interface GenerateVideoInput {
14
+ image: string;
15
+ prompt: string;
16
+ aspectRatio?: string;
17
+ duration?: number;
18
+ resolution?: string;
19
+ fps?: number;
20
+ draft?: boolean;
21
+ promptUpsampling?: boolean;
22
+ audio?: string;
23
+ disableSafetyChecker?: boolean;
24
+ }
25
+
26
+ export interface GenerateVideoOutput {
27
+ videoUrl: string;
28
+ requestId: string;
29
+ }
30
+
31
+ export class GenerateVideoUseCase {
32
+ async execute(
33
+ input: GenerateVideoInput,
34
+ apiKey: string,
35
+ signal?: AbortSignal
36
+ ): Promise<GenerateVideoOutput> {
37
+ const sessionId = logger.createSession();
38
+ const log = logger;
39
+
40
+ try {
41
+ // Validate input
42
+ const promptValidation = ValidationService.validatePrompt(input.prompt);
43
+ if (!promptValidation.isValid) {
44
+ throw new Error(promptValidation.error);
45
+ }
46
+
47
+ const imageValidation = ValidationService.validateImageData(input.image);
48
+ if (!imageValidation.isValid) {
49
+ throw new Error(imageValidation.error);
50
+ }
51
+
52
+ log.info(sessionId, 'generate-video', 'Starting video generation', {
53
+ promptLength: input.prompt.length,
54
+ hasImage: !!input.image,
55
+ hasAudio: !!input.audio,
56
+ });
57
+
58
+ // Upload image if needed
59
+ const imageUrl = await fileStorageService.uploadFile(input.image, apiKey, sessionId);
60
+
61
+ // Upload audio if provided
62
+ let audioUrl: string | undefined;
63
+ if (input.audio) {
64
+ audioUrl = await fileStorageService.uploadFile(input.audio, apiKey, sessionId);
65
+ }
66
+
67
+ // Build payload
68
+ const payload = this.buildPayload(input, imageUrl, audioUrl);
69
+ const modelId = ModelId.create('p-video');
70
+
71
+ // Submit prediction
72
+ const response = await httpClient.request<{
73
+ video_url?: string;
74
+ output?: { url?: string } | string;
75
+ get_url?: string;
76
+ status_url?: string;
77
+ }>(
78
+ {
79
+ url: PRUNA_PREDICTIONS_URL,
80
+ method: 'POST',
81
+ headers: {
82
+ apikey: apiKey,
83
+ Model: modelId.toString(),
84
+ 'Try-Sync': 'true',
85
+ 'Content-Type': 'application/json',
86
+ },
87
+ body: JSON.stringify({ input: payload }),
88
+ signal,
89
+ },
90
+ sessionId,
91
+ 'generate-video'
92
+ );
93
+
94
+ const videoUrl = this.extractResultUrl(response.data);
95
+ const requestId = `video_${Date.now()}`;
96
+
97
+ log.info(sessionId, 'generate-video', 'Generation complete', {
98
+ requestId,
99
+ videoUrl: videoUrl.substring(0, 80) + '...',
100
+ });
101
+
102
+ return { videoUrl, requestId };
103
+
104
+ } finally {
105
+ logger.endSession(sessionId);
106
+ }
107
+ }
108
+
109
+ private buildPayload(
110
+ input: GenerateVideoInput,
111
+ imageUrl: string,
112
+ audioUrl?: string
113
+ ): Record<string, unknown> {
114
+ const payload: Record<string, unknown> = {
115
+ image: imageUrl,
116
+ prompt: input.prompt,
117
+ aspect_ratio: input.aspectRatio || '1:1',
118
+ duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
119
+ resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
120
+ fps: input.fps ?? P_VIDEO_DEFAULTS.fps,
121
+ draft: input.draft ?? P_VIDEO_DEFAULTS.draft,
122
+ prompt_upsampling: input.promptUpsampling ?? P_VIDEO_DEFAULTS.promptUpsampling,
123
+ };
124
+
125
+ if (audioUrl) payload.audio = audioUrl;
126
+ if (input.disableSafetyChecker !== undefined) {
127
+ payload.disable_safety_checker = input.disableSafetyChecker;
128
+ }
129
+
130
+ return payload;
131
+ }
132
+
133
+ private extractResultUrl(data: Record<string, unknown>): string {
134
+ const url = data.video_url ||
135
+ (data.output && typeof data.output === 'object' ? (data.output as { url?: string }).url : null) ||
136
+ (typeof data.output === 'string' ? data.output : null) ||
137
+ data.data;
138
+
139
+ if (!url || typeof url !== 'string') {
140
+ throw new Error('No video URL in response');
141
+ }
142
+
143
+ return url.startsWith('/') ? `https://api.pruna.ai${url}` : url;
144
+ }
145
+ }
146
+
147
+ export const generateVideoUseCase = new GenerateVideoUseCase();