@primestyleai/tryon 1.0.1 → 1.2.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 +235 -307
- 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 +39 -0
- package/dist/react/index.js +771 -0
- package/dist/types.d.ts +55 -0
- package/package.json +25 -8
- package/dist/primestyle-tryon.umd.js +0 -1
|
@@ -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,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
import type { ButtonStyles, ModalStyles, PrimeStyleClassNames } from "../types";
|
|
3
|
+
export interface PrimeStyleTryonProps {
|
|
4
|
+
/** Product image URL to try on */
|
|
5
|
+
productImage: string;
|
|
6
|
+
/** Button text (defaults to "Virtual Try-On") */
|
|
7
|
+
buttonText?: string;
|
|
8
|
+
/** API base URL — defaults to NEXT_PUBLIC_PRIMESTYLE_API_URL env or https://api.primestyleai.com */
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
/** Show "Powered by PrimeStyle" in modal footer (defaults to true) */
|
|
11
|
+
showPoweredBy?: boolean;
|
|
12
|
+
/** Button appearance customization via CSS variables */
|
|
13
|
+
buttonStyles?: ButtonStyles;
|
|
14
|
+
/** Modal appearance customization via CSS variables */
|
|
15
|
+
modalStyles?: ModalStyles;
|
|
16
|
+
/**
|
|
17
|
+
* Override any element's className with Tailwind classes or custom CSS classes.
|
|
18
|
+
* These are appended to the default `ps-tryon-*` classes, so you can either
|
|
19
|
+
* add onto the defaults or fully replace them with your own styles.
|
|
20
|
+
*/
|
|
21
|
+
classNames?: PrimeStyleClassNames;
|
|
22
|
+
/** Additional className on the root wrapper */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** Additional inline styles on the root wrapper */
|
|
25
|
+
style?: CSSProperties;
|
|
26
|
+
onOpen?: () => void;
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
onUpload?: (file: File) => void;
|
|
29
|
+
onProcessing?: (jobId: string) => void;
|
|
30
|
+
onComplete?: (result: {
|
|
31
|
+
jobId: string;
|
|
32
|
+
imageUrl: string;
|
|
33
|
+
}) => void;
|
|
34
|
+
onError?: (error: {
|
|
35
|
+
message: string;
|
|
36
|
+
code?: string;
|
|
37
|
+
}) => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function PrimeStyleTryon({ productImage, buttonText, apiUrl, showPoweredBy, buttonStyles: btnS, modalStyles: mdlS, classNames: cn, className, style, onOpen, onClose, onUpload, onProcessing, onComplete, onError, }: PrimeStyleTryonProps): import("react/jsx-runtime").JSX.Element;
|