@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-pruna-provider",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
@@ -43,9 +43,6 @@
|
|
|
43
43
|
"@gorhom/bottom-sheet": "^5.2.8",
|
|
44
44
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
45
45
|
"@react-native-community/slider": "^5.1.2",
|
|
46
|
-
"@react-navigation/bottom-tabs": "^7.15.5",
|
|
47
|
-
"@react-navigation/native": "^7.1.33",
|
|
48
|
-
"@react-navigation/stack": "^7.8.5",
|
|
49
46
|
"@tanstack/query-async-storage-persister": "^5.90.24",
|
|
50
47
|
"@tanstack/react-query": "^5.90.21",
|
|
51
48
|
"@tanstack/react-query-persist-client": "^5.90.24",
|
|
@@ -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();
|