@umituz/react-native-ai-pruna-provider 1.0.6 → 1.0.8

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.6",
3
+ "version": "1.0.8",
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",
@@ -80,6 +80,8 @@ export interface PrunaPredictionInput {
80
80
  readonly image?: string;
81
81
  readonly images?: readonly string[];
82
82
  readonly reference_image?: string;
83
+ /** Audio file for p-video (base64 or URL). When provided, duration is determined by audio length. */
84
+ readonly audio?: string;
83
85
  readonly duration?: number;
84
86
  readonly resolution?: PrunaResolution;
85
87
  readonly fps?: number;
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Endpoints:
6
6
  * POST /v1/predictions — submit generation (with Try-Sync header for immediate results)
7
- * POST /v1/files — upload images for p-video (requires file URL, not base64)
7
+ * POST /v1/files — upload files (images, audio) for p-video (requires file URL, not base64)
8
8
  * GET {poll_url} — poll async results
9
9
  *
10
10
  * Authentication: `apikey` header
@@ -14,26 +14,28 @@
14
14
  import type { PrunaModelId, PrunaPredictionResponse, PrunaFileUploadResponse } from "../../domain/entities/pruna.types";
15
15
  import { PRUNA_BASE_URL, PRUNA_PREDICTIONS_URL, PRUNA_FILES_URL } from "./pruna-provider.constants";
16
16
  import { generationLogCollector } from "../utils/log-collector";
17
+ import { detectMimeType } from "../utils/mime-detection.util";
18
+ import { getExtensionForMime } from "../utils/constants/mime.constants";
17
19
 
18
20
  const TAG = 'pruna-api';
19
21
 
20
22
  /**
21
- * Upload a base64 image to Pruna's file storage.
22
- * p-video requires a file URL (not raw base64).
23
+ * Upload a base64 file (image or audio) to Pruna's file storage.
24
+ * p-video requires file URLs (not raw base64).
23
25
  * Returns the HTTPS file URL to use in predictions.
24
26
  */
25
- export async function uploadImageToFiles(
27
+ export async function uploadFileToStorage(
26
28
  base64Data: string,
27
29
  apiKey: string,
28
30
  sessionId: string,
29
31
  ): Promise<string> {
30
32
  // Already a URL — return as-is
31
33
  if (base64Data.startsWith('http')) {
32
- generationLogCollector.log(sessionId, TAG, 'Image already a URL, skipping upload');
34
+ generationLogCollector.log(sessionId, TAG, 'File already a URL, skipping upload');
33
35
  return base64Data;
34
36
  }
35
37
 
36
- generationLogCollector.log(sessionId, TAG, 'Uploading image to Pruna file storage...');
38
+ generationLogCollector.log(sessionId, TAG, 'Uploading file to Pruna storage...');
37
39
 
38
40
  // Strip data URI prefix if present
39
41
  const raw = base64Data.includes('base64,') ? base64Data.split('base64,')[1] : base64Data;
@@ -42,7 +44,7 @@ export async function uploadImageToFiles(
42
44
  try {
43
45
  binaryStr = atob(raw);
44
46
  } catch {
45
- throw new Error("Invalid image format. Please provide base64 or a valid URL.");
47
+ throw new Error("Invalid file format. Please provide base64 or a valid URL.");
46
48
  }
47
49
 
48
50
  const bytes = new Uint8Array(binaryStr.length);
@@ -50,13 +52,9 @@ export async function uploadImageToFiles(
50
52
  bytes[i] = binaryStr.charCodeAt(i);
51
53
  }
52
54
 
53
- // Detect MIME from first bytes
54
- let mime = 'image/png';
55
- if (bytes[0] === 0xFF && bytes[1] === 0xD8) mime = 'image/jpeg';
56
- else if (bytes[0] === 0x52 && bytes[1] === 0x49) mime = 'image/webp';
57
-
55
+ const mime = detectMimeType(bytes);
56
+ const ext = getExtensionForMime(mime);
58
57
  const blob = new Blob([bytes], { type: mime });
59
- const ext = mime.split('/')[1];
60
58
  const formData = new FormData();
61
59
  formData.append('content', blob, `upload.${ext}`);
62
60
 
@@ -84,6 +82,9 @@ export async function uploadImageToFiles(
84
82
  return fileUrl;
85
83
  }
86
84
 
85
+ /** @deprecated Use uploadFileToStorage instead */
86
+ export const uploadImageToFiles = uploadFileToStorage;
87
+
87
88
  /**
88
89
  * Strip base64 data URI prefix, returning raw base64 string.
89
90
  * If input is already a URL, returns it unchanged.
@@ -5,12 +5,12 @@
5
5
  * Each Pruna model has strict schema requirements:
6
6
  * p-image: { prompt, aspect_ratio? }
7
7
  * p-image-edit: { images: string[], prompt, aspect_ratio? }
8
- * p-video: { image: string (URL), prompt, duration, resolution, fps, draft, aspect_ratio, prompt_upsampling }
8
+ * p-video: { image: string (URL), prompt, duration, resolution, fps, draft, aspect_ratio, prompt_upsampling, audio? }
9
9
  */
10
10
 
11
11
  import type { PrunaModelId, PrunaAspectRatio, PrunaResolution } from "../../domain/entities/pruna.types";
12
12
  import { P_VIDEO_DEFAULTS, DEFAULT_ASPECT_RATIO } from "./pruna-provider.constants";
13
- import { uploadImageToFiles, stripBase64Prefix } from "./pruna-api-client";
13
+ import { uploadFileToStorage, stripBase64Prefix } from "./pruna-api-client";
14
14
  import { generationLogCollector } from "../utils/log-collector";
15
15
 
16
16
  const TAG = 'pruna-input-builder';
@@ -115,7 +115,7 @@ async function buildVideoInput(
115
115
 
116
116
  // Upload base64 to file storage if needed (p-video requires HTTPS URL)
117
117
  generationLogCollector.log(sessionId, TAG, 'p-video: preparing image for video generation...');
118
- const fileUrl = await uploadImageToFiles(rawImage, apiKey, sessionId);
118
+ const fileUrl = await uploadFileToStorage(rawImage, apiKey, sessionId);
119
119
 
120
120
  const duration = (input.duration as number) ?? P_VIDEO_DEFAULTS.duration;
121
121
  const resolution = (input.resolution as PrunaResolution) ?? P_VIDEO_DEFAULTS.resolution;
@@ -135,6 +135,15 @@ async function buildVideoInput(
135
135
  prompt_upsampling: promptUpsampling,
136
136
  };
137
137
 
138
+ // Handle audio input — upload to file storage if base64, pass URL if already remote
139
+ const rawAudio = input.audio as string | undefined;
140
+ if (rawAudio) {
141
+ generationLogCollector.log(sessionId, TAG, 'p-video: preparing audio for video generation...');
142
+ const audioUrl = await uploadFileToStorage(rawAudio, apiKey, sessionId);
143
+ payload.audio = audioUrl;
144
+ generationLogCollector.log(sessionId, TAG, 'p-video: audio attached — duration will be determined by audio length');
145
+ }
146
+
138
147
  if (input.disable_safety_checker !== undefined) payload.disable_safety_checker = input.disable_safety_checker;
139
148
 
140
149
  return payload;
@@ -0,0 +1,12 @@
1
+ export {
2
+ MIME_IMAGE_PNG,
3
+ MIME_IMAGE_JPEG,
4
+ MIME_IMAGE_WEBP,
5
+ MIME_AUDIO_MPEG,
6
+ MIME_AUDIO_WAV,
7
+ MIME_AUDIO_FLAC,
8
+ MIME_AUDIO_MP4,
9
+ MIME_DEFAULT,
10
+ MIME_TO_EXTENSION,
11
+ getExtensionForMime,
12
+ } from "./mime.constants";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * MIME Type Constants
3
+ * Supported media types for Pruna file uploads (images + audio)
4
+ */
5
+
6
+ // ── Image MIME types ────────────────────────────────────────
7
+ export const MIME_IMAGE_PNG = 'image/png' as const;
8
+ export const MIME_IMAGE_JPEG = 'image/jpeg' as const;
9
+ export const MIME_IMAGE_WEBP = 'image/webp' as const;
10
+
11
+ // ── Audio MIME types (p-video audio input: flac, mp3, wav) ──
12
+ export const MIME_AUDIO_MPEG = 'audio/mpeg' as const;
13
+ export const MIME_AUDIO_WAV = 'audio/wav' as const;
14
+ export const MIME_AUDIO_FLAC = 'audio/flac' as const;
15
+ export const MIME_AUDIO_MP4 = 'audio/mp4' as const;
16
+
17
+ // ── Fallback ────────────────────────────────────────────────
18
+ export const MIME_DEFAULT = MIME_IMAGE_PNG;
19
+
20
+ /** Maps MIME type → file extension for upload naming */
21
+ export const MIME_TO_EXTENSION: Readonly<Record<string, string>> = {
22
+ [MIME_IMAGE_PNG]: 'png',
23
+ [MIME_IMAGE_JPEG]: 'jpg',
24
+ [MIME_IMAGE_WEBP]: 'webp',
25
+ [MIME_AUDIO_MPEG]: 'mp3',
26
+ [MIME_AUDIO_WAV]: 'wav',
27
+ [MIME_AUDIO_FLAC]: 'flac',
28
+ [MIME_AUDIO_MP4]: 'm4a',
29
+ };
30
+
31
+ /**
32
+ * Get file extension for a MIME type.
33
+ * Falls back to the subtype (e.g. "png" from "image/png").
34
+ */
35
+ export function getExtensionForMime(mime: string): string {
36
+ return MIME_TO_EXTENSION[mime] || mime.split('/')[1] || 'bin';
37
+ }
@@ -28,3 +28,9 @@ export {
28
28
 
29
29
  export { generationLogCollector } from "./log-collector";
30
30
  export type { LogEntry } from "./log-collector";
31
+
32
+ export { detectMimeType } from "./mime-detection.util";
33
+ export {
34
+ MIME_TO_EXTENSION,
35
+ getExtensionForMime,
36
+ } from "./constants/mime.constants";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * MIME Type Detection Utility
3
+ * Detects file type from binary content using magic byte signatures.
4
+ *
5
+ * Supported formats:
6
+ * Image: PNG, JPEG, WebP
7
+ * Audio: MP3 (ID3 + sync word), WAV (RIFF/WAVE), FLAC, M4A/AAC (MP4 ftyp)
8
+ */
9
+
10
+ import {
11
+ MIME_IMAGE_PNG,
12
+ MIME_IMAGE_JPEG,
13
+ MIME_IMAGE_WEBP,
14
+ MIME_AUDIO_MPEG,
15
+ MIME_AUDIO_WAV,
16
+ MIME_AUDIO_FLAC,
17
+ MIME_AUDIO_MP4,
18
+ MIME_DEFAULT,
19
+ } from "./constants/mime.constants";
20
+
21
+ /**
22
+ * Detect MIME type from raw binary bytes using magic number signatures.
23
+ *
24
+ * Detection order is intentional:
25
+ * 1. JPEG (0xFF 0xD8) — checked before MP3 sync word to avoid false positives
26
+ * 2. PNG (0x89 0x50)
27
+ * 3. RIFF container → distinguish WAV vs WebP via subformat at offset 8-11
28
+ * 4. MP3 with ID3 tag (0x49 0x44 0x33)
29
+ * 5. MP3 sync word (0xFF 0xE_) — after JPEG to prevent overlap
30
+ * 6. FLAC (fLaC)
31
+ * 7. M4A/AAC (ftyp box at offset 4)
32
+ */
33
+ export function detectMimeType(bytes: Uint8Array): string {
34
+ if (bytes.length < 4) return MIME_DEFAULT;
35
+
36
+ // ── Image formats ───────────────────────────────────────
37
+ // JPEG: FF D8
38
+ if (bytes[0] === 0xFF && bytes[1] === 0xD8) return MIME_IMAGE_JPEG;
39
+
40
+ // PNG: 89 50 4E 47
41
+ if (bytes[0] === 0x89 && bytes[1] === 0x50) return MIME_IMAGE_PNG;
42
+
43
+ // RIFF container — WAV (RIFF....WAVE) or WebP (RIFF....WEBP)
44
+ if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {
45
+ if (bytes.length > 11) {
46
+ // WAVE at offset 8
47
+ if (bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) return MIME_AUDIO_WAV;
48
+ // WEBP at offset 8
49
+ if (bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) return MIME_IMAGE_WEBP;
50
+ }
51
+ return MIME_IMAGE_WEBP;
52
+ }
53
+
54
+ // ── Audio formats ───────────────────────────────────────
55
+ // MP3 with ID3v2 tag: 49 44 33
56
+ if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) return MIME_AUDIO_MPEG;
57
+
58
+ // MP3 frame sync word: FF Ex/Fx (but not FF FF)
59
+ if (bytes[0] === 0xFF && (bytes[1] & 0xE0) === 0xE0 && bytes[1] !== 0xFF) return MIME_AUDIO_MPEG;
60
+
61
+ // FLAC: 66 4C 61 43 ("fLaC")
62
+ if (bytes[0] === 0x66 && bytes[1] === 0x4C && bytes[2] === 0x61 && bytes[3] === 0x43) return MIME_AUDIO_FLAC;
63
+
64
+ // M4A / AAC in MP4 container: ftyp box at offset 4
65
+ if (bytes.length > 7 && bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) return MIME_AUDIO_MP4;
66
+
67
+ return MIME_DEFAULT;
68
+ }