@visutry/tryon-core 0.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.
Files changed (86) hide show
  1. package/README.md +58 -0
  2. package/dist/__fixtures__/faceFixtures.d.ts +13 -0
  3. package/dist/__fixtures__/faceFixtures.d.ts.map +1 -0
  4. package/dist/__fixtures__/faceFixtures.js +206 -0
  5. package/dist/__fixtures__/faceFixtures.js.map +1 -0
  6. package/dist/adapter/visutry-site.d.ts +76 -0
  7. package/dist/adapter/visutry-site.d.ts.map +1 -0
  8. package/dist/adapter/visutry-site.js +172 -0
  9. package/dist/adapter/visutry-site.js.map +1 -0
  10. package/dist/analytics.d.ts +89 -0
  11. package/dist/analytics.d.ts.map +1 -0
  12. package/dist/analytics.js +49 -0
  13. package/dist/analytics.js.map +1 -0
  14. package/dist/coordinate/CoordinateSystem.d.ts +46 -0
  15. package/dist/coordinate/CoordinateSystem.d.ts.map +1 -0
  16. package/dist/coordinate/CoordinateSystem.js +88 -0
  17. package/dist/coordinate/CoordinateSystem.js.map +1 -0
  18. package/dist/face/FaceMetricsCalculator.d.ts +52 -0
  19. package/dist/face/FaceMetricsCalculator.d.ts.map +1 -0
  20. package/dist/face/FaceMetricsCalculator.js +375 -0
  21. package/dist/face/FaceMetricsCalculator.js.map +1 -0
  22. package/dist/face/FaceSemanticMapper.d.ts +54 -0
  23. package/dist/face/FaceSemanticMapper.d.ts.map +1 -0
  24. package/dist/face/FaceSemanticMapper.js +129 -0
  25. package/dist/face/FaceSemanticMapper.js.map +1 -0
  26. package/dist/face/FaceShapeScorer.d.ts +35 -0
  27. package/dist/face/FaceShapeScorer.d.ts.map +1 -0
  28. package/dist/face/FaceShapeScorer.js +265 -0
  29. package/dist/face/FaceShapeScorer.js.map +1 -0
  30. package/dist/face/feature-extractor.d.ts +46 -0
  31. package/dist/face/feature-extractor.d.ts.map +1 -0
  32. package/dist/face/feature-extractor.js +106 -0
  33. package/dist/face/feature-extractor.js.map +1 -0
  34. package/dist/face/logreg-classifier.d.ts +117 -0
  35. package/dist/face/logreg-classifier.d.ts.map +1 -0
  36. package/dist/face/logreg-classifier.js +304 -0
  37. package/dist/face/logreg-classifier.js.map +1 -0
  38. package/dist/feature-flag.d.ts +43 -0
  39. package/dist/feature-flag.d.ts.map +1 -0
  40. package/dist/feature-flag.js +58 -0
  41. package/dist/feature-flag.js.map +1 -0
  42. package/dist/i18n/index.d.ts +15 -0
  43. package/dist/i18n/index.d.ts.map +1 -0
  44. package/dist/i18n/index.js +91 -0
  45. package/dist/i18n/index.js.map +1 -0
  46. package/dist/index.d.ts +24 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +33 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/manifest/ManifestValidator.d.ts +23 -0
  51. package/dist/manifest/ManifestValidator.d.ts.map +1 -0
  52. package/dist/manifest/ManifestValidator.js +129 -0
  53. package/dist/manifest/ManifestValidator.js.map +1 -0
  54. package/dist/pose/GlassesPoseSolver.d.ts +64 -0
  55. package/dist/pose/GlassesPoseSolver.d.ts.map +1 -0
  56. package/dist/pose/GlassesPoseSolver.js +299 -0
  57. package/dist/pose/GlassesPoseSolver.js.map +1 -0
  58. package/dist/privacy/PrivacyGuard.d.ts +44 -0
  59. package/dist/privacy/PrivacyGuard.d.ts.map +1 -0
  60. package/dist/privacy/PrivacyGuard.js +105 -0
  61. package/dist/privacy/PrivacyGuard.js.map +1 -0
  62. package/dist/quality/QualityGate.d.ts +22 -0
  63. package/dist/quality/QualityGate.d.ts.map +1 -0
  64. package/dist/quality/QualityGate.js +161 -0
  65. package/dist/quality/QualityGate.js.map +1 -0
  66. package/dist/smoothing/PoseSmoothing.d.ts +37 -0
  67. package/dist/smoothing/PoseSmoothing.d.ts.map +1 -0
  68. package/dist/smoothing/PoseSmoothing.js +146 -0
  69. package/dist/smoothing/PoseSmoothing.js.map +1 -0
  70. package/dist/types/index.d.ts +444 -0
  71. package/dist/types/index.d.ts.map +1 -0
  72. package/dist/types/index.js +8 -0
  73. package/dist/types/index.js.map +1 -0
  74. package/dist/utils/errors.d.ts +7 -0
  75. package/dist/utils/errors.d.ts.map +1 -0
  76. package/dist/utils/errors.js +20 -0
  77. package/dist/utils/errors.js.map +1 -0
  78. package/dist/utils/index.d.ts +2 -0
  79. package/dist/utils/index.d.ts.map +1 -0
  80. package/dist/utils/index.js +2 -0
  81. package/dist/utils/index.js.map +1 -0
  82. package/dist/utils/math.d.ts +40 -0
  83. package/dist/utils/math.d.ts.map +1 -0
  84. package/dist/utils/math.js +149 -0
  85. package/dist/utils/math.js.map +1 -0
  86. package/package.json +31 -0
