@primestyleai/tryon 1.0.0 → 1.1.0
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 +332 -0
- package/dist/image-utils-usff6Qu8.js +186 -0
- package/dist/{primestyle-tryon.es.js → primestyle-tryon.js} +3 -180
- package/dist/react/index.d.ts +33 -0
- package/dist/react/index.js +666 -0
- package/package.json +27 -9
- package/dist/primestyle-tryon.umd.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://primestyleai.com/logo-gold.svg" alt="PrimeStyle AI" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@primestyleai/tryon</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>AI-Powered Virtual Try-On for React & Next.js</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
Add a virtual try-on button to your product pages in 3 steps. Customers upload a photo and see how clothes look on them — powered by PrimeStyle AI.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/@primestyleai/tryon"><img src="https://img.shields.io/npm/v/@primestyleai/tryon?color=D6BA7D&label=npm" alt="npm version" /></a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/@primestyleai/tryon"><img src="https://img.shields.io/npm/dm/@primestyleai/tryon?color=D6BA7D" alt="npm downloads" /></a>
|
|
18
|
+
<a href="https://github.com/primestyleai/tryon-sdk/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@primestyleai/tryon?color=D6BA7D" alt="license" /></a>
|
|
19
|
+
<img src="https://img.shields.io/badge/gzip-5.5kB-green" alt="bundle size" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<a href="https://primestyleai.com/developer/demo">Live Demo</a> •
|
|
24
|
+
<a href="https://primestyleai.com/docs">Documentation</a> •
|
|
25
|
+
<a href="https://primestyleai.com/dashboard/developer/keys">Get API Key</a> •
|
|
26
|
+
<a href="https://primestyleai.com/pricing">Pricing</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## How It Works
|
|
32
|
+
|
|
33
|
+
1. Customer clicks the **"Virtual Try-On"** button on your product page
|
|
34
|
+
2. They upload a photo of themselves
|
|
35
|
+
3. PrimeStyle AI generates a realistic try-on image in ~15 seconds
|
|
36
|
+
4. Customer sees how the garment looks on them before buying
|
|
37
|
+
|
|
38
|
+
The entire flow happens inside a beautiful modal — no redirects, no iframes.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### 1. Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @primestyleai/tryon
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Set Your API Key
|
|
51
|
+
|
|
52
|
+
Get your key from the [Developer Dashboard](https://primestyleai.com/dashboard/developer/keys) and add it to your `.env.local`:
|
|
53
|
+
|
|
54
|
+
```env
|
|
55
|
+
NEXT_PUBLIC_PRIMESTYLE_API_KEY=ps_live_your_key_here
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Use the Component
|
|
59
|
+
|
|
60
|
+
```jsx
|
|
61
|
+
import { PrimeStyleTryon } from '@primestyleai/tryon/react';
|
|
62
|
+
|
|
63
|
+
function ProductPage({ product }) {
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
<h1>{product.name}</h1>
|
|
67
|
+
<img src={product.image} alt={product.name} />
|
|
68
|
+
|
|
69
|
+
<PrimeStyleTryon
|
|
70
|
+
productImage={product.image}
|
|
71
|
+
buttonText="Try It On"
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That's it. No API key prop needed — the component reads it from your environment automatically.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Props
|
|
83
|
+
|
|
84
|
+
| Prop | Type | Default | Description |
|
|
85
|
+
|------|------|---------|-------------|
|
|
86
|
+
| `productImage` | `string` | **required** | URL of the garment image |
|
|
87
|
+
| `buttonText` | `string` | `"Virtual Try-On"` | Text on the trigger button |
|
|
88
|
+
| `apiUrl` | `string` | `NEXT_PUBLIC_PRIMESTYLE_API_URL` or production | API endpoint override |
|
|
89
|
+
| `showPoweredBy` | `boolean` | `true` | Show "Powered by PrimeStyle AI" in modal |
|
|
90
|
+
| `buttonStyles` | `ButtonStyles` | `{}` | Customize button appearance |
|
|
91
|
+
| `modalStyles` | `ModalStyles` | `{}` | Customize modal appearance |
|
|
92
|
+
| `className` | `string` | — | Additional CSS class on wrapper |
|
|
93
|
+
| `style` | `CSSProperties` | — | Inline styles on wrapper |
|
|
94
|
+
| `onOpen` | `() => void` | — | Modal opened |
|
|
95
|
+
| `onClose` | `() => void` | — | Modal closed |
|
|
96
|
+
| `onUpload` | `(file: File) => void` | — | User uploaded a photo |
|
|
97
|
+
| `onProcessing` | `(jobId: string) => void` | — | Try-on generation started |
|
|
98
|
+
| `onComplete` | `(result) => void` | — | Result ready: `{ jobId, imageUrl }` |
|
|
99
|
+
| `onError` | `(error) => void` | — | Error occurred: `{ message, code? }` |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Customization
|
|
104
|
+
|
|
105
|
+
### Button Styles
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
<PrimeStyleTryon
|
|
109
|
+
productImage={product.image}
|
|
110
|
+
buttonStyles={{
|
|
111
|
+
backgroundColor: '#000000',
|
|
112
|
+
textColor: '#ffffff',
|
|
113
|
+
borderRadius: '50px',
|
|
114
|
+
padding: '16px 32px',
|
|
115
|
+
fontSize: '16px',
|
|
116
|
+
fontWeight: '700',
|
|
117
|
+
width: '100%',
|
|
118
|
+
border: '2px solid #333',
|
|
119
|
+
hoverBackgroundColor: '#222',
|
|
120
|
+
iconSize: '20px',
|
|
121
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**All ButtonStyles properties:**
|
|
127
|
+
|
|
128
|
+
| Property | Description |
|
|
129
|
+
|----------|-------------|
|
|
130
|
+
| `backgroundColor` | Button background color |
|
|
131
|
+
| `textColor` | Button text color |
|
|
132
|
+
| `borderRadius` | Corner rounding |
|
|
133
|
+
| `fontSize` | Text size |
|
|
134
|
+
| `fontFamily` | Font stack |
|
|
135
|
+
| `fontWeight` | Font weight |
|
|
136
|
+
| `padding` | Inner spacing |
|
|
137
|
+
| `border` | Border style |
|
|
138
|
+
| `width` | Button width (e.g. `"100%"`) |
|
|
139
|
+
| `height` | Button height |
|
|
140
|
+
| `hoverBackgroundColor` | Background on hover |
|
|
141
|
+
| `hoverTextColor` | Text color on hover |
|
|
142
|
+
| `iconSize` | Camera icon size |
|
|
143
|
+
| `iconColor` | Camera icon color |
|
|
144
|
+
| `boxShadow` | Shadow effect |
|
|
145
|
+
|
|
146
|
+
### Modal Styles
|
|
147
|
+
|
|
148
|
+
```jsx
|
|
149
|
+
<PrimeStyleTryon
|
|
150
|
+
productImage={product.image}
|
|
151
|
+
modalStyles={{
|
|
152
|
+
backgroundColor: '#ffffff',
|
|
153
|
+
textColor: '#111111',
|
|
154
|
+
overlayColor: 'rgba(0,0,0,0.7)',
|
|
155
|
+
borderRadius: '16px',
|
|
156
|
+
maxWidth: '520px',
|
|
157
|
+
headerBackgroundColor: '#f5f5f5',
|
|
158
|
+
headerTextColor: '#111111',
|
|
159
|
+
primaryButtonBackgroundColor: '#000000',
|
|
160
|
+
primaryButtonTextColor: '#ffffff',
|
|
161
|
+
loaderColor: '#000000',
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**All ModalStyles properties:**
|
|
167
|
+
|
|
168
|
+
| Property | Description |
|
|
169
|
+
|----------|-------------|
|
|
170
|
+
| `overlayColor` | Background overlay color |
|
|
171
|
+
| `backgroundColor` | Modal background |
|
|
172
|
+
| `textColor` | Modal text color |
|
|
173
|
+
| `borderRadius` | Modal corner rounding |
|
|
174
|
+
| `width` / `maxWidth` | Modal dimensions |
|
|
175
|
+
| `fontFamily` | Modal font stack |
|
|
176
|
+
| `headerBackgroundColor` | Header section background |
|
|
177
|
+
| `headerTextColor` | Header title color |
|
|
178
|
+
| `closeButtonColor` | Close (X) button color |
|
|
179
|
+
| `uploadBorderColor` | Upload drop zone border |
|
|
180
|
+
| `uploadBackgroundColor` | Upload zone background |
|
|
181
|
+
| `uploadTextColor` | Upload zone text |
|
|
182
|
+
| `uploadIconColor` | Upload icon color |
|
|
183
|
+
| `primaryButtonBackgroundColor` | Submit / download button background |
|
|
184
|
+
| `primaryButtonTextColor` | Submit / download button text |
|
|
185
|
+
| `primaryButtonBorderRadius` | Submit button rounding |
|
|
186
|
+
| `loaderColor` | Loading spinner color |
|
|
187
|
+
| `resultBorderRadius` | Result image corner rounding |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Callbacks
|
|
192
|
+
|
|
193
|
+
```jsx
|
|
194
|
+
<PrimeStyleTryon
|
|
195
|
+
productImage={product.image}
|
|
196
|
+
onOpen={() => {
|
|
197
|
+
analytics.track('tryon_opened');
|
|
198
|
+
}}
|
|
199
|
+
onUpload={(file) => {
|
|
200
|
+
console.log('Photo uploaded:', file.name);
|
|
201
|
+
}}
|
|
202
|
+
onProcessing={(jobId) => {
|
|
203
|
+
console.log('Processing:', jobId);
|
|
204
|
+
}}
|
|
205
|
+
onComplete={(result) => {
|
|
206
|
+
console.log('Result ready:', result.imageUrl);
|
|
207
|
+
analytics.track('tryon_completed', { jobId: result.jobId });
|
|
208
|
+
}}
|
|
209
|
+
onError={(error) => {
|
|
210
|
+
console.error('Try-on failed:', error.message);
|
|
211
|
+
// error.code: 'INSUFFICIENT_TOKENS' | 'API_ERROR' | etc.
|
|
212
|
+
}}
|
|
213
|
+
onClose={() => {
|
|
214
|
+
console.log('Modal closed');
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Environment Variables
|
|
222
|
+
|
|
223
|
+
| Variable | Required | Default | Description |
|
|
224
|
+
|----------|----------|---------|-------------|
|
|
225
|
+
| `NEXT_PUBLIC_PRIMESTYLE_API_KEY` | Yes | — | Your PrimeStyle API key |
|
|
226
|
+
| `NEXT_PUBLIC_PRIMESTYLE_API_URL` | No | `https://api.primestyleai.com` | Custom API endpoint |
|
|
227
|
+
|
|
228
|
+
The component reads these automatically — no need to pass them as props.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Tokens & Pricing
|
|
233
|
+
|
|
234
|
+
Each virtual try-on consumes **tokens** from your account balance.
|
|
235
|
+
|
|
236
|
+
| | |
|
|
237
|
+
|---|---|
|
|
238
|
+
| **Free trial** | Tokens included on signup |
|
|
239
|
+
| **Token packs** | Pay-as-you-go, never expire |
|
|
240
|
+
| **Monthly plans** | Volume discounts for high-traffic stores |
|
|
241
|
+
|
|
242
|
+
Manage your balance and purchase tokens at [primestyleai.com/dashboard/billing](https://primestyleai.com/dashboard/billing).
|
|
243
|
+
|
|
244
|
+
If a try-on fails, tokens are **automatically refunded** to your account.
|
|
245
|
+
|
|
246
|
+
The `onComplete` callback includes token info:
|
|
247
|
+
|
|
248
|
+
```jsx
|
|
249
|
+
onComplete={(result) => {
|
|
250
|
+
// result.jobId — the job ID
|
|
251
|
+
// result.imageUrl — the result image URL
|
|
252
|
+
}}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## API Key Management
|
|
258
|
+
|
|
259
|
+
Create and manage your API keys at the [Developer Dashboard](https://primestyleai.com/dashboard/developer/keys).
|
|
260
|
+
|
|
261
|
+
- Keys start with `ps_live_` and are shown **once** at creation
|
|
262
|
+
- Set **allowed domains** to restrict where your key can be used
|
|
263
|
+
- **Revoke** keys instantly if compromised
|
|
264
|
+
- Maximum 10 active keys per account
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Full Example
|
|
269
|
+
|
|
270
|
+
```jsx
|
|
271
|
+
// app/product/[id]/page.tsx (Next.js App Router)
|
|
272
|
+
|
|
273
|
+
import { PrimeStyleTryon } from '@primestyleai/tryon/react';
|
|
274
|
+
|
|
275
|
+
export default function ProductPage({ product }) {
|
|
276
|
+
return (
|
|
277
|
+
<div className="product-page">
|
|
278
|
+
<div className="product-gallery">
|
|
279
|
+
<img src={product.image} alt={product.name} />
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div className="product-info">
|
|
283
|
+
<h1>{product.name}</h1>
|
|
284
|
+
<p className="price">${product.price}</p>
|
|
285
|
+
|
|
286
|
+
{/* Virtual Try-On — reads API key from env */}
|
|
287
|
+
<PrimeStyleTryon
|
|
288
|
+
productImage={product.image}
|
|
289
|
+
buttonText="Try It On"
|
|
290
|
+
buttonStyles={{
|
|
291
|
+
width: '100%',
|
|
292
|
+
padding: '14px 24px',
|
|
293
|
+
borderRadius: '10px',
|
|
294
|
+
fontSize: '15px',
|
|
295
|
+
}}
|
|
296
|
+
onComplete={(result) => {
|
|
297
|
+
// Show success toast, track analytics, etc.
|
|
298
|
+
toast.success('Your try-on is ready!');
|
|
299
|
+
}}
|
|
300
|
+
onError={(error) => {
|
|
301
|
+
toast.error(error.message);
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
|
|
305
|
+
<button className="add-to-cart">Add to Cart</button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Requirements
|
|
315
|
+
|
|
316
|
+
- React 18+
|
|
317
|
+
- Next.js 13+ (App Router or Pages Router)
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Need Help?
|
|
322
|
+
|
|
323
|
+
- [Live Demo](https://primestyleai.com/developer/demo) — See it in action
|
|
324
|
+
- [Documentation](https://primestyleai.com/docs) — Full API reference
|
|
325
|
+
- [Dashboard](https://primestyleai.com/dashboard/developer/keys) — Manage keys & tokens
|
|
326
|
+
- [Contact](mailto:support@primestyleai.com) — We're here to help
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
<p align="center">
|
|
331
|
+
Built with care by <a href="https://primestyleai.com">PrimeStyle AI</a>
|
|
332
|
+
</p>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const DEFAULT_API_URL = "https://api.primestyleai.com";
|
|
2
|
+
class ApiClient {
|
|
3
|
+
constructor(apiKey, apiUrl) {
|
|
4
|
+
this.apiKey = apiKey;
|
|
5
|
+
this.baseUrl = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
get headers() {
|
|
8
|
+
return {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async submitTryOn(modelImage, garmentImage) {
|
|
14
|
+
const res = await fetch(`${this.baseUrl}/api/v1/tryon`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: this.headers,
|
|
17
|
+
body: JSON.stringify({ modelImage, garmentImage })
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const data = await res.json().catch(() => ({}));
|
|
21
|
+
if (res.status === 402) {
|
|
22
|
+
throw new PrimeStyleError(
|
|
23
|
+
data.message || "Insufficient tokens",
|
|
24
|
+
"INSUFFICIENT_TOKENS"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
throw new PrimeStyleError(
|
|
28
|
+
data.message || "Failed to submit try-on",
|
|
29
|
+
"API_ERROR"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
34
|
+
async getStatus(jobId) {
|
|
35
|
+
const res = await fetch(`${this.baseUrl}/api/v1/tryon/status/${jobId}`, {
|
|
36
|
+
headers: this.headers
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const data = await res.json().catch(() => ({}));
|
|
40
|
+
throw new PrimeStyleError(
|
|
41
|
+
data.message || "Failed to get status",
|
|
42
|
+
"API_ERROR"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return res.json();
|
|
46
|
+
}
|
|
47
|
+
getStreamUrl() {
|
|
48
|
+
return `${this.baseUrl}/api/v1/tryon/stream?key=${encodeURIComponent(this.apiKey)}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
class PrimeStyleError extends Error {
|
|
52
|
+
constructor(message, code) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "PrimeStyleError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class SseClient {
|
|
59
|
+
constructor(streamUrl) {
|
|
60
|
+
this.eventSource = null;
|
|
61
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
62
|
+
this.reconnectTimer = null;
|
|
63
|
+
this.reconnectAttempts = 0;
|
|
64
|
+
this.maxReconnectAttempts = 5;
|
|
65
|
+
this.streamUrl = streamUrl;
|
|
66
|
+
}
|
|
67
|
+
connect() {
|
|
68
|
+
if (this.eventSource) return;
|
|
69
|
+
this.eventSource = new EventSource(this.streamUrl);
|
|
70
|
+
this.eventSource.addEventListener("vto-update", (event) => {
|
|
71
|
+
try {
|
|
72
|
+
const data = JSON.parse(event.data);
|
|
73
|
+
this.emit(data.galleryId, data);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
this.eventSource.onopen = () => {
|
|
78
|
+
this.reconnectAttempts = 0;
|
|
79
|
+
};
|
|
80
|
+
this.eventSource.onerror = () => {
|
|
81
|
+
this.eventSource?.close();
|
|
82
|
+
this.eventSource = null;
|
|
83
|
+
this.scheduleReconnect();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
scheduleReconnect() {
|
|
87
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
88
|
+
if (this.listeners.size === 0) return;
|
|
89
|
+
const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
|
|
90
|
+
this.reconnectAttempts++;
|
|
91
|
+
this.reconnectTimer = setTimeout(() => {
|
|
92
|
+
this.connect();
|
|
93
|
+
}, delay);
|
|
94
|
+
}
|
|
95
|
+
onJob(jobId, callback) {
|
|
96
|
+
if (!this.listeners.has(jobId)) {
|
|
97
|
+
this.listeners.set(jobId, /* @__PURE__ */ new Set());
|
|
98
|
+
}
|
|
99
|
+
this.listeners.get(jobId).add(callback);
|
|
100
|
+
if (!this.eventSource) {
|
|
101
|
+
this.connect();
|
|
102
|
+
}
|
|
103
|
+
return () => {
|
|
104
|
+
const jobListeners = this.listeners.get(jobId);
|
|
105
|
+
if (jobListeners) {
|
|
106
|
+
jobListeners.delete(callback);
|
|
107
|
+
if (jobListeners.size === 0) {
|
|
108
|
+
this.listeners.delete(jobId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (this.listeners.size === 0) {
|
|
112
|
+
this.disconnect();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
emit(jobId, update) {
|
|
117
|
+
const callbacks = this.listeners.get(jobId);
|
|
118
|
+
if (callbacks) {
|
|
119
|
+
callbacks.forEach((cb) => cb(update));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
disconnect() {
|
|
123
|
+
if (this.reconnectTimer) {
|
|
124
|
+
clearTimeout(this.reconnectTimer);
|
|
125
|
+
this.reconnectTimer = null;
|
|
126
|
+
}
|
|
127
|
+
if (this.eventSource) {
|
|
128
|
+
this.eventSource.close();
|
|
129
|
+
this.eventSource = null;
|
|
130
|
+
}
|
|
131
|
+
this.listeners.clear();
|
|
132
|
+
this.reconnectAttempts = 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const MAX_DIMENSION = 1024;
|
|
136
|
+
const JPEG_QUALITY = 0.85;
|
|
137
|
+
function compressImage(file) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const reader = new FileReader();
|
|
140
|
+
reader.onload = () => {
|
|
141
|
+
const img = new Image();
|
|
142
|
+
img.onload = () => {
|
|
143
|
+
try {
|
|
144
|
+
const canvas = document.createElement("canvas");
|
|
145
|
+
let { width, height } = img;
|
|
146
|
+
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
|
147
|
+
if (width > height) {
|
|
148
|
+
height = Math.round(height * MAX_DIMENSION / width);
|
|
149
|
+
width = MAX_DIMENSION;
|
|
150
|
+
} else {
|
|
151
|
+
width = Math.round(width * MAX_DIMENSION / height);
|
|
152
|
+
height = MAX_DIMENSION;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
canvas.width = width;
|
|
156
|
+
canvas.height = height;
|
|
157
|
+
const ctx = canvas.getContext("2d");
|
|
158
|
+
if (!ctx) {
|
|
159
|
+
reject(new Error("Canvas context not available"));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
163
|
+
const dataUrl = canvas.toDataURL("image/jpeg", JPEG_QUALITY);
|
|
164
|
+
resolve(dataUrl);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
reject(err);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
170
|
+
img.src = reader.result;
|
|
171
|
+
};
|
|
172
|
+
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
173
|
+
reader.readAsDataURL(file);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function isValidImageFile(file) {
|
|
177
|
+
const accepted = ["image/jpeg", "image/png", "image/webp"];
|
|
178
|
+
return accepted.includes(file.type);
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
ApiClient as A,
|
|
182
|
+
PrimeStyleError as P,
|
|
183
|
+
SseClient as S,
|
|
184
|
+
compressImage as c,
|
|
185
|
+
isValidImageFile as i
|
|
186
|
+
};
|
|
@@ -1,137 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
constructor(apiKey, apiUrl) {
|
|
4
|
-
this.apiKey = apiKey;
|
|
5
|
-
this.baseUrl = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
6
|
-
}
|
|
7
|
-
get headers() {
|
|
8
|
-
return {
|
|
9
|
-
"Content-Type": "application/json",
|
|
10
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
async submitTryOn(modelImage, garmentImage) {
|
|
14
|
-
const res = await fetch(`${this.baseUrl}/api/v1/tryon`, {
|
|
15
|
-
method: "POST",
|
|
16
|
-
headers: this.headers,
|
|
17
|
-
body: JSON.stringify({ modelImage, garmentImage })
|
|
18
|
-
});
|
|
19
|
-
if (!res.ok) {
|
|
20
|
-
const data = await res.json().catch(() => ({}));
|
|
21
|
-
if (res.status === 402) {
|
|
22
|
-
throw new PrimeStyleError(
|
|
23
|
-
data.message || "Insufficient tokens",
|
|
24
|
-
"INSUFFICIENT_TOKENS"
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
throw new PrimeStyleError(
|
|
28
|
-
data.message || "Failed to submit try-on",
|
|
29
|
-
"API_ERROR"
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return res.json();
|
|
33
|
-
}
|
|
34
|
-
async getStatus(jobId) {
|
|
35
|
-
const res = await fetch(`${this.baseUrl}/api/v1/tryon/status/${jobId}`, {
|
|
36
|
-
headers: this.headers
|
|
37
|
-
});
|
|
38
|
-
if (!res.ok) {
|
|
39
|
-
const data = await res.json().catch(() => ({}));
|
|
40
|
-
throw new PrimeStyleError(
|
|
41
|
-
data.message || "Failed to get status",
|
|
42
|
-
"API_ERROR"
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return res.json();
|
|
46
|
-
}
|
|
47
|
-
getStreamUrl() {
|
|
48
|
-
return `${this.baseUrl}/api/v1/tryon/stream?key=${encodeURIComponent(this.apiKey)}`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
class PrimeStyleError extends Error {
|
|
52
|
-
constructor(message, code) {
|
|
53
|
-
super(message);
|
|
54
|
-
this.name = "PrimeStyleError";
|
|
55
|
-
this.code = code;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
class SseClient {
|
|
59
|
-
constructor(streamUrl) {
|
|
60
|
-
this.eventSource = null;
|
|
61
|
-
this.listeners = /* @__PURE__ */ new Map();
|
|
62
|
-
this.reconnectTimer = null;
|
|
63
|
-
this.reconnectAttempts = 0;
|
|
64
|
-
this.maxReconnectAttempts = 5;
|
|
65
|
-
this.streamUrl = streamUrl;
|
|
66
|
-
}
|
|
67
|
-
connect() {
|
|
68
|
-
if (this.eventSource) return;
|
|
69
|
-
this.eventSource = new EventSource(this.streamUrl);
|
|
70
|
-
this.eventSource.addEventListener("vto-update", (event) => {
|
|
71
|
-
try {
|
|
72
|
-
const data = JSON.parse(event.data);
|
|
73
|
-
this.emit(data.galleryId, data);
|
|
74
|
-
} catch {
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
this.eventSource.onopen = () => {
|
|
78
|
-
this.reconnectAttempts = 0;
|
|
79
|
-
};
|
|
80
|
-
this.eventSource.onerror = () => {
|
|
81
|
-
this.eventSource?.close();
|
|
82
|
-
this.eventSource = null;
|
|
83
|
-
this.scheduleReconnect();
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
scheduleReconnect() {
|
|
87
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
88
|
-
if (this.listeners.size === 0) return;
|
|
89
|
-
const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
|
|
90
|
-
this.reconnectAttempts++;
|
|
91
|
-
this.reconnectTimer = setTimeout(() => {
|
|
92
|
-
this.connect();
|
|
93
|
-
}, delay);
|
|
94
|
-
}
|
|
95
|
-
onJob(jobId, callback) {
|
|
96
|
-
if (!this.listeners.has(jobId)) {
|
|
97
|
-
this.listeners.set(jobId, /* @__PURE__ */ new Set());
|
|
98
|
-
}
|
|
99
|
-
this.listeners.get(jobId).add(callback);
|
|
100
|
-
if (!this.eventSource) {
|
|
101
|
-
this.connect();
|
|
102
|
-
}
|
|
103
|
-
return () => {
|
|
104
|
-
const jobListeners = this.listeners.get(jobId);
|
|
105
|
-
if (jobListeners) {
|
|
106
|
-
jobListeners.delete(callback);
|
|
107
|
-
if (jobListeners.size === 0) {
|
|
108
|
-
this.listeners.delete(jobId);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (this.listeners.size === 0) {
|
|
112
|
-
this.disconnect();
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
emit(jobId, update) {
|
|
117
|
-
const callbacks = this.listeners.get(jobId);
|
|
118
|
-
if (callbacks) {
|
|
119
|
-
callbacks.forEach((cb) => cb(update));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
disconnect() {
|
|
123
|
-
if (this.reconnectTimer) {
|
|
124
|
-
clearTimeout(this.reconnectTimer);
|
|
125
|
-
this.reconnectTimer = null;
|
|
126
|
-
}
|
|
127
|
-
if (this.eventSource) {
|
|
128
|
-
this.eventSource.close();
|
|
129
|
-
this.eventSource = null;
|
|
130
|
-
}
|
|
131
|
-
this.listeners.clear();
|
|
132
|
-
this.reconnectAttempts = 0;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
1
|
+
import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage } from "./image-utils-usff6Qu8.js";
|
|
2
|
+
import { P } from "./image-utils-usff6Qu8.js";
|
|
135
3
|
function detectProductImage() {
|
|
136
4
|
const ogImage = document.querySelector(
|
|
137
5
|
'meta[property="og:image"]'
|
|
@@ -193,51 +61,6 @@ function extractSchemaImage(data) {
|
|
|
193
61
|
}
|
|
194
62
|
return null;
|
|
195
63
|
}
|
|
196
|
-
const MAX_DIMENSION = 1024;
|
|
197
|
-
const JPEG_QUALITY = 0.85;
|
|
198
|
-
function compressImage(file) {
|
|
199
|
-
return new Promise((resolve, reject) => {
|
|
200
|
-
const reader = new FileReader();
|
|
201
|
-
reader.onload = () => {
|
|
202
|
-
const img = new Image();
|
|
203
|
-
img.onload = () => {
|
|
204
|
-
try {
|
|
205
|
-
const canvas = document.createElement("canvas");
|
|
206
|
-
let { width, height } = img;
|
|
207
|
-
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
|
208
|
-
if (width > height) {
|
|
209
|
-
height = Math.round(height * MAX_DIMENSION / width);
|
|
210
|
-
width = MAX_DIMENSION;
|
|
211
|
-
} else {
|
|
212
|
-
width = Math.round(width * MAX_DIMENSION / height);
|
|
213
|
-
height = MAX_DIMENSION;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
canvas.width = width;
|
|
217
|
-
canvas.height = height;
|
|
218
|
-
const ctx = canvas.getContext("2d");
|
|
219
|
-
if (!ctx) {
|
|
220
|
-
reject(new Error("Canvas context not available"));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
224
|
-
const dataUrl = canvas.toDataURL("image/jpeg", JPEG_QUALITY);
|
|
225
|
-
resolve(dataUrl);
|
|
226
|
-
} catch (err) {
|
|
227
|
-
reject(err);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
img.onerror = () => reject(new Error("Failed to load image"));
|
|
231
|
-
img.src = reader.result;
|
|
232
|
-
};
|
|
233
|
-
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
234
|
-
reader.readAsDataURL(file);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
function isValidImageFile(file) {
|
|
238
|
-
const accepted = ["image/jpeg", "image/png", "image/webp"];
|
|
239
|
-
return accepted.includes(file.type);
|
|
240
|
-
}
|
|
241
64
|
function getStyles() {
|
|
242
65
|
return `
|
|
243
66
|
:host {
|
|
@@ -1160,7 +983,7 @@ if (typeof window !== "undefined" && !customElements.get("primestyle-tryon")) {
|
|
|
1160
983
|
}
|
|
1161
984
|
export {
|
|
1162
985
|
ApiClient,
|
|
1163
|
-
PrimeStyleError,
|
|
986
|
+
P as PrimeStyleError,
|
|
1164
987
|
PrimeStyleTryon,
|
|
1165
988
|
SseClient,
|
|
1166
989
|
compressImage,
|