@primestyleai/tryon 5.8.38 → 5.8.39

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Face landmark detection using MediaPipe FaceLandmarker.
3
+ *
4
+ * Runs entirely client-side. Uses iris diameter (~11.7 mm — biologically
5
+ * stable across adults) as the pixel→mm scale anchor, then derives the
6
+ * measurements that eyewear + headwear size guides ask for.
7
+ *
8
+ * Shares the same `@mediapipe/tasks-vision` bundle loaded by
9
+ * `pose-detect.ts`, so the only extra cost is the ~3 MB `.task` file.
10
+ */
11
+ export interface Pt {
12
+ x: number;
13
+ y: number;
14
+ z?: number;
15
+ }
16
+ export interface FaceLandmarks {
17
+ noseTip: Pt;
18
+ noseBridge: Pt;
19
+ leftInnerEye: Pt;
20
+ rightInnerEye: Pt;
21
+ leftOuterEye: Pt;
22
+ rightOuterEye: Pt;
23
+ leftIrisCenter: Pt;
24
+ rightIrisCenter: Pt;
25
+ leftIrisRing: Pt[];
26
+ rightIrisRing: Pt[];
27
+ leftTragus: Pt;
28
+ rightTragus: Pt;
29
+ forehead: Pt;
30
+ chin: Pt;
31
+ leftMouth: Pt;
32
+ rightMouth: Pt;
33
+ }
34
+ export interface FaceMeasurementsMm {
35
+ irisDiameter: number;
36
+ pd: number;
37
+ bridgeWidth: number;
38
+ faceWidth: number;
39
+ templeLengthLeft: number;
40
+ templeLengthRight: number;
41
+ templeLength: number;
42
+ headWidth: number;
43
+ headDepth: number;
44
+ headCircumference: number;
45
+ }
46
+ export interface FaceDetectResult {
47
+ landmarks: FaceLandmarks;
48
+ measurementsMm: FaceMeasurementsMm;
49
+ irisConfidence: number;
50
+ imageWidth: number;
51
+ imageHeight: number;
52
+ }
53
+ /** Detect face landmarks and compute eyewear/headwear measurements in mm. */
54
+ export declare function detectFaceMeasurements(imageSrc: string | HTMLImageElement): Promise<FaceDetectResult | null>;
@@ -17,11 +17,11 @@ const LEFT_ANKLE = 27;
17
17
  const RIGHT_ANKLE = 28;
18
18
  const NOSE = 0;
19
19
  let poseLandmarker = null;
