@umituz/react-native-ai-pruna-provider 1.0.11 → 1.0.13

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.11",
3
+ "version": "1.0.13",
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({ input }),
143
+ body: JSON.stringify(requestBody),
141
144
  signal,
142
145
  });
143
146
 
@@ -68,7 +68,8 @@ function buildImageEditInput(
68
68
  input: Record<string, unknown>,
69
69
  sessionId: string,
70
70
  ): Record<string, unknown> {
71
- // p-image-edit expects images array (base64 or HTTPS URLs file URIs resolved by ai-generation-content)
71
+ // p-image-edit expects images array (base64 with data URI prefix or HTTPS URLs)
72
+ // Base64 format: data:image/jpeg;base64,{base64_string}
72
73
  let images: string[];
73
74
 
74
75
  if (Array.isArray(input.images)) {
@@ -76,13 +77,13 @@ function buildImageEditInput(
76
77
  if (validImages.length === 0) {
77
78
  throw new Error("Image array is empty or contains no valid strings for p-image-edit.");
78
79
  }
79
- images = validImages.map(stripBase64Prefix);
80
+ images = validImages; // Keep data URI prefix for base64 images
80
81
  } else if (typeof input.image === 'string') {
81
- images = [stripBase64Prefix(input.image as string)];
82
+ images = [input.image as string]; // Keep data URI prefix for base64 images
82
83
  } else if (typeof input.image_url === 'string') {
83
- images = [stripBase64Prefix(input.image_url as string)];
84
+ images = [input.image_url as string]; // Keep data URI prefix for base64 images
84
85
  } else if (Array.isArray(input.image_urls)) {
85
- images = (input.image_urls as string[]).map(stripBase64Prefix);
86
+ images = input.image_urls as string[]; // Keep data URI prefix for base64 images
86
87
  } else {
87
88
  throw new Error("Image is required for p-image-edit. Provide 'image', 'images', 'image_url', or 'image_urls'.");
88
89
  }
@@ -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
+ }