@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.36",
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, `Uploading as data URI (${mimeType})...`);
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
- const startTime = Date.now();
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
- // Apply timeout to prevent indefinite hangs
116
- const uploadController = new AbortController();
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
- try {
120
- // __DEV__ log request details
121
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
122
- console.log(`[DEV] [${TAG}] Sending upload request:`, {
123
- url: PRUNA_FILES_URL,
124
- method: 'POST',
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
- const uploadResponse = await fetch(PRUNA_FILES_URL, {
133
- method: 'POST',
134
- headers: { 'apikey': apiKey },
135
- body: formData,
136
- signal: uploadController.signal,
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
- if (!uploadResponse.ok) {
140
- // Get response details for debugging
141
- const statusText = uploadResponse.statusText;
142
- const status = uploadResponse.status;
143
-
144
- // Try to get error details from response
145
- let rawBody = '';
146
- let errorDetails: Record<string, unknown> = {};
147
-
148
- try {
149
- rawBody = await uploadResponse.text();
150
- if (rawBody) {
151
- try {
152
- errorDetails = JSON.parse(rawBody) as Record<string, unknown>;
153
- } catch {
154
- // If not JSON, keep raw text
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
- throw new Error(errorMessage);
171
+ } catch {
172
+ // If reading body fails, continue with status info
186
173
  }
187
174
 
188
- const data: PrunaFileUploadResponse = await uploadResponse.json();
189
- const fileUrl = data.urls?.get || `${PRUNA_FILES_URL}/${data.id}`;
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
- const elapsed = Date.now() - startTime;
192
- generationLogCollector.log(sessionId, TAG, `File upload completed in ${elapsed}ms → ${fileUrl}`);
181
+ generationLogCollector.error(sessionId, TAG, `File upload FAILED`, {
182
+ status,
183
+ statusText,
184
+ errorMessage,
185
+ rawBodyLength: rawBody.length,
186
+ });
193
187
 
194
- // __DEV__ log response details
188
+ // __DEV__ detailed error logging
195
189
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
196
- console.log(`[DEV] [${TAG}] File upload SUCCESS:`, {
197
- elapsedMs: elapsed,
198
- fileId: data.id,
199
- fileUrl,
200
- urls: data.urls,
201
- responseKeys: Object.keys(data),
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
- return fileUrl;
206
- } catch (error) {
207
- if (error instanceof Error && error.name === 'AbortError') {
208
- generationLogCollector.error(sessionId, TAG, `File upload timed out after ${UPLOAD_CONFIG.timeoutMs}ms`);
209
- throw new Error(`File upload timed out after ${UPLOAD_CONFIG.timeoutMs}ms`);
210
- }
211
- throw error;
212
- } finally {
213
- clearTimeout(timeoutId);
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
- // No temp file cleanup needed (using data URI directly)
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, `Submitting prediction for model: ${model}`, {
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, `Request: model=${model}, bodyKeys=${JSON.stringify(Object.keys(requestBody))}, inputKeys=${JSON.stringify(Object.keys(input))}`);
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 failed (${response.status}): ${errorMessage}`);
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 received in ${elapsed}ms`, {
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
- // p-image-edit: Like video-studio, use raw base64 (not file upload)
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: processing ${rawImages.length} image(s)...`);
144
+ generationLogCollector.log(sessionId, TAG, `p-image-edit: starting upload of ${rawImages.length} image(s)...`);
93
145
 
94
- // Strip data URI prefixes (like video-studio does)
95
- const strippedImages = rawImages.map(img => {
96
- if (img.startsWith('http')) {
97
- return img; // Already a URL, pass through
98
- }
99
- // Strip data URI prefix to get raw base64
100
- return img.includes('base64,') ? img.split('base64,')[1] : img;
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
- // Use first image (like video-studio)
173
+ // Build payload with image URLs (not raw base64)
104
174
  const payload: Record<string, unknown> = {
105
- images: [strippedImages[0]], // Array with first image, raw base64
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: strippedImages.length,
183
+ imageCount: imageUrls.length,
114
184
  promptLength: prompt.length,
115
185
  aspectRatio,
116
- base64Preview: strippedImages[0].substring(0, 50) + '...',
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
- generationLogCollector.log(sessionId, TAG, 'p-video: preparing image for video generation...');
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 attached duration will be determined by audio length');
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
  }