@umituz/react-native-ai-pruna-provider 1.0.11 → 1.0.12
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/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @umituz/react-native-ai-pruna-provider
|
|
2
|
+
|
|
3
|
+
Pruna AI provider for React Native - implements `IAIProvider` interface for unified AI generation.
|
|
4
|
+
|
|
5
|
+
## Supported Models
|
|
6
|
+
|
|
7
|
+
- **p-image**: Text-to-image generation ($0.005/run)
|
|
8
|
+
- **p-image-edit**: Image-to-image editing ($0.010/run)
|
|
9
|
+
- **p-video**: Image-to-video generation with draft mode support
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @umituz/react-native-ai-pruna-provider
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { initializePrunaProvider } from '@umituz/react-native-ai-pruna-provider';
|
|
21
|
+
|
|
22
|
+
// Initialize at app startup
|
|
23
|
+
initializePrunaProvider({
|
|
24
|
+
apiKey: process.env.PRUNA_API_KEY,
|
|
25
|
+
setAsActive: true,
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Text-to-Image (p-image)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { prunaProvider } from '@umituz/react-native-ai-pruna-provider';
|
|
35
|
+
|
|
36
|
+
const result = await prunaProvider.subscribe('p-image', {
|
|
37
|
+
prompt: 'A sunset over the ocean',
|
|
38
|
+
aspect_ratio: '16:9',
|
|
39
|
+
});
|
|
40
|
+
console.log(result.url);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Image Editing (p-image-edit)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const result = await prunaProvider.subscribe('p-image-edit', {
|
|
47
|
+
images: ['data:image/jpeg;base64,...'],
|
|
48
|
+
prompt: 'Make it look like a painting',
|
|
49
|
+
aspect_ratio: '1:1',
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Image-to-Video (p-video)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const result = await prunaProvider.subscribe('p-video', {
|
|
57
|
+
image: 'data:image/jpeg;base64,...',
|
|
58
|
+
prompt: 'The camera pans slowly from left to right',
|
|
59
|
+
duration: 10,
|
|
60
|
+
resolution: '1080p',
|
|
61
|
+
fps: 24,
|
|
62
|
+
draft: true, // Enable draft mode
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Draft Mode (p-video)
|
|
67
|
+
|
|
68
|
+
Draft mode is a faster, more cost-effective way to generate videos for testing and iteration.
|
|
69
|
+
|
|
70
|
+
### What is Draft Mode?
|
|
71
|
+
|
|
72
|
+
- **50% cheaper** than normal mode
|
|
73
|
+
- **4x faster** generation time
|
|
74
|
+
- **Lower quality** output (suitable for previews)
|
|
75
|
+
- **Same resolution** options (720p, 1080p)
|
|
76
|
+
|
|
77
|
+
### Pricing
|
|
78
|
+
|
|
79
|
+
| Resolution | Normal Mode | Draft Mode | Savings |
|
|
80
|
+
|------------|-------------|------------|---------|
|
|
81
|
+
| 720p | $0.05/sec | $0.025/sec | 50% |
|
|
82
|
+
| 1080p | $0.08/sec | $0.04/sec | 50% |
|
|
83
|
+
|
|
84
|
+
### When to Use Draft Mode
|
|
85
|
+
|
|
86
|
+
✅ **Recommended for:**
|
|
87
|
+
- Testing prompts and concepts
|
|
88
|
+
- Iterating on video generation
|
|
89
|
+
- Previewing before final generation
|
|
90
|
+
- Longer videos (8+ seconds)
|
|
91
|
+
- High resolution (1080p)
|
|
92
|
+
|
|
93
|
+
❌ **Not recommended for:**
|
|
94
|
+
- Final production videos
|
|
95
|
+
- Social media sharing
|
|
96
|
+
- Client deliverables
|
|
97
|
+
|
|
98
|
+
### Draft Mode Example
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Test with draft mode (fast & cheap)
|
|
102
|
+
const draftResult = await prunaProvider.subscribe('p-video', {
|
|
103
|
+
image: base64Image,
|
|
104
|
+
prompt: 'Person walking on beach',
|
|
105
|
+
duration: 10,
|
|
106
|
+
resolution: '1080p',
|
|
107
|
+
draft: true, // Enable draft mode
|
|
108
|
+
});
|
|
109
|
+
// Cost: 10 × $0.04 = $0.40
|
|
110
|
+
|
|
111
|
+
// Generate final version with normal mode
|
|
112
|
+
const finalResult = await prunaProvider.subscribe('p-video', {
|
|
113
|
+
image: base64Image,
|
|
114
|
+
prompt: 'Person walking on beach',
|
|
115
|
+
duration: 10,
|
|
116
|
+
resolution: '1080p',
|
|
117
|
+
draft: false, // Normal mode (default)
|
|
118
|
+
});
|
|
119
|
+
// Cost: 10 × $0.08 = $0.80
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### PrunaProvider
|
|
125
|
+
|
|
126
|
+
Implements `IAIProvider` interface.
|
|
127
|
+
|
|
128
|
+
#### Methods
|
|
129
|
+
|
|
130
|
+
- `initialize(config: AIProviderConfig): void` - Initialize provider with API key
|
|
131
|
+
- `isInitialized(): boolean` - Check if provider is initialized
|
|
132
|
+
- `getCapabilities(): ProviderCapabilities` - Get provider capabilities
|
|
133
|
+
- `subscribe<T>(model, input, options?): Promise<T>` - Subscribe to generation result
|
|
134
|
+
- `run<T>(model, input, options?): Promise<T>` - Run generation once
|
|
135
|
+
- `submitJob(model, input): Promise<JobSubmission>` - Submit async job
|
|
136
|
+
- `getJobStatus(model, requestId): Promise<JobStatus>` - Get job status
|
|
137
|
+
- `getJobResult<T>(model, requestId): Promise<T>` - Get job result
|
|
138
|
+
- `reset(): void` - Reset provider state
|
|
139
|
+
- `cancelCurrentRequest(): void` - Cancel current request
|
|
140
|
+
- `hasRunningRequest(): boolean` - Check if request is running
|
|
141
|
+
- `getSessionLogs(sessionId): LogEntry[]` - Get session logs
|
|
142
|
+
- `endLogSession(sessionId): LogEntry[]` - End log session
|
|
143
|
+
|
|
144
|
+
### usePrunaGeneration Hook
|
|
145
|
+
|
|
146
|
+
React hook for Pruna AI generation operations.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { usePrunaGeneration } from '@umituz/react-native-ai-pruna-provider';
|
|
150
|
+
|
|
151
|
+
const {
|
|
152
|
+
data,
|
|
153
|
+
error,
|
|
154
|
+
isLoading,
|
|
155
|
+
isRetryable,
|
|
156
|
+
requestId,
|
|
157
|
+
generate,
|
|
158
|
+
retry,
|
|
159
|
+
cancel,
|
|
160
|
+
reset,
|
|
161
|
+
} = usePrunaGeneration<{
|
|
162
|
+
url: string;
|
|
163
|
+
generation_url: string;
|
|
164
|
+
}>({
|
|
165
|
+
timeoutMs: 120000,
|
|
166
|
+
onProgress: (status) => console.log(status),
|
|
167
|
+
onError: (error) => console.error(error),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Generate video
|
|
171
|
+
await generate('p-video', {
|
|
172
|
+
image: base64Image,
|
|
173
|
+
prompt: 'Camera pans from left to right',
|
|
174
|
+
duration: 10,
|
|
175
|
+
resolution: '1080p',
|
|
176
|
+
draft: true,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Type Exports
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Core types
|
|
184
|
+
export type { PrunaModelId, PrunaResolution, PrunaAspectRatio };
|
|
185
|
+
export type { PrunaPredictionInput, PrunaPredictionResponse };
|
|
186
|
+
export type { PrunaConfig, PrunaErrorType, PrunaErrorInfo };
|
|
187
|
+
|
|
188
|
+
// Draft mode utilities
|
|
189
|
+
export {
|
|
190
|
+
validateDraftModeParams,
|
|
191
|
+
calculateDraftModeDiscount,
|
|
192
|
+
getDraftModeDescription,
|
|
193
|
+
recommendDraftMode,
|
|
194
|
+
calculateDraftModeSavings,
|
|
195
|
+
getPricingPerSecond,
|
|
196
|
+
formatPriceUSD,
|
|
197
|
+
compareDraftModePricing,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Constants
|
|
201
|
+
export {
|
|
202
|
+
P_VIDEO_PRICING,
|
|
203
|
+
DRAFT_MODE_CONFIG,
|
|
204
|
+
P_VIDEO_DEFAULTS,
|
|
205
|
+
PRUNA_CAPABILITIES,
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Error Handling
|
|
210
|
+
|
|
211
|
+
The provider provides typed error information:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
try {
|
|
215
|
+
await prunaProvider.subscribe('p-video', input);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error.retryable) {
|
|
218
|
+
// Retry the request
|
|
219
|
+
} else {
|
|
220
|
+
// Handle error
|
|
221
|
+
console.error(error.message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Error Types
|
|
227
|
+
|
|
228
|
+
- `AUTHENTICATION` - Invalid API key
|
|
229
|
+
- `RATE_LIMIT` - Too many requests
|
|
230
|
+
- `NETWORK` - Network error
|
|
231
|
+
- `TIMEOUT` - Request timeout
|
|
232
|
+
- `VALIDATION` - Invalid input parameters
|
|
233
|
+
- `QUOTA` - Credit/quota exceeded
|
|
234
|
+
- `SERVER` - Server error (5xx)
|
|
235
|
+
- `UNKNOWN` - Unknown error
|
|
236
|
+
|
|
237
|
+
## Initialization
|
|
238
|
+
|
|
239
|
+
### Direct Initialization
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { initializePrunaProvider } from '@umituz/react-native-ai-pruna-provider';
|
|
243
|
+
|
|
244
|
+
export const configureAIServices = (): void => {
|
|
245
|
+
initializePrunaProvider({
|
|
246
|
+
apiKey: getPrunaApiKey(),
|
|
247
|
+
setAsActive: true,
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Init Module Factory
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { createAiProviderInitModule } from '@umituz/react-native-ai-pruna-provider';
|
|
256
|
+
|
|
257
|
+
const prunaInitModule = createAiProviderInitModule({
|
|
258
|
+
apiKey: getPrunaApiKey(),
|
|
259
|
+
setAsActive: true,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Register with app initialization
|
|
263
|
+
appRegistry.registerModule('pruna-ai', prunaInitModule);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Best Practices
|
|
267
|
+
|
|
268
|
+
1. **Use Draft Mode for Testing**: Always test prompts with draft mode before generating final videos
|
|
269
|
+
2. **Handle Errors Gracefully**: Check `error.retryable` before retrying requests
|
|
270
|
+
3. **Monitor Progress**: Use `onProgress` callback to track generation status
|
|
271
|
+
4. **Clean Up Resources**: Call `reset()` when done to clean up request store
|
|
272
|
+
5. **Set Timeouts**: Use appropriate timeout values (120s for images, 300s for videos)
|
|
273
|
+
|
|
274
|
+
## Example: Complete Workflow
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { prunaProvider, usePrunaGeneration } from '@umituz/react-native-ai-pruna-provider';
|
|
278
|
+
|
|
279
|
+
function VideoGenerator() {
|
|
280
|
+
const { generate, data, isLoading, error } = usePrunaGeneration<{ url: string }>();
|
|
281
|
+
|
|
282
|
+
const handleGenerate = async () => {
|
|
283
|
+
// Step 1: Test with draft mode
|
|
284
|
+
const draftResult = await generate('p-video', {
|
|
285
|
+
image: base64Image,
|
|
286
|
+
prompt: 'Person walking on beach',
|
|
287
|
+
duration: 10,
|
|
288
|
+
resolution: '1080p',
|
|
289
|
+
draft: true,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (draftResult) {
|
|
293
|
+
console.log('Draft preview:', draftResult.url);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Step 2: Generate final version
|
|
297
|
+
const finalResult = await generate('p-video', {
|
|
298
|
+
image: base64Image,
|
|
299
|
+
prompt: 'Person walking on beach',
|
|
300
|
+
duration: 10,
|
|
301
|
+
resolution: '1080p',
|
|
302
|
+
draft: false,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
console.log('Final video:', finalResult.url);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<View>
|
|
310
|
+
<Button onPress={handleGenerate} disabled={isLoading}>
|
|
311
|
+
Generate Video
|
|
312
|
+
</Button>
|
|
313
|
+
{error && <Text>Error: {error.message}</Text>}
|
|
314
|
+
{data && <Video source={{ uri: data.url }} />}
|
|
315
|
+
</View>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
MIT
|
|
323
|
+
|
|
324
|
+
## Author
|
|
325
|
+
|
|
326
|
+
Umit UZ <umit@umituz.com>
|
|
327
|
+
|
|
328
|
+
## Repository
|
|
329
|
+
|
|
330
|
+
https://github.com/umituz/react-native-ai-pruna-provider
|
|
331
|
+
|
|
332
|
+
## See Also
|
|
333
|
+
|
|
334
|
+
- [@umituz/react-native-ai-generation-content](https://github.com/umituz/react-native-ai-generation-content) - Unified AI generation framework
|
|
335
|
+
- [Pruna AI Documentation](https://docs.pruna.ai/) - Official Pruna AI docs
|
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.12",
|
|
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",
|
|
@@ -44,4 +44,17 @@ export {
|
|
|
44
44
|
VALID_PRUNA_MODELS,
|
|
45
45
|
P_VIDEO_DEFAULTS,
|
|
46
46
|
DEFAULT_ASPECT_RATIO,
|
|
47
|
+
P_VIDEO_PRICING,
|
|
48
|
+
DRAFT_MODE_CONFIG,
|
|
47
49
|
} from "../infrastructure/services/pruna-provider.constants";
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
validateDraftModeParams,
|
|
53
|
+
calculateDraftModeDiscount,
|
|
54
|
+
getDraftModeDescription,
|
|
55
|
+
recommendDraftMode,
|
|
56
|
+
calculateDraftModeSavings,
|
|
57
|
+
getPricingPerSecond,
|
|
58
|
+
formatPriceUSD,
|
|
59
|
+
compareDraftModePricing,
|
|
60
|
+
} from "../infrastructure/utils/pruna-draft-mode.util";
|
|
@@ -129,6 +129,9 @@ export async function submitPrediction(
|
|
|
129
129
|
|
|
130
130
|
const startTime = Date.now();
|
|
131
131
|
|
|
132
|
+
const requestBody = { input };
|
|
133
|
+
generationLogCollector.log(sessionId, TAG, `Request body: ${JSON.stringify(requestBody).substring(0, 200)}...`);
|
|
134
|
+
|
|
132
135
|
const response = await fetch(PRUNA_PREDICTIONS_URL, {
|
|
133
136
|
method: 'POST',
|
|
134
137
|
headers: {
|
|
@@ -137,7 +140,7 @@ export async function submitPrediction(
|
|
|
137
140
|
'Try-Sync': 'true',
|
|
138
141
|
'Content-Type': 'application/json',
|
|
139
142
|
},
|
|
140
|
-
body: JSON.stringify(
|
|
143
|
+
body: JSON.stringify(requestBody),
|
|
141
144
|
signal,
|
|
142
145
|
});
|
|
143
146
|
|
|
@@ -81,3 +81,43 @@ export const P_VIDEO_DEFAULTS = {
|
|
|
81
81
|
|
|
82
82
|
/** Default aspect ratio — 1:1 is neutral for portrait/landscape input photos */
|
|
83
83
|
export const DEFAULT_ASPECT_RATIO = '1:1' as const;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* P-Video Pricing Configuration
|
|
87
|
+
*
|
|
88
|
+
* Pricing is per second of generated video.
|
|
89
|
+
* Draft mode provides 50% discount on normal pricing.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // 10 seconds at 720p normal mode:
|
|
93
|
+
* 10 × $0.05 = $0.50
|
|
94
|
+
*
|
|
95
|
+
* // 10 seconds at 720p draft mode:
|
|
96
|
+
* 10 × $0.025 = $0.25 (50% savings)
|
|
97
|
+
*/
|
|
98
|
+
export const P_VIDEO_PRICING = {
|
|
99
|
+
/** Normal mode pricing (USD per second) */
|
|
100
|
+
normal: {
|
|
101
|
+
'720p': 0.05,
|
|
102
|
+
'1080p': 0.08,
|
|
103
|
+
} as const,
|
|
104
|
+
/** Draft mode pricing (USD per second) - 50% discount */
|
|
105
|
+
draft: {
|
|
106
|
+
'720p': 0.025,
|
|
107
|
+
'1080p': 0.04,
|
|
108
|
+
} as const,
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Draft Mode Configuration
|
|
113
|
+
*
|
|
114
|
+
* Controls draft mode behavior and recommendations.
|
|
115
|
+
*/
|
|
116
|
+
export const DRAFT_MODE_CONFIG = {
|
|
117
|
+
/** Discount multiplier (0.5 = 50% off normal pricing) */
|
|
118
|
+
discountMultiplier: 0.5,
|
|
119
|
+
/** Maximum duration to recommend draft mode */
|
|
120
|
+
maxRecommendDuration: 8,
|
|
121
|
+
/** Resolutions eligible for draft recommendation */
|
|
122
|
+
eligibleResolutions: ['720p', '1080p'] as const,
|
|
123
|
+
} as const;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pruna Draft Mode Utilities
|
|
3
|
+
*
|
|
4
|
+
* Draft mode is a faster, more cost-effective way to generate videos for testing and iteration.
|
|
5
|
+
* - 50% cheaper than normal mode
|
|
6
|
+
* - Faster generation time
|
|
7
|
+
* - Lower quality output (suitable for previews)
|
|
8
|
+
* - Same resolution options (720p, 1080p)
|
|
9
|
+
*
|
|
10
|
+
* When to use draft mode:
|
|
11
|
+
* ✅ Testing prompts and concepts
|
|
12
|
+
* ✅ Iterating on video generation
|
|
13
|
+
* ✅ Previewing before final generation
|
|
14
|
+
* ✅ Longer videos (8+ seconds)
|
|
15
|
+
* ✅ High resolution (1080p)
|
|
16
|
+
*
|
|
17
|
+
* When NOT to use draft mode:
|
|
18
|
+
* ❌ Final production videos
|
|
19
|
+
* ❌ Social media sharing
|
|
20
|
+
* ❌ Client deliverables
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { PrunaResolution } from "../../domain/entities/pruna.types";
|
|
24
|
+
import { DRAFT_MODE_CONFIG, P_VIDEO_PRICING } from "../services/pruna-provider.constants";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates draft mode parameters for p-video
|
|
28
|
+
*
|
|
29
|
+
* @param params - Parameters to validate
|
|
30
|
+
* @returns Validation result with optional error message
|
|
31
|
+
*/
|
|
32
|
+
export function validateDraftModeParams(params: {
|
|
33
|
+
duration: number;
|
|
34
|
+
resolution: string;
|
|
35
|
+
draft?: boolean;
|
|
36
|
+
}): { valid: boolean; error?: string } {
|
|
37
|
+
const { duration, resolution, draft } = params;
|
|
38
|
+
|
|
39
|
+
// Validate duration
|
|
40
|
+
if (typeof duration !== "number" || duration < 1 || duration > 15) {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
error: "Duration must be between 1 and 15 seconds",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Validate resolution
|
|
48
|
+
const validResolutions: PrunaResolution[] = ["720p", "1080p"];
|
|
49
|
+
if (!validResolutions.includes(resolution as PrunaResolution)) {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
error: `Resolution must be one of: ${validResolutions.join(", ")}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Draft mode is optional and defaults to false
|
|
57
|
+
if (draft !== undefined && typeof draft !== "boolean") {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
error: "Draft must be a boolean value",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { valid: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculates pricing discount for draft mode
|
|
69
|
+
*
|
|
70
|
+
* Draft mode provides 50% discount on normal pricing.
|
|
71
|
+
*
|
|
72
|
+
* @param normalPrice - Normal mode price in USD
|
|
73
|
+
* @param draft - Whether draft mode is enabled
|
|
74
|
+
* @returns Discounted price
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* calculateDraftModeDiscount(0.10, true) // Returns 0.05
|
|
78
|
+
* calculateDraftModeDiscount(0.10, false) // Returns 0.10
|
|
79
|
+
*/
|
|
80
|
+
export function calculateDraftModeDiscount(
|
|
81
|
+
normalPrice: number,
|
|
82
|
+
draft: boolean,
|
|
83
|
+
): number {
|
|
84
|
+
if (!draft) {
|
|
85
|
+
return normalPrice;
|
|
86
|
+
}
|
|
87
|
+
return normalPrice * DRAFT_MODE_CONFIG.discountMultiplier;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns user-friendly description of draft mode
|
|
92
|
+
*
|
|
93
|
+
* @returns Draft mode description
|
|
94
|
+
*/
|
|
95
|
+
export function getDraftModeDescription(): string {
|
|
96
|
+
return (
|
|
97
|
+
"Draft mode generates videos faster and at 50% cost. " +
|
|
98
|
+
"Quality is lower than normal mode, making it ideal for " +
|
|
99
|
+
"testing prompts and iterating before generating final videos."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Determines if draft mode is recommended for given parameters
|
|
105
|
+
*
|
|
106
|
+
* Recommends draft mode for:
|
|
107
|
+
* - Longer durations (8+ seconds)
|
|
108
|
+
* - High resolutions (1080p)
|
|
109
|
+
*
|
|
110
|
+
* @param params - Video parameters
|
|
111
|
+
* @returns Whether draft mode is recommended
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* recommendDraftMode({ duration: 10, resolution: "1080p" }) // Returns true
|
|
115
|
+
* recommendDraftMode({ duration: 5, resolution: "720p" }) // Returns false
|
|
116
|
+
*/
|
|
117
|
+
export function recommendDraftMode(params: {
|
|
118
|
+
duration: number;
|
|
119
|
+
resolution: string;
|
|
120
|
+
}): boolean {
|
|
121
|
+
const { duration, resolution } = params;
|
|
122
|
+
|
|
123
|
+
// Recommend for longer durations
|
|
124
|
+
if (duration >= DRAFT_MODE_CONFIG.maxRecommendDuration) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Recommend for high resolutions
|
|
129
|
+
if (resolution === "1080p") {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Calculates the cost savings when using draft mode
|
|
138
|
+
*
|
|
139
|
+
* @param duration - Video duration in seconds
|
|
140
|
+
* @param resolution - Video resolution
|
|
141
|
+
* @returns Savings amount in USD
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* calculateDraftModeSavings(10, "1080p") // Returns 0.40 (50% off $0.80)
|
|
145
|
+
*/
|
|
146
|
+
export function calculateDraftModeSavings(
|
|
147
|
+
duration: number,
|
|
148
|
+
resolution: string,
|
|
149
|
+
): number {
|
|
150
|
+
const normalPrice = (P_VIDEO_PRICING.normal[resolution as keyof typeof P_VIDEO_PRICING.normal] || 0) * duration;
|
|
151
|
+
const draftPrice = (P_VIDEO_PRICING.draft[resolution as keyof typeof P_VIDEO_PRICING.draft] || 0) * duration;
|
|
152
|
+
return normalPrice - draftPrice;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets pricing information for a given resolution and mode
|
|
157
|
+
*
|
|
158
|
+
* @param resolution - Video resolution
|
|
159
|
+
* @param draft - Whether draft mode is enabled
|
|
160
|
+
* @returns Price per second in USD
|
|
161
|
+
*/
|
|
162
|
+
export function getPricingPerSecond(
|
|
163
|
+
resolution: string,
|
|
164
|
+
draft: boolean,
|
|
165
|
+
): number {
|
|
166
|
+
if (draft) {
|
|
167
|
+
return P_VIDEO_PRICING.draft[resolution as keyof typeof P_VIDEO_PRICING.draft] || 0;
|
|
168
|
+
}
|
|
169
|
+
return P_VIDEO_PRICING.normal[resolution as keyof typeof P_VIDEO_PRICING.normal] || 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Formats price as USD string
|
|
174
|
+
*
|
|
175
|
+
* @param price - Price in USD
|
|
176
|
+
* @returns Formatted price string
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* formatPriceUSD(0.05) // Returns "$0.05"
|
|
180
|
+
* formatPriceUSD(0.10) // Returns "$0.10"
|
|
181
|
+
*/
|
|
182
|
+
export function formatPriceUSD(price: number): string {
|
|
183
|
+
return `$${price.toFixed(3)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Compares draft vs normal mode pricing for a video
|
|
188
|
+
*
|
|
189
|
+
* @param duration - Video duration in seconds
|
|
190
|
+
* @param resolution - Video resolution
|
|
191
|
+
* @returns Comparison object with prices and savings
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* compareDraftModePricing(10, "1080p")
|
|
195
|
+
* // Returns:
|
|
196
|
+
* // {
|
|
197
|
+
* // normalPrice: 0.80,
|
|
198
|
+
* // draftPrice: 0.40,
|
|
199
|
+
* // savings: 0.40,
|
|
200
|
+
* // discountPercent: 50
|
|
201
|
+
* // }
|
|
202
|
+
*/
|
|
203
|
+
export function compareDraftModePricing(
|
|
204
|
+
duration: number,
|
|
205
|
+
resolution: string,
|
|
206
|
+
): {
|
|
207
|
+
normalPrice: number;
|
|
208
|
+
draftPrice: number;
|
|
209
|
+
savings: number;
|
|
210
|
+
discountPercent: number;
|
|
211
|
+
} {
|
|
212
|
+
const normalPrice = getPricingPerSecond(resolution, false) * duration;
|
|
213
|
+
const draftPrice = getPricingPerSecond(resolution, true) * duration;
|
|
214
|
+
const savings = normalPrice - draftPrice;
|
|
215
|
+
const discountPercent = Math.round((savings / normalPrice) * 100);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
normalPrice,
|
|
219
|
+
draftPrice,
|
|
220
|
+
savings,
|
|
221
|
+
discountPercent,
|
|
222
|
+
};
|
|
223
|
+
}
|