@@ -0,0 +1,54 @@
1
+ import type { FaceResultSource, FaceSemanticPoints, Point3D } from "../types/index.js";
2
+ /**
3
+ * Mapping from a semantic point name to the index in the raw landmark array.
4
+ * Adapters supply the index map appropriate for their tracker; the core ships a
5
+ * default MediaPipe Face Landmarker map.
6
+ */
7
+ export type SemanticIndexMap = Partial<Record<keyof FaceSemanticPoints, number>>;
8
+ /**
9
+ * Default MediaPipe Face Landmarker (468/478 point topology) index map, as
10
+ * specified in the SDK spec §10.1.
11
+ */
12
+ export declare const MEDIAPIPE_SEMANTIC_INDEX_MAP: SemanticIndexMap;
13
+ export interface FaceSemanticMapperOptions {
14
+ indexMap?: SemanticIndexMap;
15
+ /** When true, derive eye centers / eyes center from outer+inner corners. Default true. */
16
+ deriveCenters?: boolean;
17
+ }
18
+ /**
19
+ * Maps raw tracker landmarks onto the stable `FaceSemanticPoints` contract.
20
+ *
21
+ * This class is intentionally side-effect free and tracker-agnostic: it only
22
+ * needs an index map describing where each semantic point lives in the raw
23
+ * array. The web adapter passes the MediaPipe map; the WeChat adapter passes a
24
+ * custom map or relies on direct construction.
25
+ */
26
+ export declare class FaceSemanticMapper {
27
+ private readonly indexMap;
28
+ private readonly deriveCenters;
29
+ constructor(options?: FaceSemanticMapperOptions);
30
+ /**
31
+ * Build a `FaceSemanticPoints` from a raw normalized landmark array.
32
+ * Missing indices or undefined landmarks are silently skipped — downstream
33
+ * consumers must tolerate optional points.
34
+ */
35
+ map(landmarks: Point3D[]): FaceSemanticPoints;
36
+ /**
37
+ * Derive leftEyeCenter, rightEyeCenter and eyesCenter from the outer/inner
38
+ * eye corners when they are available. These derived points are the backbone
39
+ * of the glasses pose solver and face metrics.
40
+ */
41
+ private deriveEyeCenters;
42
+ /**
43
+ * Count how many of the *required* semantic points (for analysis) are present.
44
+ * Used by the quality gate to emit `MISSING_KEY_POINTS`.
45
+ */
46
+ static countMissing(semantic: FaceSemanticPoints, required?: (keyof FaceSemanticPoints)[]): {
47
+ missing: string[];
48
+ present: number;
49
+ total: number;
50
+ };
51
+ /** Convenience factory bound to a specific source's default map. */
52
+ static forSource(source: FaceResultSource): FaceSemanticMapper;
53
+ }
54
+ //# sourceMappingURL=FaceSemanticMapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FaceSemanticMapper.d.ts","sourceRoot":"","sources":["../../src/face/FaceSemanticMapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGvF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;AAEjF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,gBAsB1C,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,0FAA0F;IAC1F,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;gBAE5B,OAAO,GAAE,yBAA8B;IAKnD;;;;OAIG;IACH,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,kBAAkB;IAuB7C;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;;OAGG;IACH,MAAM,CAAC,YAAY,CACjB,QAAQ,EAAE,kBAAkB,EAC5B,QAAQ,GAAE,CAAC,MAAM,kBAAkB,CAAC,EASnC,GACA;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAYxD,oEAAoE;IACpE,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,kBAAkB;CAO/D"}
@@ -0,0 +1,129 @@
1
+ import { midpoint } from "../utils/math.js";
2
+ /**
3
+ * Default MediaPipe Face Landmarker (468/478 point topology) index map, as
4
+ * specified in the SDK spec §10.1.
5
+ */
6
+ export const MEDIAPIPE_SEMANTIC_INDEX_MAP = {
7
+ leftEyeOuter: 33,
8
+ leftEyeInner: 133,
9
+ rightEyeInner: 362,
10
+ rightEyeOuter: 263,
11
+ noseBridge: 168,
12
+ noseTip: 1,
13
+ leftBrowCenter: 105,
14
+ rightBrowCenter: 334,
15
+ foreheadCenter: 10,
16
+ chin: 152,
17
+ leftCheek: 123,
18
+ rightCheek: 352,
19
+ leftJaw: 172,
20
+ rightJaw: 397,
21
+ // visutry additions — used for richer face shape classification
22
+ leftFace: 234,
23
+ rightFace: 454,
24
+ leftForehead: 103,
25
+ rightForehead: 332,
26
+ noseLeft: 98,
27
+ noseRight: 327,
28
+ };
29
+ /**
30
+ * Maps raw tracker landmarks onto the stable `FaceSemanticPoints` contract.
31
+ *
32
+ * This class is intentionally side-effect free and tracker-agnostic: it only
33
+ * needs an index map describing where each semantic point lives in the raw
34
+ * array. The web adapter passes the MediaPipe map; the WeChat adapter passes a
35
+ * custom map or relies on direct construction.
36
+ */
37
+ export class FaceSemanticMapper {
38
+ constructor(options = {}) {
39
+ Object.defineProperty(this, "indexMap", {
40
+ enumerable: true,
41
+ configurable: true,
42
+ writable: true,
43
+ value: void 0
44
+ });
45
+ Object.defineProperty(this, "deriveCenters", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: void 0
50
+ });
51
+ this.indexMap = options.indexMap ?? MEDIAPIPE_SEMANTIC_INDEX_MAP;
52
+ this.deriveCenters = options.deriveCenters ?? true;
53
+ }
54
+ /**
55
+ * Build a `FaceSemanticPoints` from a raw normalized landmark array.
56
+ * Missing indices or undefined landmarks are silently skipped — downstream
57
+ * consumers must tolerate optional points.
58
+ */
59
+ map(landmarks) {
60
+ const semantic = {};
61
+ for (const key of Object.keys(this.indexMap)) {
62
+ const idx = this.indexMap[key];
63
+ if (idx === undefined)
64
+ continue;
65
+ const pt = landmarks[idx];
66
+ if (pt && typeof pt.x === "number" && typeof pt.y === "number") {
67
+ semantic[key] = {
68
+ x: pt.x,
69
+ y: pt.y,
70
+ z: pt.z ?? 0,
71
+ };
72
+ }
73
+ }
74
+ if (this.deriveCenters) {
75
+ this.deriveEyeCenters(semantic);
76
+ }
77
+ return semantic;
78
+ }
79
+ /**
80
+ * Derive leftEyeCenter, rightEyeCenter and eyesCenter from the outer/inner
81
+ * eye corners when they are available. These derived points are the backbone
82
+ * of the glasses pose solver and face metrics.
83
+ */
84
+ deriveEyeCenters(semantic) {
85
+ if (!semantic.leftEyeCenter && semantic.leftEyeOuter && semantic.leftEyeInner) {
86
+ semantic.leftEyeCenter = midpoint(semantic.leftEyeOuter, semantic.leftEyeInner);
87
+ }
88
+ if (!semantic.rightEyeCenter && semantic.rightEyeInner && semantic.rightEyeOuter) {
89
+ semantic.rightEyeCenter = midpoint(semantic.rightEyeInner, semantic.rightEyeOuter);
90
+ }
91
+ if (!semantic.eyesCenter && semantic.leftEyeCenter && semantic.rightEyeCenter) {
92
+ semantic.eyesCenter = midpoint(semantic.leftEyeCenter, semantic.rightEyeCenter);
93
+ }
94
+ }
95
+ /**
96
+ * Count how many of the *required* semantic points (for analysis) are present.
97
+ * Used by the quality gate to emit `MISSING_KEY_POINTS`.
98
+ */
99
+ static countMissing(semantic, required = [
100
+ "leftEyeCenter",
101
+ "rightEyeCenter",
102
+ "noseBridge",
103
+ "chin",
104
+ "leftCheek",
105
+ "rightCheek",
106
+ "leftJaw",
107
+ "rightJaw",
108
+ ]) {
109
+ const missing = [];
110
+ for (const key of required) {
111
+ if (!semantic[key])
112
+ missing.push(key);
113
+ }
114
+ return {
115
+ missing,
116
+ present: required.length - missing.length,
117
+ total: required.length,
118
+ };
119
+ }
120
+ /** Convenience factory bound to a specific source's default map. */
121
+ static forSource(source) {
122
+ if (source === "mediapipe") {
123
+ return new FaceSemanticMapper({ indexMap: MEDIAPIPE_SEMANTIC_INDEX_MAP });
124
+ }
125
+ // wechat-vk and custom callers must supply their own map at construction.
126
+ return new FaceSemanticMapper({ indexMap: {} });
127
+ }
128
+ }
129
+ //# sourceMappingURL=FaceSemanticMapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FaceSemanticMapper.js","sourceRoot":"","sources":["../../src/face/FaceSemanticMapper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { FaceMetrics, FaceQualityWarning, FaceShapeResult, NormalizedFaceResult } from "../types/index.js";
2
+ import { FaceMetricsCalculator } from "./FaceMetricsCalculator.js";
3
+ export declare const FACE_SHAPE_SCORER_VERSION = "0.2.0";
4
+ /**
5
+ * Scores face shapes from geometric metrics.
6
+ *
7
+ * v0.2.0: Exact port of visutry's classifyFaceGeometry algorithm.
8
+ * Uses if/else integer scoring on 2D ratios — not bell/softmax.
9
+ * This ensures numerical equivalence with visutry's main site.
10
+ *
11
+ * Future enhancements (bell functions, softmax, chinType, multi-frame)
12
+ * can be layered on top of this known-good baseline.
13
+ */
14
+ export declare class FaceShapeScorer {
15
+ private readonly metricsCalculator;
16
+ constructor(metricsCalculator?: FaceMetricsCalculator);
17
+ /**
18
+ * Score a single face result.
19
+ */
20
+ score(face: NormalizedFaceResult): FaceShapeResult;
21
+ /**
22
+ * Score from pre-aggregated metrics.
23
+ */
24
+ scoreFromMetrics(metrics: FaceMetrics, warnings?: FaceQualityWarning[]): FaceShapeResult;
25
+ /**
26
+ * Multi-frame scoring: aggregate metrics first, then score.
27
+ */
28
+ scoreFrames(frames: NormalizedFaceResult[]): FaceShapeResult;
29
+ /**
30
+ * Get raw integer scores for all 7 shapes — same as visutry's scoring.
31
+ */
32
+ private getAllScores;
33
+ private unknownResult;
34
+ }
35
+ //# sourceMappingURL=FaceShapeScorer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FaceShapeScorer.d.ts","sourceRoot":"","sources":["../../src/face/FaceShapeScorer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAGlB,eAAe,EACf,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAyHjD;;;;;;;;;GASG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAwB;gBAE9C,iBAAiB,CAAC,EAAE,qBAAqB;IAIrD;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,oBAAoB,GAAG,eAAe;IAKlD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,GAAE,kBAAkB,EAAO,GAAG,eAAe;IA6E5F;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,oBAAoB,EAAE,GAAG,eAAe;IAa5D;;OAEG;IACH,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,aAAa;CA0BtB"}
@@ -0,0 +1,265 @@
1
+ import { FaceMetricsCalculator } from "./FaceMetricsCalculator.js";
2
+ export const FACE_SHAPE_SCORER_VERSION = "0.2.0";
3
+ /**
4
+ * All canonical face shapes, in the same order as visutry's CANONICAL_FACE_SHAPES.
5
+ */
6
+ const CANONICAL_SHAPES = [
7
+ "oval",
8
+ "round",
9
+ "square",
10
+ "heart",
11
+ "diamond",
12
+ "oblong",
13
+ "triangle",
14
+ ];
15
+ function clamp(value, min, max) {
16
+ return Math.min(max, Math.max(min, value));
17
+ }
18
+ function round(value, digits = 3) {
19
+ const factor = 10 ** digits;
20
+ return Math.round(value * factor) / factor;
21
+ }
22
+ /**
23
+ * Build geometry signals — exact port of visutry's buildGeometrySignals().
24
+ */
25
+ function buildGeometrySignals(shape, ratios) {
26
+ const lengthSignal = ratios.faceAspectRatio >= 1.42
27
+ ? "Longer vertical face proportion"
28
+ : ratios.faceAspectRatio < 1.2
29
+ ? "Compact face length relative to width"
30
+ : "Balanced face length-to-width ratio";
31
+ const jawSignal = ratios.jawToCheekWidth >= 0.94
32
+ ? "Jaw width is close to cheekbone width"
33
+ : ratios.jawToCheekWidth <= 0.78
34
+ ? "Jawline tapers below the cheekbones"
35
+ : "Jawline has moderate taper";
36
+ const upperSignal = ratios.foreheadToCheekWidth >= 0.9
37
+ ? "Forehead width is close to cheekbone width"
38
+ : "Cheekbones read wider than the upper face";
39
+ const shapeSignal = shape.charAt(0).toUpperCase() + shape.slice(1) + " shape supported by measured proportions";
40
+ return [shapeSignal, lengthSignal, jawSignal, upperSignal];
41
+ }
42
+ /**
43
+ * classifyFaceGeometry — exact port of visutry's classifyFaceGeometry().
44
+ *
45
+ * Uses integer if/else scoring on three key ratios:
46
+ * - faceAspectRatio (H/W, 2D)
47
+ * - jawToCheekWidth
48
+ * - foreheadToCheekWidth
49
+ *
50
+ * Confidence: clamp(0.56 + best.score * 0.065 + margin * 0.035, 0.58, 0.93)
51
+ */
52
+ function classifyFaceGeometry(ratios) {
53
+ const scores = {
54
+ round: 0,
55
+ square: 0,
56
+ oval: 0,
57
+ heart: 0,
58
+ diamond: 0,
59
+ oblong: 0,
60
+ triangle: 0,
61
+ };
62
+ const { faceAspectRatio, jawToCheekWidth, foreheadToCheekWidth } = ratios;
63
+ // --- Exact replication of visutry's scoring rules ---
64
+ if (faceAspectRatio >= 1.42)
65
+ scores.oblong += 4;
66
+ if (faceAspectRatio >= 1.27 && faceAspectRatio < 1.42)
67
+ scores.oval += 3;
68
+ if (faceAspectRatio < 1.2)
69
+ scores.round += 2;
70
+ if (faceAspectRatio < 1.18 && jawToCheekWidth >= 0.86)
71
+ scores.square += 3;
72
+ if (jawToCheekWidth >= 0.92 && foreheadToCheekWidth >= 0.9)
73
+ scores.square += 3;
74
+ if (jawToCheekWidth < 0.76 && foreheadToCheekWidth >= 0.84)
75
+ scores.heart += 4;
76
+ if (jawToCheekWidth < 0.78 && foreheadToCheekWidth < 0.84)
77
+ scores.diamond += 4;
78
+ if (jawToCheekWidth > 0.98 && foreheadToCheekWidth < 0.88)
79
+ scores.triangle += 4;
80
+ if (jawToCheekWidth >= 0.78 && jawToCheekWidth <= 0.9 && faceAspectRatio >= 1.2) {
81
+ scores.oval += 2;
82
+ }
83
+ if (jawToCheekWidth >= 0.82 && jawToCheekWidth <= 0.94 && faceAspectRatio < 1.22) {
84
+ scores.round += 2;
85
+ }
86
+ // --- Rank candidates ---
87
+ const ranked = CANONICAL_SHAPES.map((shape) => ({ shape, score: scores[shape] })).sort((a, b) => b.score - a.score);
88
+ const best = ranked[0];
89
+ const second = ranked[1];
90
+ const confidence = clamp(0.56 + best.score * 0.065 + (best.score - second.score) * 0.035, 0.58, 0.93);
91
+ const alternatives = ranked
92
+ .slice(1, 3)
93
+ .filter((candidate) => candidate.score > 0)
94
+ .map((candidate) => candidate.shape);
95
+ return {
96
+ shape: best.shape,
97
+ alternatives,
98
+ confidence: round(confidence, 2),
99
+ signals: buildGeometrySignals(best.shape, ratios),
100
+ };
101
+ }
102
+ /**
103
+ * Scores face shapes from geometric metrics.
104
+ *
105
+ * v0.2.0: Exact port of visutry's classifyFaceGeometry algorithm.
106
+ * Uses if/else integer scoring on 2D ratios — not bell/softmax.
107
+ * This ensures numerical equivalence with visutry's main site.
108
+ *
109
+ * Future enhancements (bell functions, softmax, chinType, multi-frame)
110
+ * can be layered on top of this known-good baseline.
111
+ */
112
+ export class FaceShapeScorer {
113
+ constructor(metricsCalculator) {
114
+ Object.defineProperty(this, "metricsCalculator", {
115
+ enumerable: true,
116
+ configurable: true,
117
+ writable: true,
118
+ value: void 0
119
+ });
120
+ this.metricsCalculator = metricsCalculator ?? new FaceMetricsCalculator();
121
+ }
122
+ /**
123
+ * Score a single face result.
124
+ */
125
+ score(face) {
126
+ const metrics = this.metricsCalculator.compute(face);
127
+ return this.scoreFromMetrics(metrics, face.quality.warnings);
128
+ }
129
+ /**
130
+ * Score from pre-aggregated metrics.
131
+ */
132
+ scoreFromMetrics(metrics, warnings = []) {
133
+ // Require visutry-compatible ratios for classification.
134
+ if (!metrics.visutry) {
135
+ return this.unknownResult(metrics, [...warnings, "MISSING_KEY_POINTS"]);
136
+ }
137
+ const v = metrics.visutry;
138
+ // --- Quality gates (exact match to visutry's analyzeFaceLandmarks) ---
139
+ const MAX_TILT = 15;
140
+ const MAX_SYMMETRY = 0.14;
141
+ const MIN_SPAN = 0.16;
142
+ const allWarnings = [...warnings];
143
+ // Face span check
144
+ if (metrics.faceSpan !== undefined && metrics.faceSpan < MIN_SPAN) {
145
+ allWarnings.push("FACE_TOO_SMALL");
146
+ }
147
+ // Tilt check — visutry rejects > 15° as unavailable
148
+ if (Math.abs(v.eyeLineTiltDeg) > MAX_TILT) {
149
+ allWarnings.push("EXCESSIVE_TILT");
150
+ }
151
+ // Symmetry check — visutry rejects > 0.14 as unavailable
152
+ if (v.symmetryOffset > MAX_SYMMETRY) {
153
+ allWarnings.push("ASYMMETRIC_FACE");
154
+ }
155
+ // If quality gates failed, return unknown
156
+ if (allWarnings.some((w) => w === "EXCESSIVE_TILT" || w === "ASYMMETRIC_FACE" || w === "FACE_TOO_SMALL")) {
157
+ return this.unknownResult(metrics, allWarnings);
158
+ }
159
+ // --- Classify using visutry's exact algorithm ---
160
+ const result = classifyFaceGeometry(v);
161
+ // --- Build candidates list ---
162
+ // visutry returns shape + alternatives; we also include all shapes with
163
+ // their integer scores as candidates for SDK consumers.
164
+ const scores = this.getAllScores(v);
165
+ const ranked = CANONICAL_SHAPES.map((shape) => ({
166
+ shape,
167
+ score: scores[shape],
168
+ })).sort((a, b) => b.score - a.score);
169
+ const maxScore = Math.max(...ranked.map((r) => r.score), 1);
170
+ const candidates = ranked.map((r) => ({
171
+ shape: r.shape,
172
+ score: round(r.score / maxScore, 3),
173
+ reasons: buildGeometrySignals(r.shape, v),
174
+ }));
175
+ // --- Soft warnings for borderline quality ---
176
+ if (Math.abs(v.eyeLineTiltDeg) > 8) {
177
+ if (!allWarnings.includes("EXCESSIVE_TILT")) {
178
+ allWarnings.push("EXCESSIVE_TILT");
179
+ }
180
+ }
181
+ if (v.symmetryOffset > 0.08) {
182
+ if (!allWarnings.includes("ASYMMETRIC_FACE")) {
183
+ allWarnings.push("ASYMMETRIC_FACE");
184
+ }
185
+ }
186
+ return {
187
+ primary: result.shape,
188
+ candidates,
189
+ confidence: result.confidence,
190
+ metrics,
191
+ warnings: allWarnings,
192
+ version: FACE_SHAPE_SCORER_VERSION,
193
+ };
194
+ }
195
+ /**
196
+ * Multi-frame scoring: aggregate metrics first, then score.
197
+ */
198
+ scoreFrames(frames) {
199
+ if (frames.length === 0) {
200
+ return this.unknownResult();
201
+ }
202
+ const metrics = this.metricsCalculator.aggregate(frames);
203
+ const warnings = frames[0].quality.warnings;
204
+ return this.scoreFromMetrics(metrics, warnings);
205
+ }
206
+ // -----------------------------------------------------------------------
207
+ // Internal helpers
208
+ // -----------------------------------------------------------------------
209
+ /**
210
+ * Get raw integer scores for all 7 shapes — same as visutry's scoring.
211
+ */
212
+ getAllScores(v) {
213
+ const scores = {
214
+ round: 0, square: 0, oval: 0, heart: 0, diamond: 0, oblong: 0, triangle: 0,
215
+ };
216
+ const { faceAspectRatio, jawToCheekWidth, foreheadToCheekWidth } = v;
217
+ if (faceAspectRatio >= 1.42)
218
+ scores.oblong += 4;
219
+ if (faceAspectRatio >= 1.27 && faceAspectRatio < 1.42)
220
+ scores.oval += 3;
221
+ if (faceAspectRatio < 1.2)
222
+ scores.round += 2;
223
+ if (faceAspectRatio < 1.18 && jawToCheekWidth >= 0.86)
224
+ scores.square += 3;
225
+ if (jawToCheekWidth >= 0.92 && foreheadToCheekWidth >= 0.9)
226
+ scores.square += 3;
227
+ if (jawToCheekWidth < 0.76 && foreheadToCheekWidth >= 0.84)
228
+ scores.heart += 4;
229
+ if (jawToCheekWidth < 0.78 && foreheadToCheekWidth < 0.84)
230
+ scores.diamond += 4;
231
+ if (jawToCheekWidth > 0.98 && foreheadToCheekWidth < 0.88)
232
+ scores.triangle += 4;
233
+ if (jawToCheekWidth >= 0.78 && jawToCheekWidth <= 0.9 && faceAspectRatio >= 1.2) {
234
+ scores.oval += 2;
235
+ }
236
+ if (jawToCheekWidth >= 0.82 && jawToCheekWidth <= 0.94 && faceAspectRatio < 1.22) {
237
+ scores.round += 2;
238
+ }
239
+ return scores;
240
+ }
241
+ unknownResult(metrics, warnings = ["LOW_CONFIDENCE"]) {
242
+ return {
243
+ primary: "unknown",
244
+ candidates: [],
245
+ confidence: 0,
246
+ metrics: metrics ?? {
247
+ faceWidth: 0,
248
+ faceHeight: 0,
249
+ cheekboneWidth: 0,
250
+ jawWidth: 0,
251
+ eyeOuterDistance: 0,
252
+ eyeInnerDistance: 0,
253
+ eyeCenterDistance: 0,
254
+ noseBridgeToEyeLine: 0,
255
+ widthHeightRatio: 0,
256
+ jawCheekRatio: 0,
257
+ chinType: "unknown",
258
+ measurementQuality: 0,
259
+ },
260
+ warnings,
261
+ version: FACE_SHAPE_SCORER_VERSION,
262
+ };
263
+ }
264
+ }
265
+ //# sourceMappingURL=FaceShapeScorer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FaceShapeScorer.js","sourceRoot":"","sources":["../../src/face/FaceShapeScorer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Feature extraction for the logistic-regression classifier (spec §9.3).
3
+ *
4
+ * The feature vector is derived from `FaceMetrics` and must use exactly the
5
+ * same canonical order, missing-value strategy and preprocessing in both the
6
+ * Python training script and the TypeScript inference module. Any change to
7
+ * this file MUST bump `LOGREG_FEATURE_SCHEMA_VERSION` and trigger retraining.
8
+ */
9
+ import type { FaceMetrics } from "../types/index.js";
10
+ /**
11
+ * Feature schema version — must match the version baked into every exported
12
+ * model JSON. When the feature set changes (add/remove/reorder), bump this and
13
+ * retrain. See spec §9.3.
14
+ */
15
+ export declare const LOGREG_FEATURE_SCHEMA_VERSION = "2.1.0";
16
+ /**
17
+ * Canonical feature names in fixed order. The index in this array is the
18
+ * column index used in `weights`, `mean` and `std` arrays of the model JSON.
19
+ *
20
+ * Design rationale:
21
+ * - Only scale-invariant, pose-robust ratios and curvatures are used.
22
+ * Raw pixel widths/heights depend on image resolution and camera distance.
23
+ * - `chinType` is ordinal-encoded: pointed (0) < rounded (1) < square (2).
24
+ * `unknown` maps to the midpoint (0.5) so it does not bias the model.
25
+ * - Missing optionals default to 0 (ratio neutral) — see `missingValueStrategy`
26
+ * in the model JSON.
27
+ */
28
+ export declare const LOGREG_FEATURE_NAMES: readonly ["faceWidthToHeight", "jawCheekRatio", "foreheadCheekRatio", "jawToEyeOuter", "eyeOuterToCheek", "noseBridgeRatio", "jawCurvature", "foreheadCurvature", "symmetryOffset", "smileIntensity", "chinTypeEncoded"];
29
+ /** Number of features — convenience constant. */
30
+ export declare const LOGREG_NUM_FEATURES: 11;
31
+ /** Ordinal encoding for `chinType`. */
32
+ export declare function encodeChinType(chinType: FaceMetrics["chinType"]): number;
33
+ /**
34
+ * Extract the canonical feature vector from `FaceMetrics`.
35
+ *
36
+ * The returned array is in the exact order defined by `LOGREG_FEATURE_NAMES`.
37
+ * Missing optional fields default to 0 (ratios/curvatures) or 0.5 (chinType
38
+ * unknown). This matches the `missingValueStrategy: "zero"` declared in the
39
+ * model JSON.
40
+ *
41
+ * **This function is the single source of truth for feature order.** Both the
42
+ * TS inference module and the Python training script must replicate this
43
+ * exact extraction logic.
44
+ */
45
+ export declare function extractFeatures(metrics: FaceMetrics): number[];
46
+ //# sourceMappingURL=feature-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-extractor.d.ts","sourceRoot":"","sources":["../../src/face/feature-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMrD;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,UAAU,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,0NAYvB,CAAC;AAEX,iDAAiD;AACjD,eAAO,MAAM,mBAAmB,IAA8B,CAAC;AAM/D,uCAAuC;AACvC,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,UAAU,CAAC,GAAG,MAAM,CAaxE;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE,CA4B9D"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Feature extraction for the logistic-regression classifier (spec §9.3).
3
+ *
4
+ * The feature vector is derived from `FaceMetrics` and must use exactly the
5
+ * same canonical order, missing-value strategy and preprocessing in both the
6
+ * Python training script and the TypeScript inference module. Any change to
7
+ * this file MUST bump `LOGREG_FEATURE_SCHEMA_VERSION` and trigger retraining.
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Feature schema
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * Feature schema version — must match the version baked into every exported
14
+ * model JSON. When the feature set changes (add/remove/reorder), bump this and
15
+ * retrain. See spec §9.3.
16
+ */
17
+ export const LOGREG_FEATURE_SCHEMA_VERSION = "2.1.0";
18
+ /**
19
+ * Canonical feature names in fixed order. The index in this array is the
20
+ * column index used in `weights`, `mean` and `std` arrays of the model JSON.
21
+ *
22
+ * Design rationale:
23
+ * - Only scale-invariant, pose-robust ratios and curvatures are used.
24
+ * Raw pixel widths/heights depend on image resolution and camera distance.
25
+ * - `chinType` is ordinal-encoded: pointed (0) < rounded (1) < square (2).
26
+ * `unknown` maps to the midpoint (0.5) so it does not bias the model.
27
+ * - Missing optionals default to 0 (ratio neutral) — see `missingValueStrategy`
28
+ * in the model JSON.
29
+ */
30
+ export const LOGREG_FEATURE_NAMES = [
31
+ "faceWidthToHeight",
32
+ "jawCheekRatio",
33
+ "foreheadCheekRatio",
34
+ "jawToEyeOuter",
35
+ "eyeOuterToCheek",
36
+ "noseBridgeRatio",
37
+ "jawCurvature",
38
+ "foreheadCurvature",
39
+ "symmetryOffset",
40
+ "smileIntensity",
41
+ "chinTypeEncoded",
42
+ ];
43
+ /** Number of features — convenience constant. */
44
+ export const LOGREG_NUM_FEATURES = LOGREG_FEATURE_NAMES.length;
45
+ // ---------------------------------------------------------------------------
46
+ // chinType encoding
47
+ // ---------------------------------------------------------------------------
48
+ /** Ordinal encoding for `chinType`. */
49
+ export function encodeChinType(chinType) {
50
+ switch (chinType) {
51
+ case "pointed":
52
+ return 0;
53
+ case "rounded":
54
+ return 1;
55
+ case "square":
56
+ return 2;
57
+ case "unknown":
58
+ return 0.5;
59
+ default:
60
+ return 0.5;
61
+ }
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Feature extraction
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Extract the canonical feature vector from `FaceMetrics`.
68
+ *
69
+ * The returned array is in the exact order defined by `LOGREG_FEATURE_NAMES`.
70
+ * Missing optional fields default to 0 (ratios/curvatures) or 0.5 (chinType
71
+ * unknown). This matches the `missingValueStrategy: "zero"` declared in the
72
+ * model JSON.
73
+ *
74
+ * **This function is the single source of truth for feature order.** Both the
75
+ * TS inference module and the Python training script must replicate this
76
+ * exact extraction logic.
77
+ */
78
+ export function extractFeatures(metrics) {
79
+ const eyeOuter = metrics.eyeOuterDistance || 1e-6;
80
+ const cheek = metrics.cheekboneWidth || 1e-6;
81
+ return [
82
+ // 0: faceWidthToHeight (W/H)
83
+ metrics.faceWidthToHeight,
84
+ // 1: jawCheekRatio (jawWidth / cheekboneWidth)
85
+ metrics.jawCheekRatio,
86
+ // 2: foreheadCheekRatio (foreheadWidth / cheekboneWidth)
87
+ metrics.foreheadCheekRatio ?? 0,
88
+ // 3: jawToEyeOuter (jawWidth / eyeOuterDistance)
89
+ (metrics.jawWidth || 0) / eyeOuter,
90
+ // 4: eyeOuterToCheek (eyeOuterDistance / cheekboneWidth)
91
+ eyeOuter / cheek,
92
+ // 5: noseBridgeRatio (noseBridgeWidth / faceWidth)
93
+ metrics.noseBridgeRatio ?? 0,
94
+ // 6: jawCurvature
95
+ metrics.jawCurvature ?? 0,
96
+ // 7: foreheadCurvature
97
+ metrics.foreheadCurvature ?? 0,
98
+ // 8: symmetryOffset
99
+ metrics.symmetryOffset ?? 0,
100
+ // 9: smileIntensity
101
+ metrics.smileIntensity ?? 0,
102
+ // 10: chinTypeEncoded
103
+ encodeChinType(metrics.chinType),
104
+ ];
105
+ }
106
+ //# sourceMappingURL=feature-extractor.js.map