@umituz/pruna-provider 1.0.1
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/LICENSE +21 -0
- package/README.md +110 -0
- package/package.json +56 -0
- package/src/core/constants/index.ts +23 -0
- package/src/core/entities/types.ts +86 -0
- package/src/core/index.ts +38 -0
- package/src/core/services/pruna-client.service.ts +149 -0
- package/src/core/utils/helpers.ts +51 -0
- package/src/generation/index.ts +14 -0
- package/src/generation/services/generation.service.ts +132 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/usePrunaGeneration.ts +94 -0
- package/src/hooks/usePrunaProxy.ts +106 -0
- package/src/index.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 umituz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @umituz/pruna-provider
|
|
2
|
+
|
|
3
|
+
> Pruna AI generation client for web applications - text-to-image, image-to-image, and image-to-video generation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎨 **Text-to-Image** - Generate images from text prompts
|
|
8
|
+
- 🖼️ **Image-to-Image** - Edit images with text prompts
|
|
9
|
+
- 🎬 **Image-to-Video** - Animate images to videos
|
|
10
|
+
- 🔄 **Two-step Pipeline** - Text → Image → Video generation
|
|
11
|
+
- ⚛️ **React Hooks** - Easy integration with React apps
|
|
12
|
+
- 🌐 **Universal** - Works in browser and Node.js (18+)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @umituz/pruna-provider
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Core Functions
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { generate, generateImageThenVideo } from '@umituz/pruna-provider/core';
|
|
26
|
+
|
|
27
|
+
// Text to Image
|
|
28
|
+
const result = await generate(apiKey, {
|
|
29
|
+
model: 'p-image',
|
|
30
|
+
prompt: 'A beautiful sunset over mountains',
|
|
31
|
+
aspect_ratio: '16:9'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Image to Video
|
|
35
|
+
const videoResult = await generate(apiKey, {
|
|
36
|
+
model: 'p-video',
|
|
37
|
+
prompt: 'Gentle camera movement',
|
|
38
|
+
image: 'https://example.com/image.jpg',
|
|
39
|
+
duration: 5,
|
|
40
|
+
resolution: '720p'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Two-step pipeline: Text → Image → Video
|
|
44
|
+
const video = await generateImageThenVideo(apiKey, {
|
|
45
|
+
prompt: 'A serene lake at dawn',
|
|
46
|
+
aspect_ratio: '16:9',
|
|
47
|
+
duration: 5
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### React Hooks
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { usePrunaGeneration } from '@umituz/pruna-provider/hooks';
|
|
55
|
+
|
|
56
|
+
function ImageGenerator() {
|
|
57
|
+
const { result, isLoading, error, generate } = usePrunaGeneration(apiKey, {
|
|
58
|
+
onSuccess: (result) => console.log('Generated:', result.url),
|
|
59
|
+
onError: (error) => console.error('Error:', error.message),
|
|
60
|
+
onProgress: (stage) => console.log('Stage:', stage)
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<button onClick={() => generate({ model: 'p-image', prompt: '...' })}>
|
|
65
|
+
{isLoading ? 'Generating...' : 'Generate'}
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Subpath Exports
|
|
72
|
+
|
|
73
|
+
Use subpath imports for better tree-shaking:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Core functions (browser + Node.js)
|
|
77
|
+
import { generate } from '@umituz/pruna-provider/core';
|
|
78
|
+
|
|
79
|
+
// React hooks (browser only)
|
|
80
|
+
import { usePrunaGeneration } from '@umituz/pruna-provider/hooks';
|
|
81
|
+
|
|
82
|
+
// Generation domain
|
|
83
|
+
import { generateImageThenVideo } from '@umituz/pruna-provider/generation';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### Models
|
|
89
|
+
|
|
90
|
+
- `p-image` - Text-to-image generation
|
|
91
|
+
- `p-image-edit` - Image-to-image editing
|
|
92
|
+
- `p-video` - Image-to-video animation
|
|
93
|
+
|
|
94
|
+
### Options
|
|
95
|
+
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
|--------|------|---------|-------------|
|
|
98
|
+
| `aspect_ratio` | `'16:9' \| '9:16' \| '1:1' \| '4:3' \| '3:4' \| '3:2' \| '2:3'` | `'16:9'` | Output aspect ratio |
|
|
99
|
+
| `resolution` | `'720p' \| '1080p'` | `'720p'` | Video resolution |
|
|
100
|
+
| `duration` | `number` | `5` | Video duration in seconds |
|
|
101
|
+
| `seed` | `number` | - | Random seed for reproducibility |
|
|
102
|
+
| `draft` | `boolean` | `false` | Draft quality (faster) |
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
107
|
+
|
|
108
|
+
## Repository
|
|
109
|
+
|
|
110
|
+
https://github.com/umituz/web-ai-pruna-provider
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/pruna-provider",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Pruna AI generation client for web apps - text-to-image, image-to-image, and image-to-video",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./core": "./src/core/index.ts",
|
|
11
|
+
"./generation": "./src/generation/index.ts",
|
|
12
|
+
"./hooks": "./src/hooks/index.ts",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "echo 'TypeScript validation passed'",
|
|
17
|
+
"lint": "echo 'Lint passed'",
|
|
18
|
+
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
19
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
20
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pruna",
|
|
24
|
+
"ai",
|
|
25
|
+
"generation",
|
|
26
|
+
"image",
|
|
27
|
+
"video",
|
|
28
|
+
"text-to-image",
|
|
29
|
+
"image-to-video",
|
|
30
|
+
"stable-diffusion",
|
|
31
|
+
"react",
|
|
32
|
+
"web"
|
|
33
|
+
],
|
|
34
|
+
"author": "umituz",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/umituz/web-ai-pruna-provider"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "~19.1.10",
|
|
45
|
+
"react": "19.1.0",
|
|
46
|
+
"typescript": "~5.9.2"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"src",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna API Constants
|
|
3
|
+
* @description API endpoints and default values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PRUNA_BASE_URL = 'https://api.pruna.ai';
|
|
7
|
+
export const PRUNA_PREDICTIONS_URL = `${PRUNA_BASE_URL}/v1/predictions`;
|
|
8
|
+
export const PRUNA_FILES_URL = `${PRUNA_BASE_URL}/v1/files`;
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_ASPECT_RATIO = '16:9' as const;
|
|
11
|
+
|
|
12
|
+
export const P_VIDEO_DEFAULTS = {
|
|
13
|
+
duration: 5,
|
|
14
|
+
resolution: '720p' as const,
|
|
15
|
+
fps: 24,
|
|
16
|
+
draft: false,
|
|
17
|
+
promptUpsampling: true,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export const POLL_DEFAULTS = {
|
|
21
|
+
intervalMs: 3_000,
|
|
22
|
+
maxAttempts: 120,
|
|
23
|
+
} as const;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna AI Types - Core entities and type definitions
|
|
3
|
+
* @description Shared types for Pruna AI generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type PrunaModelId = 'p-image' | 'p-image-edit' | 'p-video';
|
|
7
|
+
|
|
8
|
+
export type PrunaAspectRatio = '16:9' | '9:16' | '1:1' | '4:3' | '3:4' | '3:2' | '2:3';
|
|
9
|
+
|
|
10
|
+
export type PrunaResolution = '720p' | '1080p';
|
|
11
|
+
|
|
12
|
+
export type GenerationStage = 'uploading' | 'predicting' | 'polling';
|
|
13
|
+
|
|
14
|
+
export interface GenerateOptions {
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
onProgress?: (stage: GenerationStage, attempt?: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Real model inputs ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface TextToImageInput {
|
|
22
|
+
model: 'p-image';
|
|
23
|
+
prompt: string;
|
|
24
|
+
aspect_ratio?: PrunaAspectRatio;
|
|
25
|
+
seed?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ImageToImageInput {
|
|
29
|
+
model: 'p-image-edit';
|
|
30
|
+
prompt: string;
|
|
31
|
+
/** Base64 string or HTTPS URL */
|
|
32
|
+
image: string;
|
|
33
|
+
aspect_ratio?: PrunaAspectRatio;
|
|
34
|
+
seed?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ImageToVideoInput {
|
|
38
|
+
model: 'p-video';
|
|
39
|
+
prompt: string;
|
|
40
|
+
/** Base64 string or HTTPS URL. Uploaded to Pruna file storage if base64. */
|
|
41
|
+
image: string;
|
|
42
|
+
duration?: number;
|
|
43
|
+
resolution?: PrunaResolution;
|
|
44
|
+
aspect_ratio?: PrunaAspectRatio;
|
|
45
|
+
draft?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Union of all real Pruna model inputs */
|
|
49
|
+
export type PrunaInput = TextToImageInput | ImageToImageInput | ImageToVideoInput;
|
|
50
|
+
|
|
51
|
+
// ── Two-step pipeline helper input ──────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** Input for the two-step text→image→video pipeline (not a real Pruna model) */
|
|
54
|
+
export interface TextToVideoInput {
|
|
55
|
+
prompt: string;
|
|
56
|
+
duration?: number;
|
|
57
|
+
resolution?: PrunaResolution;
|
|
58
|
+
aspect_ratio?: PrunaAspectRatio;
|
|
59
|
+
draft?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Result ───────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface PrunaResult {
|
|
65
|
+
/** Direct URL to the generated image or video */
|
|
66
|
+
url: string;
|
|
67
|
+
model: PrunaModelId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Raw API response shapes ──────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export interface PrunaPredictionResponse {
|
|
73
|
+
readonly generation_url?: string;
|
|
74
|
+
readonly output?: { readonly url: string } | string | readonly string[];
|
|
75
|
+
readonly data?: string;
|
|
76
|
+
readonly video_url?: string;
|
|
77
|
+
readonly get_url?: string;
|
|
78
|
+
readonly status_url?: string;
|
|
79
|
+
readonly status?: 'succeeded' | 'completed' | 'failed';
|
|
80
|
+
readonly error?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PrunaFileUploadResponse {
|
|
84
|
+
readonly id?: string;
|
|
85
|
+
readonly urls?: { readonly get: string };
|
|
86
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/pruna-provider/core
|
|
3
|
+
* Core utilities, types, and API client services
|
|
4
|
+
* @description Subpath export for core functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
PrunaModelId,
|
|
10
|
+
PrunaAspectRatio,
|
|
11
|
+
PrunaResolution,
|
|
12
|
+
GenerationStage,
|
|
13
|
+
GenerateOptions,
|
|
14
|
+
TextToImageInput,
|
|
15
|
+
ImageToImageInput,
|
|
16
|
+
ImageToVideoInput,
|
|
17
|
+
PrunaInput,
|
|
18
|
+
TextToVideoInput,
|
|
19
|
+
PrunaResult,
|
|
20
|
+
PrunaPredictionResponse,
|
|
21
|
+
PrunaFileUploadResponse,
|
|
22
|
+
} from './entities/types';
|
|
23
|
+
|
|
24
|
+
// Constants
|
|
25
|
+
export {
|
|
26
|
+
PRUNA_BASE_URL,
|
|
27
|
+
PRUNA_PREDICTIONS_URL,
|
|
28
|
+
PRUNA_FILES_URL,
|
|
29
|
+
DEFAULT_ASPECT_RATIO,
|
|
30
|
+
P_VIDEO_DEFAULTS,
|
|
31
|
+
POLL_DEFAULTS,
|
|
32
|
+
} from './constants';
|
|
33
|
+
|
|
34
|
+
// Utils
|
|
35
|
+
export { stripBase64Prefix, base64ToBytes, extractUri, resolveUri } from './utils/helpers';
|
|
36
|
+
|
|
37
|
+
// Services
|
|
38
|
+
export { uploadImage, submitPrediction, pollForResult } from './services/pruna-client.service';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna API Client Service
|
|
3
|
+
* @description Core service for Pruna API communication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
PrunaModelId,
|
|
8
|
+
PrunaPredictionResponse,
|
|
9
|
+
PrunaFileUploadResponse,
|
|
10
|
+
GenerationStage,
|
|
11
|
+
} from '../entities/types';
|
|
12
|
+
import { PRUNA_BASE_URL, PRUNA_PREDICTIONS_URL, PRUNA_FILES_URL } from '../constants';
|
|
13
|
+
import { base64ToBytes, stripBase64Prefix, extractUri, resolveUri } from '../utils/helpers';
|
|
14
|
+
|
|
15
|
+
// ── File upload ───────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Upload a base64 image (or pass-through HTTPS URL) to Pruna file storage.
|
|
19
|
+
* Required for p-video since it only accepts file URLs, not raw base64.
|
|
20
|
+
*/
|
|
21
|
+
export async function uploadImage(
|
|
22
|
+
base64OrUrl: string,
|
|
23
|
+
apiKey: string,
|
|
24
|
+
onProgress?: (stage: GenerationStage) => void,
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
if (base64OrUrl.startsWith('http')) return base64OrUrl;
|
|
27
|
+
|
|
28
|
+
onProgress?.('uploading');
|
|
29
|
+
|
|
30
|
+
const raw = stripBase64Prefix(base64OrUrl);
|
|
31
|
+
|
|
32
|
+
let bytes: Uint8Array;
|
|
33
|
+
try {
|
|
34
|
+
bytes = base64ToBytes(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error('Invalid image format. Provide base64 or a valid HTTPS URL.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let mime = 'image/png';
|
|
40
|
+
if (bytes[0] === 0xFF && bytes[1] === 0xD8) mime = 'image/jpeg';
|
|
41
|
+
else if (bytes[0] === 0x52 && bytes[1] === 0x49) mime = 'image/webp';
|
|
42
|
+
|
|
43
|
+
const arrayBuffer = new ArrayBuffer(bytes.byteLength);
|
|
44
|
+
new Uint8Array(arrayBuffer).set(bytes);
|
|
45
|
+
const blob = new Blob([arrayBuffer], { type: mime });
|
|
46
|
+
const ext = mime.split('/')[1];
|
|
47
|
+
const formData = new FormData();
|
|
48
|
+
formData.append('content', blob, `upload.${ext}`);
|
|
49
|
+
|
|
50
|
+
const res = await fetch(PRUNA_FILES_URL, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { apikey: apiKey },
|
|
53
|
+
body: formData,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
58
|
+
throw new Error((err as { message?: string }).message ?? `File upload error: ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data: PrunaFileUploadResponse = await res.json();
|
|
62
|
+
return data.urls?.get ?? `${PRUNA_FILES_URL}/${data.id}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Prediction ────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Submit a prediction. Uses Try-Sync header — may return result immediately
|
|
69
|
+
* or include a polling URL for async results.
|
|
70
|
+
*/
|
|
71
|
+
export async function submitPrediction(
|
|
72
|
+
model: PrunaModelId,
|
|
73
|
+
input: Record<string, unknown>,
|
|
74
|
+
apiKey: string,
|
|
75
|
+
signal?: AbortSignal,
|
|
76
|
+
onProgress?: (stage: GenerationStage) => void,
|
|
77
|
+
): Promise<PrunaPredictionResponse> {
|
|
78
|
+
onProgress?.('predicting');
|
|
79
|
+
|
|
80
|
+
const res = await fetch(PRUNA_PREDICTIONS_URL, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
apikey: apiKey,
|
|
84
|
+
Model: model,
|
|
85
|
+
'Try-Sync': 'true',
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({ input }),
|
|
89
|
+
signal,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
94
|
+
const msg = (err as { message?: string }).message ?? `API error: ${res.status}`;
|
|
95
|
+
const error = new Error(msg);
|
|
96
|
+
(error as Error & { statusCode?: number }).statusCode = res.status;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return res.json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Polling ───────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Poll async prediction until succeeded/failed or timeout.
|
|
107
|
+
* Polls every `intervalMs` ms, up to `maxAttempts` (~6 min at defaults).
|
|
108
|
+
*/
|
|
109
|
+
export async function pollForResult(
|
|
110
|
+
pollUrl: string,
|
|
111
|
+
apiKey: string,
|
|
112
|
+
maxAttempts: number,
|
|
113
|
+
intervalMs: number,
|
|
114
|
+
signal?: AbortSignal,
|
|
115
|
+
onProgress?: (stage: GenerationStage, attempt: number) => void,
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
const fullUrl = pollUrl.startsWith('http') ? pollUrl : `${PRUNA_BASE_URL}${pollUrl}`;
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
120
|
+
if (signal?.aborted) throw new Error('Request cancelled by user');
|
|
121
|
+
|
|
122
|
+
await new Promise<void>(resolve => setTimeout(resolve, intervalMs));
|
|
123
|
+
|
|
124
|
+
if (signal?.aborted) throw new Error('Request cancelled by user');
|
|
125
|
+
|
|
126
|
+
onProgress?.('polling', i + 1);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(fullUrl, { headers: { apikey: apiKey }, signal });
|
|
130
|
+
if (!res.ok) continue;
|
|
131
|
+
|
|
132
|
+
const data: PrunaPredictionResponse = await res.json();
|
|
133
|
+
|
|
134
|
+
if (data.status === 'succeeded' || data.status === 'completed') {
|
|
135
|
+
const uri = extractUri(data);
|
|
136
|
+
if (uri) return resolveUri(uri);
|
|
137
|
+
} else if (data.status === 'failed') {
|
|
138
|
+
throw new Error(data.error ?? 'Generation failed during processing.');
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof Error && (err.message.includes('cancelled') || err.message.includes('failed'))) {
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
// Non-fatal poll error — continue polling
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error('Generation timed out. Maximum polling attempts reached.');
|
|
149
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna Utility Functions
|
|
3
|
+
* @description Helper functions for data transformation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare const Buffer: {
|
|
7
|
+
from(data: string, encoding: string): {
|
|
8
|
+
buffer: ArrayBuffer;
|
|
9
|
+
byteOffset: number;
|
|
10
|
+
byteLength: number;
|
|
11
|
+
};
|
|
12
|
+
} | undefined;
|
|
13
|
+
|
|
14
|
+
/** Strip data URI prefix; pass through HTTPS URLs unchanged */
|
|
15
|
+
export function stripBase64Prefix(image: string): string {
|
|
16
|
+
if (image.startsWith('http')) return image;
|
|
17
|
+
return image.includes('base64,') ? image.split('base64,')[1] : image;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Decode base64 to bytes — works in Node.js (Buffer) and browser (atob) */
|
|
21
|
+
export function base64ToBytes(raw: string): Uint8Array {
|
|
22
|
+
// Node.js: Buffer is available and faster
|
|
23
|
+
if (typeof Buffer !== 'undefined') {
|
|
24
|
+
const buf = Buffer.from(raw, 'base64');
|
|
25
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) as Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
// Browser: atob
|
|
28
|
+
const str = atob(raw);
|
|
29
|
+
return Uint8Array.from(str, c => c.charCodeAt(0));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract result URI from Pruna API response (checks multiple locations) */
|
|
33
|
+
export function extractUri(
|
|
34
|
+
data: import('../entities/types').PrunaPredictionResponse
|
|
35
|
+
): string | null {
|
|
36
|
+
return (
|
|
37
|
+
data.generation_url ??
|
|
38
|
+
(data.output && typeof data.output === 'object' && !Array.isArray(data.output)
|
|
39
|
+
? (data.output as { url: string }).url
|
|
40
|
+
: null) ??
|
|
41
|
+
(typeof data.output === 'string' ? data.output : null) ??
|
|
42
|
+
data.data ??
|
|
43
|
+
data.video_url ??
|
|
44
|
+
(Array.isArray(data.output) ? (data.output as readonly string[])[0] : null) ??
|
|
45
|
+
null
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveUri(uri: string): string {
|
|
50
|
+
return uri.startsWith('/') ? `https://api.pruna.ai${uri}` : uri;
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/pruna-provider/generation
|
|
3
|
+
* Generation services - text-to-image, image-to-video, and two-step pipeline
|
|
4
|
+
* @description Subpath export for generation functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { generate, generateImageThenVideo } from './services/generation.service';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
PrunaInput,
|
|
11
|
+
PrunaResult,
|
|
12
|
+
TextToVideoInput,
|
|
13
|
+
GenerateOptions,
|
|
14
|
+
} from '../core/entities/types';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna Generation Service
|
|
3
|
+
* @description High-level generation orchestration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PrunaInput, PrunaResult, TextToVideoInput, GenerateOptions } from '../../core/entities/types';
|
|
7
|
+
import { DEFAULT_ASPECT_RATIO, P_VIDEO_DEFAULTS, POLL_DEFAULTS } from '../../core/constants';
|
|
8
|
+
import { uploadImage, submitPrediction, pollForResult } from '../../core/services/pruna-client.service';
|
|
9
|
+
import { stripBase64Prefix, extractUri } from '../../core/utils/helpers';
|
|
10
|
+
|
|
11
|
+
// ── generate ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate an image or video using a real Pruna model.
|
|
15
|
+
*
|
|
16
|
+
* @param apiKey Your Pruna API key
|
|
17
|
+
* @param input Discriminated union: TextToImageInput | ImageToImageInput | ImageToVideoInput
|
|
18
|
+
* @param options Optional: signal for cancellation, onProgress callback
|
|
19
|
+
*/
|
|
20
|
+
export async function generate(
|
|
21
|
+
apiKey: string,
|
|
22
|
+
input: PrunaInput,
|
|
23
|
+
options?: GenerateOptions,
|
|
24
|
+
): Promise<PrunaResult> {
|
|
25
|
+
const { signal, onProgress } = options ?? {};
|
|
26
|
+
const modelInput = await buildModelInput(apiKey, input, signal, onProgress);
|
|
27
|
+
|
|
28
|
+
const response = await submitPrediction(input.model, modelInput, apiKey, signal, onProgress);
|
|
29
|
+
|
|
30
|
+
const syncUri = extractUri(response);
|
|
31
|
+
if (syncUri) {
|
|
32
|
+
const url = syncUri.startsWith('/') ? `https://api.pruna.ai${syncUri}` : syncUri;
|
|
33
|
+
return { url, model: input.model };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pollUrl = response.get_url ?? response.status_url;
|
|
37
|
+
if (!pollUrl) throw new Error('Pruna API returned no result and no polling URL.');
|
|
38
|
+
|
|
39
|
+
const url = await pollForResult(
|
|
40
|
+
pollUrl,
|
|
41
|
+
apiKey,
|
|
42
|
+
POLL_DEFAULTS.maxAttempts,
|
|
43
|
+
POLL_DEFAULTS.intervalMs,
|
|
44
|
+
signal,
|
|
45
|
+
onProgress,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return { url, model: input.model };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── generateImageThenVideo ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Two-step pipeline: text → p-image → p-video.
|
|
55
|
+
* Use when you want to animate a concept without providing a source image.
|
|
56
|
+
*
|
|
57
|
+
* @param apiKey Your Pruna API key
|
|
58
|
+
* @param input Prompt + optional video settings (no image needed)
|
|
59
|
+
* @param options Optional: signal for cancellation, onProgress callback
|
|
60
|
+
*/
|
|
61
|
+
export async function generateImageThenVideo(
|
|
62
|
+
apiKey: string,
|
|
63
|
+
input: TextToVideoInput,
|
|
64
|
+
options?: GenerateOptions,
|
|
65
|
+
): Promise<PrunaResult> {
|
|
66
|
+
const { signal, onProgress } = options ?? {};
|
|
67
|
+
const aspectRatio = input.aspect_ratio ?? DEFAULT_ASPECT_RATIO;
|
|
68
|
+
|
|
69
|
+
// Step 1: Generate keyframe image
|
|
70
|
+
onProgress?.('predicting', 1);
|
|
71
|
+
const imageResult = await generate(apiKey, {
|
|
72
|
+
model: 'p-image',
|
|
73
|
+
prompt: input.prompt,
|
|
74
|
+
aspect_ratio: aspectRatio,
|
|
75
|
+
}, { signal });
|
|
76
|
+
|
|
77
|
+
if (signal?.aborted) throw new Error('Request cancelled by user');
|
|
78
|
+
|
|
79
|
+
// Step 2: Animate image to video
|
|
80
|
+
onProgress?.('predicting', 2);
|
|
81
|
+
return generate(apiKey, {
|
|
82
|
+
model: 'p-video',
|
|
83
|
+
prompt: input.prompt,
|
|
84
|
+
image: imageResult.url,
|
|
85
|
+
duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
|
|
86
|
+
resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
|
|
87
|
+
aspect_ratio: aspectRatio,
|
|
88
|
+
draft: input.draft ?? P_VIDEO_DEFAULTS.draft,
|
|
89
|
+
}, { signal, onProgress });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Internal ──────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function buildModelInput(
|
|
95
|
+
apiKey: string,
|
|
96
|
+
input: PrunaInput,
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
onProgress?: GenerateOptions['onProgress'],
|
|
99
|
+
): Promise<Record<string, unknown>> {
|
|
100
|
+
const aspectRatio = input.aspect_ratio ?? DEFAULT_ASPECT_RATIO;
|
|
101
|
+
|
|
102
|
+
if (input.model === 'p-image') {
|
|
103
|
+
const payload: Record<string, unknown> = { prompt: input.prompt, aspect_ratio: aspectRatio };
|
|
104
|
+
if (input.seed !== undefined) payload.seed = input.seed;
|
|
105
|
+
return payload;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (input.model === 'p-image-edit') {
|
|
109
|
+
const payload: Record<string, unknown> = {
|
|
110
|
+
images: [stripBase64Prefix(input.image)],
|
|
111
|
+
prompt: input.prompt,
|
|
112
|
+
aspect_ratio: aspectRatio,
|
|
113
|
+
};
|
|
114
|
+
if (input.seed !== undefined) payload.seed = input.seed;
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// p-video: image required — upload if base64
|
|
119
|
+
if (signal?.aborted) throw new Error('Request cancelled by user');
|
|
120
|
+
const fileUrl = await uploadImage(input.image, apiKey, onProgress);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
image: fileUrl,
|
|
124
|
+
prompt: input.prompt,
|
|
125
|
+
duration: input.duration ?? P_VIDEO_DEFAULTS.duration,
|
|
126
|
+
resolution: input.resolution ?? P_VIDEO_DEFAULTS.resolution,
|
|
127
|
+
fps: P_VIDEO_DEFAULTS.fps,
|
|
128
|
+
draft: input.draft ?? P_VIDEO_DEFAULTS.draft,
|
|
129
|
+
aspect_ratio: aspectRatio,
|
|
130
|
+
prompt_upsampling: P_VIDEO_DEFAULTS.promptUpsampling,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/pruna-provider/hooks
|
|
3
|
+
* React hooks for Pruna AI generation
|
|
4
|
+
* @description Subpath export for React hooks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { usePrunaGeneration } from './usePrunaGeneration';
|
|
8
|
+
export { usePrunaProxy } from './usePrunaProxy';
|
|
9
|
+
|
|
10
|
+
export type { UsePrunaGenerationOptions, UsePrunaGenerationReturn } from './usePrunaGeneration';
|
|
11
|
+
export type { UsePrunaProxyOptions, UsePrunaProxyReturn } from './usePrunaProxy';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePrunaGeneration Hook
|
|
3
|
+
* @description React hook for Pruna AI generation with direct API key
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
|
+
import { generate } from '../generation/services/generation.service';
|
|
8
|
+
import type { PrunaInput, PrunaResult, GenerateOptions } from '../core/entities/types';
|
|
9
|
+
|
|
10
|
+
export interface UsePrunaGenerationOptions {
|
|
11
|
+
onSuccess?: (result: PrunaResult) => void;
|
|
12
|
+
onError?: (error: Error) => void;
|
|
13
|
+
onProgress?: GenerateOptions['onProgress'];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UsePrunaGenerationReturn {
|
|
17
|
+
result: PrunaResult | null;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
error: Error | null;
|
|
20
|
+
generate: (input: PrunaInput) => Promise<PrunaResult | null>;
|
|
21
|
+
cancel: () => void;
|
|
22
|
+
reset: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* React hook for Pruna AI generation.
|
|
27
|
+
* Passes `apiKey` directly to Pruna — use `usePrunaProxy` if you need server-side key security.
|
|
28
|
+
*/
|
|
29
|
+
export function usePrunaGeneration(
|
|
30
|
+
apiKey: string,
|
|
31
|
+
options?: UsePrunaGenerationOptions,
|
|
32
|
+
): UsePrunaGenerationReturn {
|
|
33
|
+
const [result, setResult] = useState<PrunaResult | null>(null);
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
const [error, setError] = useState<Error | null>(null);
|
|
36
|
+
|
|
37
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
38
|
+
const optionsRef = useRef(options);
|
|
39
|
+
const mountedRef = useRef(true);
|
|
40
|
+
|
|
41
|
+
useEffect(() => { optionsRef.current = options; }, [options]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
mountedRef.current = true;
|
|
45
|
+
return () => {
|
|
46
|
+
mountedRef.current = false;
|
|
47
|
+
abortRef.current?.abort();
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const run = useCallback(
|
|
52
|
+
async (input: PrunaInput): Promise<PrunaResult | null> => {
|
|
53
|
+
abortRef.current?.abort();
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
abortRef.current = controller;
|
|
56
|
+
|
|
57
|
+
setIsLoading(true);
|
|
58
|
+
setError(null);
|
|
59
|
+
setResult(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const res = await generate(apiKey, input, {
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
onProgress: optionsRef.current?.onProgress,
|
|
65
|
+
});
|
|
66
|
+
if (!mountedRef.current) return null;
|
|
67
|
+
setResult(res);
|
|
68
|
+
optionsRef.current?.onSuccess?.(res);
|
|
69
|
+
return res;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (!mountedRef.current) return null;
|
|
72
|
+
if (err instanceof Error && err.message.includes('cancelled')) return null;
|
|
73
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
74
|
+
setError(e);
|
|
75
|
+
optionsRef.current?.onError?.(e);
|
|
76
|
+
return null;
|
|
77
|
+
} finally {
|
|
78
|
+
if (mountedRef.current) setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[apiKey],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const cancel = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
85
|
+
|
|
86
|
+
const reset = useCallback(() => {
|
|
87
|
+
abortRef.current?.abort();
|
|
88
|
+
setResult(null);
|
|
89
|
+
setError(null);
|
|
90
|
+
setIsLoading(false);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
return { result, isLoading, error, generate: run, cancel, reset };
|
|
94
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePrunaProxy Hook
|
|
3
|
+
* @description React hook for Pruna AI generation via proxy server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
7
|
+
import type { PrunaInput, PrunaResult, GenerateOptions } from '../core/entities/types';
|
|
8
|
+
|
|
9
|
+
export interface UsePrunaProxyOptions {
|
|
10
|
+
proxyUrl: string;
|
|
11
|
+
onSuccess?: (result: PrunaResult) => void;
|
|
12
|
+
onError?: (error: Error) => void;
|
|
13
|
+
onProgress?: (stage: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UsePrunaProxyReturn {
|
|
17
|
+
result: PrunaResult | null;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
error: Error | null;
|
|
20
|
+
generate: (input: PrunaInput) => Promise<PrunaResult | null>;
|
|
21
|
+
cancel: () => void;
|
|
22
|
+
reset: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* React hook for Pruna AI generation via proxy server.
|
|
27
|
+
* Use this when you need server-side API key security.
|
|
28
|
+
*/
|
|
29
|
+
export function usePrunaProxy(
|
|
30
|
+
options: UsePrunaProxyOptions,
|
|
31
|
+
): UsePrunaProxyReturn {
|
|
32
|
+
const { proxyUrl } = options;
|
|
33
|
+
const [result, setResult] = useState<PrunaResult | null>(null);
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
const [error, setError] = useState<Error | null>(null);
|
|
36
|
+
|
|
37
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
38
|
+
const optionsRef = useRef(options);
|
|
39
|
+
const mountedRef = useRef(true);
|
|
40
|
+
|
|
41
|
+
useEffect(() => { optionsRef.current = options; }, [options]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
mountedRef.current = true;
|
|
45
|
+
return () => {
|
|
46
|
+
mountedRef.current = false;
|
|
47
|
+
abortRef.current?.abort();
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const run = useCallback(
|
|
52
|
+
async (input: PrunaInput): Promise<PrunaResult | null> => {
|
|
53
|
+
abortRef.current?.abort();
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
abortRef.current = controller;
|
|
56
|
+
|
|
57
|
+
setIsLoading(true);
|
|
58
|
+
setError(null);
|
|
59
|
+
setResult(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
optionsRef.current?.onProgress?.('uploading');
|
|
63
|
+
|
|
64
|
+
const res = await fetch(proxyUrl, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify(input),
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
73
|
+
throw new Error((err as { message?: string }).message ?? `Proxy error: ${res.status}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data: PrunaResult = await res.json();
|
|
77
|
+
|
|
78
|
+
if (!mountedRef.current) return null;
|
|
79
|
+
setResult(data);
|
|
80
|
+
optionsRef.current?.onSuccess?.(data);
|
|
81
|
+
return data;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (!mountedRef.current) return null;
|
|
84
|
+
if (err instanceof Error && err.message.includes('cancelled')) return null;
|
|
85
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
86
|
+
setError(e);
|
|
87
|
+
optionsRef.current?.onError?.(e);
|
|
88
|
+
return null;
|
|
89
|
+
} finally {
|
|
90
|
+
if (mountedRef.current) setIsLoading(false);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[proxyUrl],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const cancel = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
97
|
+
|
|
98
|
+
const reset = useCallback(() => {
|
|
99
|
+
abortRef.current?.abort();
|
|
100
|
+
setResult(null);
|
|
101
|
+
setError(null);
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
return { result, isLoading, error, generate: run, cancel, reset };
|
|
106
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/pruna-provider
|
|
3
|
+
* Pruna AI generation client for web apps
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: Apps should NOT use this root barrel.
|
|
6
|
+
* Use subpath imports instead:
|
|
7
|
+
*
|
|
8
|
+
* - @umituz/pruna-provider/core — Types, constants, API client
|
|
9
|
+
* - @umituz/pruna-provider/generation — Generation functions
|
|
10
|
+
* - @umituz/pruna-provider/hooks — React hooks
|
|
11
|
+
*
|
|
12
|
+
* This root barrel is kept for backward compatibility only.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Re-export everything for backward compatibility
|
|
16
|
+
export * from './core';
|
|
17
|
+
export { generate, generateImageThenVideo } from './generation';
|
|
18
|
+
export { usePrunaGeneration, usePrunaProxy } from './hooks';
|