@umituz/react-native-ai-pruna-provider 1.0.36 → 1.0.38
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.
|
|
3
|
+
"version": "1.0.38",
|
|
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",
|
|
@@ -29,14 +29,23 @@ export async function uploadFileToStorage(
|
|
|
29
29
|
apiKey: string,
|
|
30
30
|
sessionId: string,
|
|
31
31
|
): Promise<string> {
|
|
32
|
+
generationLogCollector.log(sessionId, TAG, `>>> uploadFileToStorage START`, {
|
|
33
|
+
dataLength: base64Data.length,
|
|
34
|
+
startsWithHttp: base64Data.startsWith('http'),
|
|
35
|
+
startsWithDataUri: base64Data.startsWith('data:'),
|
|
36
|
+
});
|
|
37
|
+
|
|
32
38
|
// Guard: empty or whitespace-only input
|
|
33
39
|
if (!base64Data || !base64Data.trim()) {
|
|
40
|
+
generationLogCollector.error(sessionId, TAG, 'File data validation FAILED: empty input');
|
|
34
41
|
throw new Error("File data is empty. Provide a base64 string or URL.");
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
// Already a URL — return as-is
|
|
38
45
|
if (base64Data.startsWith('http')) {
|
|
39
|
-
generationLogCollector.log(sessionId, TAG, 'File already a URL, skipping upload'
|
|
46
|
+
generationLogCollector.log(sessionId, TAG, 'File already a URL, skipping upload', {
|
|
47
|
+
url: base64Data.substring(0, 80) + '...',
|
|
48
|
+
});
|
|
40
49
|
return base64Data;
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -48,27 +57,26 @@ export async function uploadFileToStorage(
|
|
|
48
57
|
console.log(`[DEV] [${TAG}] File upload input:`, {
|
|
49
58
|
dataSizeKB,
|
|
50
59
|
startsWithDataUri: base64Data.startsWith('data:'),
|
|
51
|
-
startsWithHttp: base64Data.startsWith('http'),
|
|
52
60
|
preview: base64Data.substring(0, 50) + '...',
|
|
53
61
|
});
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
// Already a URL — return as-is
|
|
57
|
-
if (base64Data.startsWith('http')) {
|
|
58
|
-
generationLogCollector.log(sessionId, TAG, 'File already a URL, skipping upload');
|
|
59
|
-
return base64Data;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
64
|
// Strip data URI prefix if present to get raw base64
|
|
63
65
|
const rawBase64 = base64Data.includes('base64,')
|
|
64
66
|
? base64Data.split('base64,')[1]
|
|
65
67
|
: base64Data;
|
|
66
68
|
|
|
69
|
+
generationLogCollector.log(sessionId, TAG, 'Base64 processing complete', {
|
|
70
|
+
originalLength: base64Data.length,
|
|
71
|
+
rawLength: rawBase64.length,
|
|
72
|
+
hadDataUriPrefix: base64Data.includes('base64,'),
|
|
73
|
+
});
|
|
74
|
+
|
|
67
75
|
// Use default JPEG MIME type (detectMimeType fails on base64)
|
|
68
76
|
const mimeType = 'image/jpeg';
|
|
69
77
|
const dataUri = `data:${mimeType};base64,${rawBase64}`;
|
|
70
78
|
|
|
71
|
-
generationLogCollector.log(sessionId, TAG, `
|
|
79
|
+
generationLogCollector.log(sessionId, TAG, `Creating data URI for upload (${mimeType})...`);
|
|
72
80
|
|
|
73
81
|
// __DEV__ log upload details
|
|
74
82
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
@@ -98,6 +106,12 @@ export async function uploadFileToStorage(
|
|
|
98
106
|
|
|
99
107
|
formData.append('content', fileObject);
|
|
100
108
|
|
|
109
|
+
generationLogCollector.log(sessionId, TAG, 'FormData created', {
|
|
110
|
+
hasContent: formData.has('content'),
|
|
111
|
+
fileName: fileObject.name,
|
|
112
|
+
mimeType: fileObject.type,
|
|
113
|
+
});
|
|
114
|
+
|
|
101
115
|
// __DEV__ log FormData
|
|
102
116
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
103
117
|
console.log(`[DEV] [${TAG}] FormData created (file object format):`, {
|
|
@@ -110,110 +124,122 @@ export async function uploadFileToStorage(
|
|
|
110
124
|
});
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
|
|
127
|
+
// __DEV__ log request details
|
|
128
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
129
|
+
console.log(`[DEV] [${TAG}] Sending upload request:`, {
|
|
130
|
+
url: PRUNA_FILES_URL,
|
|
131
|
+
method: 'POST',
|
|
132
|
+
formDataKeys: Array.from(formData.keys()),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
114
135
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const timeoutId = setTimeout(() => uploadController.abort(), UPLOAD_CONFIG.timeoutMs);
|
|
136
|
+
const uploadStart = Date.now();
|
|
137
|
+
generationLogCollector.log(sessionId, TAG, 'Sending POST request to file storage...');
|
|
118
138
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
headers: {
|
|
126
|
-
'apikey': apiKey.substring(0, 15) + '...',
|
|
127
|
-
},
|
|
128
|
-
formDataKeys: Array.from(formData.keys()),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
139
|
+
const uploadResponse = await fetch(PRUNA_FILES_URL, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'apikey': apiKey },
|
|
142
|
+
body: formData,
|
|
143
|
+
signal: uploadController.signal,
|
|
144
|
+
});
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
const uploadElapsed = Date.now() - uploadStart;
|
|
147
|
+
generationLogCollector.log(sessionId, TAG, 'Upload response received', {
|
|
148
|
+
statusCode: uploadResponse.status,
|
|
149
|
+
statusText: uploadResponse.statusText,
|
|
150
|
+
elapsedMs: uploadElapsed,
|
|
151
|
+
});
|
|
138
152
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
153
|
+
if (!uploadResponse.ok) {
|
|
154
|
+
// Get response details for debugging
|
|
155
|
+
const statusText = uploadResponse.statusText;
|
|
156
|
+
const status = uploadResponse.status;
|
|
157
|
+
|
|
158
|
+
// Try to get error details from response
|
|
159
|
+
let rawBody = '';
|
|
160
|
+
let errorDetails: Record<string, unknown> = {};
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
rawBody = await uploadResponse.text();
|
|
164
|
+
if (rawBody) {
|
|
165
|
+
try {
|
|
166
|
+
errorDetails = JSON.parse(rawBody) as Record<string, unknown>;
|
|
167
|
+
} catch {
|
|
168
|
+
// If not JSON, keep raw text
|
|
156
169
|
}
|
|
157
|
-
} catch {
|
|
158
|
-
// If reading body fails, continue with status info
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const errorMessage = (errorDetails as { message?: string; detail?: string; error?: string }).message ||
|
|
162
|
-
(errorDetails as { detail?: string }).detail ||
|
|
163
|
-
(errorDetails as { error?: string }).error ||
|
|
164
|
-
rawBody ||
|
|
165
|
-
`File upload error: ${status}`;
|
|
166
|
-
|
|
167
|
-
generationLogCollector.error(sessionId, TAG, `File upload failed: ${errorMessage}`);
|
|
168
|
-
|
|
169
|
-
// __DEV__ detailed error logging
|
|
170
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
171
|
-
console.error(`[DEV] [${TAG}] File upload FAILED:`, {
|
|
172
|
-
status,
|
|
173
|
-
statusText,
|
|
174
|
-
errorMessage,
|
|
175
|
-
rawBody: rawBody.substring(0, 1000),
|
|
176
|
-
errorDetails,
|
|
177
|
-
url: PRUNA_FILES_URL,
|
|
178
|
-
formDataPreview: {
|
|
179
|
-
hasContent: formData.has('content'),
|
|
180
|
-
contentType: formData.get('content')?.toString().substring(0, 100) + '...',
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
170
|
}
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
} catch {
|
|
172
|
+
// If reading body fails, continue with status info
|
|
186
173
|
}
|
|
187
174
|
|
|
188
|
-
const
|
|
189
|
-
|
|
175
|
+
const errorMessage = (errorDetails as { message?: string; detail?: string; error?: string }).message ||
|
|
176
|
+
(errorDetails as { detail?: string }).detail ||
|
|
177
|
+
(errorDetails as { error?: string }).error ||
|
|
178
|
+
rawBody ||
|
|
179
|
+
`File upload error: ${status}`;
|
|
190
180
|
|
|
191
|
-
|
|
192
|
-
|
|
181
|
+
generationLogCollector.error(sessionId, TAG, `File upload FAILED`, {
|
|
182
|
+
status,
|
|
183
|
+
statusText,
|
|
184
|
+
errorMessage,
|
|
185
|
+
rawBodyLength: rawBody.length,
|
|
186
|
+
});
|
|
193
187
|
|
|
194
|
-
// __DEV__
|
|
188
|
+
// __DEV__ detailed error logging
|
|
195
189
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
196
|
-
console.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
190
|
+
console.error(`[DEV] [${TAG}] File upload FAILED:`, {
|
|
191
|
+
status,
|
|
192
|
+
statusText,
|
|
193
|
+
errorMessage,
|
|
194
|
+
rawBody: rawBody.substring(0, 1000),
|
|
195
|
+
errorDetails,
|
|
196
|
+
url: PRUNA_FILES_URL,
|
|
197
|
+
formDataPreview: {
|
|
198
|
+
hasContent: formData.has('content'),
|
|
199
|
+
contentType: formData.get('content')?.toString().substring(0, 100) + '...',
|
|
200
|
+
},
|
|
202
201
|
});
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
throw new Error(errorMessage);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const data: PrunaFileUploadResponse = await uploadResponse.json();
|
|
208
|
+
const fileUrl = data.urls?.get || `${PRUNA_FILES_URL}/${data.id}`;
|
|
209
|
+
|
|
210
|
+
const elapsed = Date.now() - startTime;
|
|
211
|
+
generationLogCollector.log(sessionId, TAG, `File upload completed in ${elapsed}ms`, {
|
|
212
|
+
fileId: data.id,
|
|
213
|
+
fileUrl: fileUrl.substring(0, 80) + '...',
|
|
214
|
+
responseKeys: Object.keys(data),
|
|
215
|
+
hasUrlsGet: !!data.urls?.get,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// __DEV__ log response details
|
|
219
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
220
|
+
console.log(`[DEV] [${TAG}] File upload SUCCESS:`, {
|
|
221
|
+
elapsedMs: elapsed,
|
|
222
|
+
fileId: data.id,
|
|
223
|
+
fileUrl,
|
|
224
|
+
urls: data.urls,
|
|
225
|
+
responseKeys: Object.keys(data),
|
|
226
|
+
});
|
|
214
227
|
}
|
|
228
|
+
|
|
229
|
+
generationLogCollector.log(sessionId, TAG, `<<< uploadFileToStorage COMPLETE`, {
|
|
230
|
+
totalElapsedMs: elapsed,
|
|
231
|
+
resultUrl: fileUrl.substring(0, 60) + '...',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return fileUrl;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
237
|
+
generationLogCollector.error(sessionId, TAG, `File upload timed out after ${UPLOAD_CONFIG.timeoutMs}ms`);
|
|
238
|
+
throw new Error(`File upload timed out after ${UPLOAD_CONFIG.timeoutMs}ms`);
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
215
241
|
} finally {
|
|
216
|
-
|
|
242
|
+
clearTimeout(timeoutId);
|
|
217
243
|
}
|
|
218
244
|
}
|
|
219
245
|
|
|
@@ -238,8 +264,10 @@ export async function submitPrediction(
|
|
|
238
264
|
sessionId: string,
|
|
239
265
|
signal?: AbortSignal,
|
|
240
266
|
): Promise<PrunaPredictionResponse> {
|
|
241
|
-
generationLogCollector.log(sessionId, TAG,
|
|
267
|
+
generationLogCollector.log(sessionId, TAG, `>>> submitPrediction START`, {
|
|
268
|
+
model,
|
|
242
269
|
inputKeys: Object.keys(input),
|
|
270
|
+
hasSignal: !!signal,
|
|
243
271
|
});
|
|
244
272
|
|
|
245
273
|
const startTime = Date.now();
|
|
@@ -265,11 +293,19 @@ export async function submitPrediction(
|
|
|
265
293
|
bodyTopLevelKeys: Object.keys(requestBody),
|
|
266
294
|
inputKeys: Object.keys(input),
|
|
267
295
|
inputSummary,
|
|
296
|
+
requestBodySizeKB: Math.round(JSON.stringify(requestBody).length / 1024),
|
|
268
297
|
});
|
|
269
298
|
}
|
|
270
299
|
|
|
271
|
-
generationLogCollector.log(sessionId, TAG, `
|
|
300
|
+
generationLogCollector.log(sessionId, TAG, `Sending POST request...`, {
|
|
301
|
+
url: PRUNA_PREDICTIONS_URL,
|
|
302
|
+
model,
|
|
303
|
+
bodyKeys: Object.keys(requestBody),
|
|
304
|
+
inputKeys: Object.keys(input),
|
|
305
|
+
hasTrySync: true,
|
|
306
|
+
});
|
|
272
307
|
|
|
308
|
+
const requestStart = Date.now();
|
|
273
309
|
const response = await fetch(PRUNA_PREDICTIONS_URL, {
|
|
274
310
|
method: 'POST',
|
|
275
311
|
headers: {
|
|
@@ -281,6 +317,14 @@ export async function submitPrediction(
|
|
|
281
317
|
body: JSON.stringify(requestBody),
|
|
282
318
|
signal,
|
|
283
319
|
});
|
|
320
|
+
const requestElapsed = Date.now() - requestStart;
|
|
321
|
+
|
|
322
|
+
generationLogCollector.log(sessionId, TAG, `Response received`, {
|
|
323
|
+
statusCode: response.status,
|
|
324
|
+
statusText: response.statusText,
|
|
325
|
+
requestElapsedMs: requestElapsed,
|
|
326
|
+
ok: response.ok,
|
|
327
|
+
});
|
|
284
328
|
|
|
285
329
|
if (!response.ok) {
|
|
286
330
|
const rawBody = await response.text().catch(() => '');
|
|
@@ -292,7 +336,12 @@ export async function submitPrediction(
|
|
|
292
336
|
if (rawBody) errorMessage = rawBody;
|
|
293
337
|
}
|
|
294
338
|
|
|
295
|
-
generationLogCollector.error(sessionId, TAG, `Prediction
|
|
339
|
+
generationLogCollector.error(sessionId, TAG, `Prediction FAILED`, {
|
|
340
|
+
statusCode: response.status,
|
|
341
|
+
statusText: response.statusText,
|
|
342
|
+
errorMessage,
|
|
343
|
+
rawBodyLength: rawBody.length,
|
|
344
|
+
});
|
|
296
345
|
|
|
297
346
|
// __DEV__ detailed error logging
|
|
298
347
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
@@ -314,11 +363,14 @@ export async function submitPrediction(
|
|
|
314
363
|
const elapsed = Date.now() - startTime;
|
|
315
364
|
const result: PrunaPredictionResponse = await response.json();
|
|
316
365
|
|
|
317
|
-
generationLogCollector.log(sessionId, TAG, `Prediction response
|
|
366
|
+
generationLogCollector.log(sessionId, TAG, `Prediction response parsing complete`, {
|
|
367
|
+
elapsedMs: elapsed,
|
|
318
368
|
hasUri: !!extractUri(result),
|
|
319
369
|
hasGetUrl: !!result.get_url,
|
|
320
370
|
hasStatusUrl: !!result.status_url,
|
|
321
371
|
status: result.status,
|
|
372
|
+
hasId: !!result.id,
|
|
373
|
+
responseKeys: Object.keys(result),
|
|
322
374
|
});
|
|
323
375
|
|
|
324
376
|
// __DEV__ response logging
|
|
@@ -327,9 +379,15 @@ export async function submitPrediction(
|
|
|
327
379
|
hasUri: !!extractUri(result),
|
|
328
380
|
status: result.status,
|
|
329
381
|
responseKeys: Object.keys(result),
|
|
382
|
+
resultId: result.id,
|
|
330
383
|
});
|
|
331
384
|
}
|
|
332
385
|
|
|
386
|
+
generationLogCollector.log(sessionId, TAG, `<<< submitPrediction COMPLETE`, {
|
|
387
|
+
totalElapsedMs: elapsed,
|
|
388
|
+
resultStatus: result.status,
|
|
389
|
+
});
|
|
390
|
+
|
|
333
391
|
return result;
|
|
334
392
|
}
|
|
335
393
|
|
|
@@ -25,25 +25,42 @@ export async function buildModelInput(
|
|
|
25
25
|
apiKey: string,
|
|
26
26
|
sessionId: string,
|
|
27
27
|
): Promise<Record<string, unknown>> {
|
|
28
|
+
generationLogCollector.log(sessionId, TAG, `>>> buildModelInput START`, {
|
|
29
|
+
model,
|
|
30
|
+
inputKeys: Object.keys(input),
|
|
31
|
+
hasPrompt: !!input.prompt,
|
|
32
|
+
hasAspectRatio: !!input.aspect_ratio,
|
|
33
|
+
});
|
|
34
|
+
|
|
28
35
|
const prompt = input.prompt as string | undefined;
|
|
29
36
|
if (!prompt) {
|
|
37
|
+
generationLogCollector.error(sessionId, TAG, 'Prompt validation FAILED: missing prompt');
|
|
30
38
|
throw new Error("Prompt is required for all Pruna models.");
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
const aspectRatio = (input.aspect_ratio as PrunaAspectRatio) || DEFAULT_ASPECT_RATIO;
|
|
42
|
+
generationLogCollector.log(sessionId, TAG, `Prompt validation OK`, {
|
|
43
|
+
promptLength: prompt.length,
|
|
44
|
+
promptPreview: prompt.substring(0, 50) + '...',
|
|
45
|
+
aspectRatio,
|
|
46
|
+
});
|
|
34
47
|
|
|
35
48
|
if (model === 'p-image') {
|
|
49
|
+
generationLogCollector.log(sessionId, TAG, `Routing to buildImageInput`);
|
|
36
50
|
return buildImageInput(prompt, aspectRatio, input);
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
if (model === 'p-image-edit') {
|
|
54
|
+
generationLogCollector.log(sessionId, TAG, `Routing to buildImageEditInput`);
|
|
40
55
|
return await buildImageEditInput(prompt, aspectRatio, input, apiKey, sessionId);
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
if (model === 'p-video') {
|
|
59
|
+
generationLogCollector.log(sessionId, TAG, `Routing to buildVideoInput`);
|
|
44
60
|
return await buildVideoInput(prompt, aspectRatio, input, apiKey, sessionId);
|
|
45
61
|
}
|
|
46
62
|
|
|
63
|
+
generationLogCollector.error(sessionId, TAG, `Unknown model: ${model}`);
|
|
47
64
|
throw new Error(`Unknown Pruna model: ${model}`);
|
|
48
65
|
}
|
|
49
66
|
|
|
@@ -52,6 +69,12 @@ function buildImageInput(
|
|
|
52
69
|
aspectRatio: PrunaAspectRatio,
|
|
53
70
|
input: Record<string, unknown>,
|
|
54
71
|
): Record<string, unknown> {
|
|
72
|
+
generationLogCollector.log('build-image-input', TAG, `buildImageInput START`, {
|
|
73
|
+
promptLength: prompt.length,
|
|
74
|
+
aspectRatio,
|
|
75
|
+
optionalParams: Object.keys(input).filter(k => !['prompt', 'aspect_ratio'].includes(k)),
|
|
76
|
+
});
|
|
77
|
+
|
|
55
78
|
const payload: Record<string, unknown> = { prompt, aspect_ratio: aspectRatio };
|
|
56
79
|
|
|
57
80
|
if (input.seed !== undefined) payload.seed = input.seed;
|
|
@@ -59,6 +82,12 @@ function buildImageInput(
|
|
|
59
82
|
if (input.width !== undefined) payload.width = input.width;
|
|
60
83
|
if (input.height !== undefined) payload.height = input.height;
|
|
61
84
|
|
|
85
|
+
generationLogCollector.log('build-image-input', TAG, `buildImageInput COMPLETE`, {
|
|
86
|
+
payloadKeys: Object.keys(payload),
|
|
87
|
+
hasSeed: !!payload.seed,
|
|
88
|
+
hasDimensions: !!(payload.width || payload.height),
|
|
89
|
+
});
|
|
90
|
+
|
|
62
91
|
return payload;
|
|
63
92
|
}
|
|
64
93
|
|
|
@@ -69,40 +98,81 @@ async function buildImageEditInput(
|
|
|
69
98
|
apiKey: string,
|
|
70
99
|
sessionId: string,
|
|
71
100
|
): Promise<Record<string, unknown>> {
|
|
72
|
-
|
|
101
|
+
generationLogCollector.log(sessionId, TAG, `>>> buildImageEditInput START`, {
|
|
102
|
+
promptLength: prompt.length,
|
|
103
|
+
aspectRatio,
|
|
104
|
+
inputKeys: Object.keys(input),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// p-image-edit: Upload images to file storage and use URLs (required by Pruna API)
|
|
73
108
|
// Get base64 image(s) from input
|
|
74
109
|
let rawImages: string[];
|
|
75
110
|
|
|
111
|
+
generationLogCollector.log(sessionId, TAG, `Detecting image source...`, {
|
|
112
|
+
hasImages: Array.isArray(input.images),
|
|
113
|
+
hasImage: typeof input.image === 'string',
|
|
114
|
+
hasImageUrl: typeof input.image_url === 'string',
|
|
115
|
+
hasImageUrls: Array.isArray(input.image_urls),
|
|
116
|
+
});
|
|
117
|
+
|
|
76
118
|
if (Array.isArray(input.images)) {
|
|
77
119
|
const validImages = (input.images as unknown[]).filter((img): img is string => typeof img === 'string');
|
|
78
120
|
if (validImages.length === 0) {
|
|
121
|
+
generationLogCollector.error(sessionId, TAG, 'Image array validation FAILED: empty or invalid');
|
|
79
122
|
throw new Error("Image array is empty or contains no valid strings for p-image-edit.");
|
|
80
123
|
}
|
|
81
124
|
rawImages = validImages;
|
|
125
|
+
generationLogCollector.log(sessionId, TAG, `Image source: 'images' array`, { count: rawImages.length });
|
|
82
126
|
} else if (typeof input.image === 'string') {
|
|
83
127
|
rawImages = [input.image as string];
|
|
128
|
+
generationLogCollector.log(sessionId, TAG, `Image source: 'image' string`, {
|
|
129
|
+
length: rawImages[0].length,
|
|
130
|
+
isBase64: rawImages[0].includes('base64'),
|
|
131
|
+
isHttp: rawImages[0].startsWith('http'),
|
|
132
|
+
});
|
|
84
133
|
} else if (typeof input.image_url === 'string') {
|
|
85
134
|
rawImages = [input.image_url as string];
|
|
135
|
+
generationLogCollector.log(sessionId, TAG, `Image source: 'image_url' string`);
|
|
86
136
|
} else if (Array.isArray(input.image_urls)) {
|
|
87
137
|
rawImages = input.image_urls as string[];
|
|
138
|
+
generationLogCollector.log(sessionId, TAG, `Image source: 'image_urls' array`, { count: rawImages.length });
|
|
88
139
|
} else {
|
|
140
|
+
generationLogCollector.error(sessionId, TAG, 'Image validation FAILED: no valid image source found');
|
|
89
141
|
throw new Error("Image is required for p-image-edit. Provide 'image', 'images', 'image_url', or 'image_urls'.");
|
|
90
142
|
}
|
|
91
143
|
|
|
92
|
-
generationLogCollector.log(sessionId, TAG, `p-image-edit:
|
|
144
|
+
generationLogCollector.log(sessionId, TAG, `p-image-edit: starting upload of ${rawImages.length} image(s)...`);
|
|
93
145
|
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
146
|
+
// Upload images to Pruna file storage and collect URLs
|
|
147
|
+
const imageUrls: string[] = [];
|
|
148
|
+
for (let i = 0; i < rawImages.length; i++) {
|
|
149
|
+
const rawImage = rawImages[i];
|
|
150
|
+
const uploadStart = Date.now();
|
|
151
|
+
|
|
152
|
+
generationLogCollector.log(sessionId, TAG, `p-image-edit: [${i + 1}/${rawImages.length}] Starting upload...`, {
|
|
153
|
+
imageSizeKB: Math.round(rawImage.length / 1024),
|
|
154
|
+
isBase64: rawImage.includes('base64'),
|
|
155
|
+
isUrl: rawImage.startsWith('http'),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Upload to file storage (if already a URL, it will be returned as-is)
|
|
159
|
+
const fileUrl = await uploadFileToStorage(rawImage, apiKey, sessionId);
|
|
160
|
+
imageUrls.push(fileUrl);
|
|
161
|
+
|
|
162
|
+
const uploadElapsed = Date.now() - uploadStart;
|
|
163
|
+
generationLogCollector.log(sessionId, TAG, `p-image-edit: [${i + 1}/${rawImages.length}] Upload complete`, {
|
|
164
|
+
fileUrl: fileUrl.substring(0, 80) + '...',
|
|
165
|
+
elapsedMs: uploadElapsed,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
generationLogCollector.log(sessionId, TAG, `All images uploaded successfully`, {
|
|
170
|
+
totalImages: imageUrls.length,
|
|
101
171
|
});
|
|
102
172
|
|
|
103
|
-
//
|
|
173
|
+
// Build payload with image URLs (not raw base64)
|
|
104
174
|
const payload: Record<string, unknown> = {
|
|
105
|
-
images:
|
|
175
|
+
images: imageUrls, // Array of HTTPS URLs from file upload
|
|
106
176
|
prompt,
|
|
107
177
|
aspect_ratio: aspectRatio,
|
|
108
178
|
};
|
|
@@ -110,10 +180,11 @@ async function buildImageEditInput(
|
|
|
110
180
|
// __DEV__ log final payload
|
|
111
181
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
112
182
|
console.log(`[DEV] [${TAG}] Final p-image-edit payload:`, {
|
|
113
|
-
imageCount:
|
|
183
|
+
imageCount: imageUrls.length,
|
|
114
184
|
promptLength: prompt.length,
|
|
115
185
|
aspectRatio,
|
|
116
|
-
|
|
186
|
+
firstImageUrl: imageUrls[0]?.substring(0, 60) + '...',
|
|
187
|
+
payloadSizeKB: Math.round(JSON.stringify(payload).length / 1024),
|
|
117
188
|
});
|
|
118
189
|
}
|
|
119
190
|
|
|
@@ -122,6 +193,12 @@ async function buildImageEditInput(
|
|
|
122
193
|
if (input.width !== undefined) payload.width = input.width;
|
|
123
194
|
if (input.height !== undefined) payload.height = input.height;
|
|
124
195
|
|
|
196
|
+
generationLogCollector.log(sessionId, TAG, `<<< buildImageEditInput COMPLETE`, {
|
|
197
|
+
payloadKeys: Object.keys(payload),
|
|
198
|
+
imageCount: imageUrls.length,
|
|
199
|
+
hasSeed: !!payload.seed,
|
|
200
|
+
});
|
|
201
|
+
|
|
125
202
|
return payload;
|
|
126
203
|
}
|
|
127
204
|
|
|
@@ -132,15 +209,34 @@ async function buildVideoInput(
|
|
|
132
209
|
apiKey: string,
|
|
133
210
|
sessionId: string,
|
|
134
211
|
): Promise<Record<string, unknown>> {
|
|
212
|
+
generationLogCollector.log(sessionId, TAG, `>>> buildVideoInput START`, {
|
|
213
|
+
promptLength: prompt.length,
|
|
214
|
+
aspectRatio,
|
|
215
|
+
inputKeys: Object.keys(input),
|
|
216
|
+
});
|
|
217
|
+
|
|
135
218
|
// p-video requires an image file URL
|
|
136
219
|
const rawImage = (input.image as string) || (input.image_url as string);
|
|
137
220
|
if (!rawImage) {
|
|
221
|
+
generationLogCollector.error(sessionId, TAG, 'Video image validation FAILED: no image provided');
|
|
138
222
|
throw new Error("Image is required for p-video. Provide 'image' or 'image_url'.");
|
|
139
223
|
}
|
|
140
224
|
|
|
225
|
+
generationLogCollector.log(sessionId, TAG, 'p-video: preparing image for video generation...', {
|
|
226
|
+
imageSizeKB: Math.round(rawImage.length / 1024),
|
|
227
|
+
isBase64: rawImage.includes('base64'),
|
|
228
|
+
isUrl: rawImage.startsWith('http'),
|
|
229
|
+
});
|
|
230
|
+
|
|
141
231
|
// Upload base64 to file storage if needed (p-video requires HTTPS URL)
|
|
142
|
-
|
|
232
|
+
const uploadStart = Date.now();
|
|
143
233
|
const fileUrl = await uploadFileToStorage(rawImage, apiKey, sessionId);
|
|
234
|
+
const uploadElapsed = Date.now() - uploadStart;
|
|
235
|
+
|
|
236
|
+
generationLogCollector.log(sessionId, TAG, 'p-video: image upload complete', {
|
|
237
|
+
fileUrl: fileUrl.substring(0, 80) + '...',
|
|
238
|
+
elapsedMs: uploadElapsed,
|
|
239
|
+
});
|
|
144
240
|
|
|
145
241
|
const duration = (input.duration as number) ?? P_VIDEO_DEFAULTS.duration;
|
|
146
242
|
const resolution = (input.resolution as PrunaResolution) ?? P_VIDEO_DEFAULTS.resolution;
|
|
@@ -149,6 +245,14 @@ async function buildVideoInput(
|
|
|
149
245
|
const fps = (input.fps as number) ?? P_VIDEO_DEFAULTS.fps;
|
|
150
246
|
const promptUpsampling = (input.prompt_upsampling as boolean) ?? P_VIDEO_DEFAULTS.promptUpsampling;
|
|
151
247
|
|
|
248
|
+
generationLogCollector.log(sessionId, TAG, 'p-video: video parameters', {
|
|
249
|
+
duration,
|
|
250
|
+
resolution,
|
|
251
|
+
fps,
|
|
252
|
+
draft,
|
|
253
|
+
promptUpsampling,
|
|
254
|
+
});
|
|
255
|
+
|
|
152
256
|
const payload: Record<string, unknown> = {
|
|
153
257
|
image: fileUrl,
|
|
154
258
|
prompt,
|
|
@@ -163,13 +267,30 @@ async function buildVideoInput(
|
|
|
163
267
|
// Handle audio input — upload to file storage if base64, pass URL if already remote
|
|
164
268
|
const rawAudio = input.audio as string | undefined;
|
|
165
269
|
if (rawAudio) {
|
|
166
|
-
generationLogCollector.log(sessionId, TAG, 'p-video: preparing audio for video generation...'
|
|
270
|
+
generationLogCollector.log(sessionId, TAG, 'p-video: preparing audio for video generation...', {
|
|
271
|
+
audioSizeKB: Math.round(rawAudio.length / 1024),
|
|
272
|
+
isBase64: rawAudio.includes('base64'),
|
|
273
|
+
isUrl: rawAudio.startsWith('http'),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const audioUploadStart = Date.now();
|
|
167
277
|
const audioUrl = await uploadFileToStorage(rawAudio, apiKey, sessionId);
|
|
278
|
+
const audioUploadElapsed = Date.now() - audioUploadStart;
|
|
279
|
+
|
|
168
280
|
payload.audio = audioUrl;
|
|
169
|
-
generationLogCollector.log(sessionId, TAG, 'p-video: audio
|
|
281
|
+
generationLogCollector.log(sessionId, TAG, 'p-video: audio upload complete', {
|
|
282
|
+
audioUrl: audioUrl.substring(0, 80) + '...',
|
|
283
|
+
elapsedMs: audioUploadElapsed,
|
|
284
|
+
});
|
|
170
285
|
}
|
|
171
286
|
|
|
172
287
|
if (input.disable_safety_checker !== undefined) payload.disable_safety_checker = input.disable_safety_checker;
|
|
173
288
|
|
|
289
|
+
generationLogCollector.log(sessionId, TAG, `<<< buildVideoInput COMPLETE`, {
|
|
290
|
+
payloadKeys: Object.keys(payload),
|
|
291
|
+
hasAudio: !!payload.audio,
|
|
292
|
+
videoParams: { duration, resolution, fps, draft },
|
|
293
|
+
});
|
|
294
|
+
|
|
174
295
|
return payload;
|
|
175
296
|
}
|