20
- let loadingPromise = null;
20
+ let loadingPromise$1 = null;
21
21
  async function loadMediaPipe() {
22
22
  if (poseLandmarker) return;
23
- if (loadingPromise) return loadingPromise;
24
- loadingPromise = (async () => {
23
+ if (loadingPromise$1) return loadingPromise$1;
24
+ loadingPromise$1 = (async () => {
25
25
  const vision = await import(
26
26
  /* webpackIgnore: true */
27
27
  // @ts-ignore dynamic CDN import
@@ -40,12 +40,12 @@ async function loadMediaPipe() {
40
40
  numPoses: 1
41
41
  });
42
42
  })();
43
- return loadingPromise;
43
+ return loadingPromise$1;
44
44
  }
45
45
  async function detectMeasurementLines(imageSrc) {
46
46
  try {
47
47
  await loadMediaPipe();
48
- const img = await loadImage(imageSrc);
48
+ const img = await loadImage$1(imageSrc);
49
49
  const result = poseLandmarker.detect(img);
50
50
  if (!result?.landmarks?.length || result.landmarks[0].length < 25) {
51
51
  return null;
@@ -80,7 +80,7 @@ async function detectMeasurementLines(imageSrc) {
80
80
  return null;
81
81
  }
82
82
  }
83
- function loadImage(src) {
83
+ function loadImage$1(src) {
84
84
  return new Promise((resolve, reject) => {
85
85
  const img = new Image();
86
86
  img.crossOrigin = "anonymous";
@@ -99,7 +99,7 @@ async function detectBodyLandmarks(imageSrc) {
99
99
  await loadMediaPipe();
100
100
  let img;
101
101
  if (typeof imageSrc === "string") {
102
- img = await loadImage(imageSrc);
102
+ img = await loadImage$1(imageSrc);
103
103
  } else {
104
104
  img = imageSrc;
105
105
  }
@@ -128,6 +128,165 @@ async function detectBodyLandmarks(imageSrc) {
128
128
  return null;
129
129
  }
130
130
  }
131
+ const IDX = {
132
+ noseTip: 1,
133
+ noseBridge: 168,
134
+ leftInnerEye: 133,
135
+ rightInnerEye: 362,
136
+ leftOuterEye: 33,
137
+ rightOuterEye: 263,
138
+ // Iris landmarks (478-point model with iris refinement)
139
+ leftIrisCenter: 468,
140
+ leftIrisRing: [469, 470, 471, 472],
141
+ rightIrisCenter: 473,
142
+ rightIrisRing: [474, 475, 476, 477],
143
+ // Tragus (ear attach point) — best approximations in the face mesh
144
+ leftTragus: 234,
145
+ rightTragus: 454,
146
+ forehead: 10,
147
+ chin: 152,
148
+ leftMouth: 61,
149
+ rightMouth: 291
150
+ };
151
+ const IRIS_DIAMETER_MM = 11.7;
152
+ let faceLandmarker = null;
153
+ let loadingPromise = null;
154
+ async function loadFaceLandmarker() {
155
+ if (faceLandmarker) return;
156
+ if (loadingPromise) return loadingPromise;
157
+ loadingPromise = (async () => {
158
+ const vision = await import(
159
+ /* webpackIgnore: true */
160
+ // @ts-ignore dynamic CDN import
161
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/vision_bundle.mjs"
162
+ );
163
+ const { FilesetResolver, FaceLandmarker } = vision;
164
+ const filesetResolver = await FilesetResolver.forVisionTasks(
165
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/wasm"
166
+ );
167
+ faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
168
+ baseOptions: {
169
+ modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
170
+ delegate: "GPU"
171
+ },
172
+ runningMode: "IMAGE",
173
+ numFaces: 1,
174
+ outputFaceBlendshapes: false,
175
+ outputFacialTransformationMatrixes: false
176
+ });
177
+ })();
178
+ return loadingPromise;
179
+ }
180
+ function loadImage(src) {
181
+ return new Promise((resolve, reject) => {
182
+ const img = new Image();
183
+ img.crossOrigin = "anonymous";
184
+ img.onload = () => resolve(img);
185
+ img.onerror = () => {
186
+ const img2 = new Image();
187
+ img2.onload = () => resolve(img2);
188
+ img2.onerror = () => reject(new Error("Failed to load image"));
189
+ img2.src = src;
190
+ };
191
+ img.src = src;
192
+ });
193
+ }
194
+ function irisDiameterPx(ring, imageWidth, imageHeight) {
195
+ if (ring.length < 4) return 0;
196
+ const toPx = (p) => ({ x: p.x * imageWidth, y: p.y * imageHeight });
197
+ const [p0, p1, p2, p3] = ring.map(toPx);
198
+ const d1 = Math.hypot(p0.x - p2.x, p0.y - p2.y);
199
+ const d2 = Math.hypot(p1.x - p3.x, p1.y - p3.y);
200
+ return (d1 + d2) / 2;
201
+ }
202
+ function extractLandmarks(raw) {
203
+ if (raw.length < 478) return null;
204
+ const at = (i) => ({ x: raw[i].x, y: raw[i].y, z: raw[i].z });
205
+ return {
206
+ noseTip: at(IDX.noseTip),
207
+ noseBridge: at(IDX.noseBridge),
208
+ leftInnerEye: at(IDX.leftInnerEye),
209
+ rightInnerEye: at(IDX.rightInnerEye),
210
+ leftOuterEye: at(IDX.leftOuterEye),
211
+ rightOuterEye: at(IDX.rightOuterEye),
212
+ leftIrisCenter: at(IDX.leftIrisCenter),
213
+ rightIrisCenter: at(IDX.rightIrisCenter),
214
+ leftIrisRing: IDX.leftIrisRing.map(at),
215
+ rightIrisRing: IDX.rightIrisRing.map(at),
216
+ leftTragus: at(IDX.leftTragus),
217
+ rightTragus: at(IDX.rightTragus),
218
+ forehead: at(IDX.forehead),
219
+ chin: at(IDX.chin),
220
+ leftMouth: at(IDX.leftMouth),
221
+ rightMouth: at(IDX.rightMouth)
222
+ };
223
+ }
224
+ function computeMeasurements(lm, imageWidth, imageHeight) {
225
+ const leftPx = irisDiameterPx(lm.leftIrisRing, imageWidth, imageHeight);
226
+ const rightPx = irisDiameterPx(lm.rightIrisRing, imageWidth, imageHeight);
227
+ const irisPx = (leftPx + rightPx) / 2;
228
+ let irisConfidence = 1;
229
+ if (irisPx < 8) irisConfidence = 0.3;
230
+ else if (Math.abs(leftPx - rightPx) / irisPx > 0.3) irisConfidence = 0.5;
231
+ else if (Math.abs(leftPx - rightPx) / irisPx > 0.15) irisConfidence = 0.8;
232
+ const pxToMm = irisPx > 0 ? IRIS_DIAMETER_MM / irisPx : 0;
233
+ const mmBetween = (a2, b2) => {
234
+ const pxDist = Math.hypot(
235
+ (a2.x - b2.x) * imageWidth,
236
+ (a2.y - b2.y) * imageHeight
237
+ );
238
+ return pxDist * pxToMm;
239
+ };
240
+ const pd = mmBetween(lm.leftIrisCenter, lm.rightIrisCenter);
241
+ const bridgeWidth = mmBetween(lm.leftInnerEye, lm.rightInnerEye);
242
+ const faceWidth = mmBetween(lm.leftTragus, lm.rightTragus);
243
+ const templeLengthLeft = mmBetween(lm.leftTragus, lm.leftOuterEye);
244
+ const templeLengthRight = mmBetween(lm.rightTragus, lm.rightOuterEye);
245
+ const templeLength = (templeLengthLeft + templeLengthRight) / 2;
246
+ const headWidth = faceWidth * 1.07;
247
+ const zDepthNorm = Math.abs((lm.forehead.z ?? 0) - (lm.chin.z ?? 0));
248
+ const rawHeadDepthMm = zDepthNorm * imageWidth * pxToMm;
249
+ const headDepth = Math.max(170, Math.min(210, rawHeadDepthMm || 190));
250
+ const a = headWidth / 2;
251
+ const b = headDepth / 2;
252
+ const headCircumference = Math.PI * Math.sqrt(2 * (a * a + b * b));
253
+ return {
254
+ measurements: {
255
+ irisDiameter: IRIS_DIAMETER_MM,
256
+ pd: round1(pd),
257
+ bridgeWidth: round1(bridgeWidth),
258
+ faceWidth: round1(faceWidth),
259
+ templeLengthLeft: round1(templeLengthLeft),
260
+ templeLengthRight: round1(templeLengthRight),
261
+ templeLength: round1(templeLength),
262
+ headWidth: round1(headWidth),
263
+ headDepth: round1(headDepth),
264
+ headCircumference: round1(headCircumference)
265
+ },
266
+ irisConfidence
267
+ };
268
+ }
269
+ function round1(n) {
270
+ return Math.round(n * 10) / 10;
271
+ }
272
+ async function detectFaceMeasurements(imageSrc) {
273
+ try {
274
+ await loadFaceLandmarker();
275
+ const img = typeof imageSrc === "string" ? await loadImage(imageSrc) : imageSrc;
276
+ const result = faceLandmarker.detect(img);
277
+ if (!result?.faceLandmarks?.length) return null;
278
+ const raw = result.faceLandmarks[0];
279
+ const landmarks = extractLandmarks(raw);
280
+ if (!landmarks) return null;
281
+ const imageWidth = img.naturalWidth || img.width;
282
+ const imageHeight = img.naturalHeight || img.height;
283
+ const { measurements, irisConfidence } = computeMeasurements(landmarks, imageWidth, imageHeight);
284
+ return { landmarks, measurementsMm: measurements, irisConfidence, imageWidth, imageHeight };
285
+ } catch (err) {
286
+ console.error("[PS-SDK] Face detection failed:", err);
287
+ return null;
288
+ }
289
+ }
131
290
  function parseRange(s) {
132
291
  const ns = s.replace(/[^\d.\-–]/g, " ").trim().split(/[\s\-–]+/).filter(Boolean).map(Number).filter((n) => !isNaN(n));
133
292
  return ns.length ? { min: Math.min(...ns), max: Math.max(...ns) } : { min: 0, max: 0 };
@@ -12118,7 +12277,6 @@ function HeadSizeView(props) {
12118
12277
  title: "Headwear Measurements",
12119
12278
  fields,
12120
12279
  photoVariant: "close-up",
12121
- disablePhotoUpload: true,
12122
12280
  ...rest
12123
12281
  }
12124
12282
  );
@@ -12178,7 +12336,6 @@ function FaceSizeView(props) {
12178
12336
  fields,
12179
12337
  unitOptions: EYEWEAR_UNIT_OPTIONS,
12180
12338
  photoVariant: "close-up",
12181
- disablePhotoUpload: true,
12182
12339
  ...rest
12183
12340
  }
12184
12341
  );
@@ -12866,6 +13023,53 @@ function PrimeStyleTryonInner({
12866
13023
  setSizingLoading(true);
12867
13024
  setEstimationDone(false);
12868
13025
  setView("size-result");
13026
+ const measurementType = detectMeasurementType(productTitle);
13027
+ if (measurementType === "face" || measurementType === "head") {
13028
+ try {
13029
+ const faceResult = await detectFaceMeasurements(objUrl);
13030
+ const facePayload = {
13031
+ product: { title: productTitle },
13032
+ sizeGuide: sizeGuide ?? { found: false },
13033
+ sizingUnit: measurementType === "head" ? "cm" : "mm",
13034
+ category: measurementType,
13035
+ bodyImage: data.photoBase64
13036
+ };
13037
+ if (faceResult) {
13038
+ facePayload.faceMeasurementsMm = faceResult.measurementsMm;
13039
+ facePayload.faceLandmarks = faceResult.landmarks;
13040
+ facePayload.irisConfidence = faceResult.irisConfidence;
13041
+ }
13042
+ const recRes = await fetch(`${baseUrl}/api/v1/sizing/face-recommend`, {
13043
+ method: "POST",
13044
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
13045
+ body: JSON.stringify(facePayload)
13046
+ });
13047
+ if (recRes.ok) {
13048
+ const recData = await recRes.json();
13049
+ setSizingResult(recData);
13050
+ onComplete?.(recData);
13051
+ persistResultToProfile(
13052
+ {
13053
+ gender: data.gender,
13054
+ height: data.height,
13055
+ weight: data.weight,
13056
+ heightUnit: data.heightUnit,
13057
+ weightUnit: data.weightUnit,
13058
+ age: data.age,
13059
+ bodyImage: data.photoBase64
13060
+ },
13061
+ recData
13062
+ );
13063
+ } else {
13064
+ setEstimationDone(true);
13065
+ }
13066
+ } catch (err) {
13067
+ console.error("[ps-sdk] face-recommend failed:", err);
13068
+ setEstimationDone(true);
13069
+ }
13070
+ setSizingLoading(false);
13071
+ return;
13072
+ }
12869
13073
  modelPoseRef.current = null;
12870
13074
  setBodyLandmarks(null);
12871
13075
  detectMeasurementLines(objUrl).then((lines) => {
@@ -9479,11 +9479,11 @@ const LEFT_ANKLE = 27;
9479
9479
  const RIGHT_ANKLE = 28;
9480
9480
  const NOSE = 0;
9481
9481
  let poseLandmarker = null;
9482
- let loadingPromise = null;
9482
+ let loadingPromise$1 = null;
9483
9483
  async function loadMediaPipe() {
9484
9484
  if (poseLandmarker) return;
9485
- if (loadingPromise) return loadingPromise;
9486
- loadingPromise = (async () => {
9485
+ if (loadingPromise$1) return loadingPromise$1;
9486
+ loadingPromise$1 = (async () => {
9487
9487
  const vision = await import(
9488
9488
  /* webpackIgnore: true */
9489
9489
  // @ts-ignore dynamic CDN import
@@ -9502,12 +9502,12 @@ async function loadMediaPipe() {
9502
9502
  numPoses: 1
9503
9503
  });
9504
9504
  })();
9505
- return loadingPromise;
9505
+ return loadingPromise$1;
9506
9506
  }
9507
9507
  async function detectMeasurementLines(imageSrc) {
9508
9508
  try {
9509
9509
  await loadMediaPipe();
9510
- const img = await loadImage(imageSrc);
9510
+ const img = await loadImage$1(imageSrc);
9511
9511
  const result = poseLandmarker.detect(img);
9512
9512
  if (!result?.landmarks?.length || result.landmarks[0].length < 25) {
9513
9513
  return null;
@@ -9542,7 +9542,7 @@ async function detectMeasurementLines(imageSrc) {
9542
9542
  return null;
9543
9543
  }
9544
9544
  }
9545
- function loadImage(src) {
9545
+ function loadImage$1(src) {
9546
9546
  return new Promise((resolve, reject) => {
9547
9547
  const img = new Image();
9548
9548
  img.crossOrigin = "anonymous";
@@ -9561,7 +9561,7 @@ async function detectBodyLandmarks(imageSrc) {
9561
9561
  await loadMediaPipe();
9562
9562
  let img;
9563
9563
  if (typeof imageSrc === "string") {
9564
- img = await loadImage(imageSrc);
9564
+ img = await loadImage$1(imageSrc);
9565
9565
  } else {
9566
9566
  img = imageSrc;
9567
9567
  }
@@ -9590,6 +9590,165 @@ async function detectBodyLandmarks(imageSrc) {
9590
9590
  return null;
9591
9591
  }
9592
9592
  }
9593
+ const IDX = {
9594
+ noseTip: 1,
9595
+ noseBridge: 168,
9596
+ leftInnerEye: 133,
9597
+ rightInnerEye: 362,
9598
+ leftOuterEye: 33,
9599
+ rightOuterEye: 263,
9600
+ // Iris landmarks (478-point model with iris refinement)
9601
+ leftIrisCenter: 468,
9602
+ leftIrisRing: [469, 470, 471, 472],
9603
+ rightIrisCenter: 473,
9604
+ rightIrisRing: [474, 475, 476, 477],
9605
+ // Tragus (ear attach point) — best approximations in the face mesh
9606
+ leftTragus: 234,
9607
+ rightTragus: 454,
9608
+ forehead: 10,
9609
+ chin: 152,
9610
+ leftMouth: 61,
9611
+ rightMouth: 291
9612
+ };
9613
+ const IRIS_DIAMETER_MM = 11.7;
9614
+ let faceLandmarker = null;
9615
+ let loadingPromise = null;
9616
+ async function loadFaceLandmarker() {
9617
+ if (faceLandmarker) return;
9618
+ if (loadingPromise) return loadingPromise;
9619
+ loadingPromise = (async () => {
9620
+ const vision = await import(
9621
+ /* webpackIgnore: true */
9622
+ // @ts-ignore dynamic CDN import
9623
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/vision_bundle.mjs"
9624
+ );
9625
+ const { FilesetResolver, FaceLandmarker } = vision;
9626
+ const filesetResolver = await FilesetResolver.forVisionTasks(
9627
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/wasm"
9628
+ );
9629
+ faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
9630
+ baseOptions: {
9631
+ modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
9632
+ delegate: "GPU"
9633
+ },
9634
+ runningMode: "IMAGE",
9635
+ numFaces: 1,
9636
+ outputFaceBlendshapes: false,
9637
+ outputFacialTransformationMatrixes: false
9638
+ });
9639
+ })();
9640
+ return loadingPromise;
9641
+ }
9642
+ function loadImage(src) {
9643
+ return new Promise((resolve, reject) => {
9644
+ const img = new Image();
9645
+ img.crossOrigin = "anonymous";
9646
+ img.onload = () => resolve(img);
9647
+ img.onerror = () => {
9648
+ const img2 = new Image();
9649
+ img2.onload = () => resolve(img2);
9650
+ img2.onerror = () => reject(new Error("Failed to load image"));
9651
+ img2.src = src;
9652
+ };
9653
+ img.src = src;
9654
+ });
9655
+ }
9656
+ function irisDiameterPx(ring, imageWidth, imageHeight) {
9657
+ if (ring.length < 4) return 0;
9658
+ const toPx = (p4) => ({ x: p4.x * imageWidth, y: p4.y * imageHeight });
9659
+ const [p0, p1, p2, p3] = ring.map(toPx);
9660
+ const d1 = Math.hypot(p0.x - p2.x, p0.y - p2.y);
9661
+ const d2 = Math.hypot(p1.x - p3.x, p1.y - p3.y);
9662
+ return (d1 + d2) / 2;
9663
+ }
9664
+ function extractLandmarks(raw) {
9665
+ if (raw.length < 478) return null;
9666
+ const at = (i) => ({ x: raw[i].x, y: raw[i].y, z: raw[i].z });
9667
+ return {
9668
+ noseTip: at(IDX.noseTip),
9669
+ noseBridge: at(IDX.noseBridge),
9670
+ leftInnerEye: at(IDX.leftInnerEye),
9671
+ rightInnerEye: at(IDX.rightInnerEye),
9672
+ leftOuterEye: at(IDX.leftOuterEye),
9673
+ rightOuterEye: at(IDX.rightOuterEye),
9674
+ leftIrisCenter: at(IDX.leftIrisCenter),
9675
+ rightIrisCenter: at(IDX.rightIrisCenter),
9676
+ leftIrisRing: IDX.leftIrisRing.map(at),
9677
+ rightIrisRing: IDX.rightIrisRing.map(at),
9678
+ leftTragus: at(IDX.leftTragus),
9679
+ rightTragus: at(IDX.rightTragus),
9680
+ forehead: at(IDX.forehead),
9681
+ chin: at(IDX.chin),
9682
+ leftMouth: at(IDX.leftMouth),
9683
+ rightMouth: at(IDX.rightMouth)
9684
+ };
9685
+ }
9686
+ function computeMeasurements(lm, imageWidth, imageHeight) {
9687
+ const leftPx = irisDiameterPx(lm.leftIrisRing, imageWidth, imageHeight);
9688
+ const rightPx = irisDiameterPx(lm.rightIrisRing, imageWidth, imageHeight);
9689
+ const irisPx = (leftPx + rightPx) / 2;
9690
+ let irisConfidence = 1;
9691
+ if (irisPx < 8) irisConfidence = 0.3;
9692
+ else if (Math.abs(leftPx - rightPx) / irisPx > 0.3) irisConfidence = 0.5;
9693
+ else if (Math.abs(leftPx - rightPx) / irisPx > 0.15) irisConfidence = 0.8;
9694
+ const pxToMm = irisPx > 0 ? IRIS_DIAMETER_MM / irisPx : 0;
9695
+ const mmBetween = (a2, b2) => {
9696
+ const pxDist = Math.hypot(
9697
+ (a2.x - b2.x) * imageWidth,
9698
+ (a2.y - b2.y) * imageHeight
9699
+ );
9700
+ return pxDist * pxToMm;
9701
+ };
9702
+ const pd2 = mmBetween(lm.leftIrisCenter, lm.rightIrisCenter);
9703
+ const bridgeWidth = mmBetween(lm.leftInnerEye, lm.rightInnerEye);
9704
+ const faceWidth = mmBetween(lm.leftTragus, lm.rightTragus);
9705
+ const templeLengthLeft = mmBetween(lm.leftTragus, lm.leftOuterEye);
9706
+ const templeLengthRight = mmBetween(lm.rightTragus, lm.rightOuterEye);
9707
+ const templeLength = (templeLengthLeft + templeLengthRight) / 2;
9708
+ const headWidth = faceWidth * 1.07;
9709
+ const zDepthNorm = Math.abs((lm.forehead.z ?? 0) - (lm.chin.z ?? 0));
9710
+ const rawHeadDepthMm = zDepthNorm * imageWidth * pxToMm;
9711
+ const headDepth = Math.max(170, Math.min(210, rawHeadDepthMm || 190));
9712
+ const a = headWidth / 2;
9713
+ const b = headDepth / 2;
9714
+ const headCircumference = Math.PI * Math.sqrt(2 * (a * a + b * b));
9715
+ return {
9716
+ measurements: {
9717
+ irisDiameter: IRIS_DIAMETER_MM,
9718
+ pd: round1(pd2),
9719
+ bridgeWidth: round1(bridgeWidth),
9720
+ faceWidth: round1(faceWidth),
9721
+ templeLengthLeft: round1(templeLengthLeft),
9722
+ templeLengthRight: round1(templeLengthRight),
9723
+ templeLength: round1(templeLength),
9724
+ headWidth: round1(headWidth),
9725
+ headDepth: round1(headDepth),
9726
+ headCircumference: round1(headCircumference)
9727
+ },
9728
+ irisConfidence
9729
+ };
9730
+ }
9731
+ function round1(n2) {
9732
+ return Math.round(n2 * 10) / 10;
9733
+ }
9734
+ async function detectFaceMeasurements(imageSrc) {
9735
+ try {
9736
+ await loadFaceLandmarker();
9737
+ const img = typeof imageSrc === "string" ? await loadImage(imageSrc) : imageSrc;
9738
+ const result = faceLandmarker.detect(img);
9739
+ if (!result?.faceLandmarks?.length) return null;
9740
+ const raw = result.faceLandmarks[0];
9741
+ const landmarks = extractLandmarks(raw);
9742
+ if (!landmarks) return null;
9743
+ const imageWidth = img.naturalWidth || img.width;
9744
+ const imageHeight = img.naturalHeight || img.height;
9745
+ const { measurements, irisConfidence } = computeMeasurements(landmarks, imageWidth, imageHeight);
9746
+ return { landmarks, measurementsMm: measurements, irisConfidence, imageWidth, imageHeight };
9747
+ } catch (err) {
9748
+ console.error("[PS-SDK] Face detection failed:", err);
9749
+ return null;
9750
+ }
9751
+ }
9593
9752
  function parseRange(s) {
9594
9753
  const ns = s.replace(/[^\d.\-–]/g, " ").trim().split(/[\s\-–]+/).filter(Boolean).map(Number).filter((n2) => !isNaN(n2));
9595
9754
  return ns.length ? { min: Math.min(...ns), max: Math.max(...ns) } : { min: 0, max: 0 };
@@ -21542,7 +21701,6 @@ function HeadSizeView(props) {
21542
21701
  title: "Headwear Measurements",
21543
21702
  fields,
21544
21703
  photoVariant: "close-up",
21545
- disablePhotoUpload: true,
21546
21704
  ...rest
21547
21705
  }
21548
21706
  );
@@ -21602,7 +21760,6 @@ function FaceSizeView(props) {
21602
21760
  fields,
21603
21761
  unitOptions: EYEWEAR_UNIT_OPTIONS,
21604
21762
  photoVariant: "close-up",
21605
- disablePhotoUpload: true,
21606
21763
  ...rest
21607
21764
  }
21608
21765
  );
@@ -22290,6 +22447,53 @@ function PrimeStyleTryonInner({
22290
22447
  setSizingLoading(true);
22291
22448
  setEstimationDone(false);
22292
22449
  setView("size-result");
22450
+ const measurementType = detectMeasurementType(productTitle);
22451
+ if (measurementType === "face" || measurementType === "head") {
22452
+ try {
22453
+ const faceResult = await detectFaceMeasurements(objUrl);
22454
+ const facePayload = {
22455
+ product: { title: productTitle },
22456
+ sizeGuide: sizeGuide ?? { found: false },
22457
+ sizingUnit: measurementType === "head" ? "cm" : "mm",
22458
+ category: measurementType,
22459
+ bodyImage: data.photoBase64
22460
+ };
22461
+ if (faceResult) {
22462
+ facePayload.faceMeasurementsMm = faceResult.measurementsMm;
22463
+ facePayload.faceLandmarks = faceResult.landmarks;
22464
+ facePayload.irisConfidence = faceResult.irisConfidence;
22465
+ }
22466
+ const recRes = await fetch(`${baseUrl}/api/v1/sizing/face-recommend`, {
22467
+ method: "POST",
22468
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
22469
+ body: JSON.stringify(facePayload)
22470
+ });
22471
+ if (recRes.ok) {
22472
+ const recData = await recRes.json();
22473
+ setSizingResult(recData);
22474
+ onComplete?.(recData);
22475
+ persistResultToProfile(
22476
+ {
22477
+ gender: data.gender,
22478
+ height: data.height,
22479
+ weight: data.weight,
22480
+ heightUnit: data.heightUnit,
22481
+ weightUnit: data.weightUnit,
22482
+ age: data.age,
22483
+ bodyImage: data.photoBase64
22484
+ },
22485
+ recData
22486
+ );
22487
+ } else {
22488
+ setEstimationDone(true);
22489
+ }
22490
+ } catch (err) {
22491
+ console.error("[ps-sdk] face-recommend failed:", err);
22492
+ setEstimationDone(true);
22493
+ }
22494
+ setSizingLoading(false);
22495
+ return;
22496
+ }
22293
22497
  modelPoseRef.current = null;
22294
22498
  setBodyLandmarks(null);
22295
22499
  detectMeasurementLines(objUrl).then((lines) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "5.8.38",
3
+ "version": "5.8.39",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",