@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 @@
1
+ {"version":3,"file":"feature-extractor.js","sourceRoot":"","sources":["../../src/face/feature-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,OAAO,CAAC;AAErD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,mBAAmB;IACnB,eAAe;IACf,oBAAoB;IACpB,eAAe;IACf,iBAAiB;IACjB,iBAAiB;IACjB,cAAc;IACd,mBAAmB;IACnB,gBAAgB;IAChB,gBAAgB;IAChB,iBAAiB;CACT,CAAC;AAEX,iDAAiD;AACjD,MAAM,CAAC,MAAM,mBAAmB,GAAG,oBAAoB,CAAC,MAAM,CAAC;AAE/D,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,uCAAuC;AACvC,MAAM,UAAU,cAAc,CAAC,QAAiC;IAC9D,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,SAAS;YACZ,OAAO,CAAC,CAAC;QACX,KAAK,SAAS;YACZ,OAAO,CAAC,CAAC;QACX,KAAK,QAAQ;YACX,OAAO,CAAC,CAAC;QACX,KAAK,SAAS;YACZ,OAAO,GAAG,CAAC;QACb;YACE,OAAO,GAAG,CAAC;IACf,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,OAAoB;IAClD,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,IAAI,IAAI,CAAC;IAClD,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,CAAC;IAE7C,OAAO;QACL,6BAA6B;QAC7B,OAAO,CAAC,iBAAiB;QACzB,+CAA+C;QAC/C,OAAO,CAAC,aAAa;QACrB,yDAAyD;QACzD,OAAO,CAAC,kBAAkB,IAAI,CAAC;QAC/B,iDAAiD;QACjD,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,QAAQ;QAClC,yDAAyD;QACzD,QAAQ,GAAG,KAAK;QAChB,mDAAmD;QACnD,OAAO,CAAC,eAAe,IAAI,CAAC;QAC5B,kBAAkB;QAClB,OAAO,CAAC,YAAY,IAAI,CAAC;QACzB,uBAAuB;QACvB,OAAO,CAAC,iBAAiB,IAAI,CAAC;QAC9B,oBAAoB;QACpB,OAAO,CAAC,cAAc,IAAI,CAAC;QAC3B,oBAAoB;QACpB,OAAO,CAAC,cAAc,IAAI,CAAC;QAC3B,sBAAsB;QACtB,cAAc,CAAC,OAAO,CAAC,QAAQ,CAAC;KACjC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Logistic-regression classifier — pure TypeScript inference (spec §9.3).
3
+ *
4
+ * The model is trained offline in Python (scikit-learn) and exported as a
5
+ * compact JSON file (< 5 KB target). The browser loads this JSON and runs
6
+ * inference without any ML runtime (no ONNX, no TensorFlow.js).
7
+ *
8
+ * The forward pass is: standardize → linear logits → softmax → calibration.
9
+ *
10
+ * **Calibration**: softmax outputs are NOT inherently calibrated probabilities.
11
+ * The model JSON declares which calibration was applied on an independent
12
+ * calibration set (temperature / Platt / isotonic / none). Until ECE/Brier
13
+ * validation passes (spec §9.4), `calibrated` stays `false` and the output
14
+ * is treated as relative `matchStrength`, not probability.
15
+ */
16
+ import type { FaceMetrics, FaceShape, FaceShapeCandidate } from "../types/index.js";
17
+ /** Shape class labels in canonical order — matches `weights` rows. */
18
+ export declare const LOGREG_CLASS_NAMES: FaceShape[];
19
+ /** Number of classes — convenience constant. */
20
+ export declare const LOGREG_NUM_CLASSES: number;
21
+ /** Calibration strategy applied on an independent calibration set. */
22
+ export type CalibrationType = "none" | "temperature" | "platt" | "isotonic";
23
+ /** Isotonic regression calibration map (ascending input → calibrated output). */
24
+ export interface IsotonicMap {
25
+ /** Sorted ascending input thresholds. */
26
+ inputs: number[];
27
+ /** Calibrated outputs aligned with `inputs`. */
28
+ outputs: number[];
29
+ }
30
+ export interface CalibrationParams {
31
+ type: CalibrationType;
32
+ /** Temperature scaling divisor for logits (temperature scaling only). */
33
+ temperature?: number;
34
+ /** Platt scaling: `a * logit + b` then sigmoid (Platt only). */
35
+ plattA?: number;
36
+ plattB?: number;
37
+ /** Per-class isotonic maps (isotonic only). Length = numClasses. */
38
+ isotonic?: IsotonicMap[];
39
+ }
40
+ export interface TrainingMeta {
41
+ trainSamples: number;
42
+ valSamples: number;
43
+ calibrationSamples: number;
44
+ regularization: "l1" | "l2" | "elasticnet" | "none";
45
+ C: number;
46
+ classWeight: "balanced" | "none";
47
+ trainedAt: string;
48
+ sdkVersion: string;
49
+ }
50
+ /**
51
+ * Model JSON format exported by the Python training script.
52
+ * This is the on-the-wire contract between Python training and TS inference.
53
+ */
54
+ export interface LogRegModel {
55
+ modelVersion: string;
56
+ featureSchemaVersion: string;
57
+ classNames: FaceShape[];
58
+ featureNames: string[];
59
+ numClasses: number;
60
+ numFeatures: number;
61
+ /** Shape: [numClasses][numFeatures]. */
62
+ weights: number[][];
63
+ /** Length: numClasses. */
64
+ bias: number[];
65
+ /** Length: numFeatures. Feature means from training set. */
66
+ mean: number[];
67
+ /** Length: numFeatures. Feature std-devs from training set. */
68
+ std: number[];
69
+ /** How missing optionals were handled during training. */
70
+ missingValueStrategy: "zero";
71
+ calibration: CalibrationParams;
72
+ trainingMeta: TrainingMeta;
73
+ }
74
+ export declare class ModelValidationError extends Error {
75
+ constructor(message: string);
76
+ }
77
+ /**
78
+ * Validate a parsed JSON object against the `LogRegModel` contract.
79
+ * Throws `ModelValidationError` on any mismatch.
80
+ */
81
+ export declare function validateModel(model: unknown): asserts model is LogRegModel;
82
+ /**
83
+ * Parse and validate a model JSON string. Returns a ready-to-use `LogRegModel`.
84
+ */
85
+ export declare function loadModel(json: string): LogRegModel;
86
+ /**
87
+ * Logistic-regression face shape classifier (spec §9.3).
88
+ *
89
+ * Loads a trained model JSON and produces `FaceShapeCandidate[]` with
90
+ * `matchStrength` values. The `matchStrength` is the (optionally calibrated)
91
+ * softmax probability — but it is NOT advertised as a probability until
92
+ * `calibrated` is `true` and ECE/Brier validation passes.
93
+ */
94
+ export declare class LogRegClassifier {
95
+ private readonly model;
96
+ readonly modelVersion: string;
97
+ readonly featureSchemaVersion: string;
98
+ readonly calibrated: boolean;
99
+ constructor(model: LogRegModel);
100
+ /**
101
+ * Classify face metrics into shape candidates.
102
+ *
103
+ * Returns candidates sorted by `matchStrength` (descending). The
104
+ * `matchStrength` values sum to 1.0 (softmax normalization).
105
+ */
106
+ classify(metrics: FaceMetrics): FaceShapeCandidate[];
107
+ /**
108
+ * Metadata for the `FaceShapeResult.model` field.
109
+ */
110
+ modelMeta(): {
111
+ engine: "logreg";
112
+ version: string;
113
+ featureSchemaVersion: string;
114
+ calibrated: boolean;
115
+ };
116
+ }
117
+ //# sourceMappingURL=logreg-classifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logreg-classifier.d.ts","sourceRoot":"","sources":["../../src/face/logreg-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAapF,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,EAAE,SAAS,EAQzC,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,QAA4B,CAAC;AAE5D,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,aAAa,GAAG,OAAO,GAAG,UAAU,CAAC;AAE5E,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gDAAgD;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,IAAI,GAAG,IAAI,GAAG,YAAY,GAAG,MAAM,CAAC;IACpD,CAAC,EAAE,MAAM,CAAC;IACV,WAAW,EAAE,UAAU,GAAG,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;IACpB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,4DAA4D;IAC5D,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,+DAA+D;IAC/D,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,0DAA0D;IAC1D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,iBAAiB,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;CAC5B;AAMD,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,WAAW,CAsF1E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAInD;AA4HD;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;gBAEjB,KAAK,EAAE,WAAW;IAQ9B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,kBAAkB,EAAE;IA0BpD;;OAEG;IACH,SAAS,IAAI;QAAE,MAAM,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,oBAAoB,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE;CAQtG"}
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Logistic-regression classifier — pure TypeScript inference (spec §9.3).
3
+ *
4
+ * The model is trained offline in Python (scikit-learn) and exported as a
5
+ * compact JSON file (< 5 KB target). The browser loads this JSON and runs
6
+ * inference without any ML runtime (no ONNX, no TensorFlow.js).
7
+ *
8
+ * The forward pass is: standardize → linear logits → softmax → calibration.
9
+ *
10
+ * **Calibration**: softmax outputs are NOT inherently calibrated probabilities.
11
+ * The model JSON declares which calibration was applied on an independent
12
+ * calibration set (temperature / Platt / isotonic / none). Until ECE/Brier
13
+ * validation passes (spec §9.4), `calibrated` stays `false` and the output
14
+ * is treated as relative `matchStrength`, not probability.
15
+ */
16
+ import { softmax, clamp01 } from "../utils/math.js";
17
+ import { extractFeatures, LOGREG_FEATURE_SCHEMA_VERSION, LOGREG_NUM_FEATURES, LOGREG_FEATURE_NAMES, } from "./feature-extractor.js";
18
+ // ---------------------------------------------------------------------------
19
+ // Model JSON schema
20
+ // ---------------------------------------------------------------------------
21
+ /** Shape class labels in canonical order — matches `weights` rows. */
22
+ export const LOGREG_CLASS_NAMES = [
23
+ "oval",
24
+ "round",
25
+ "square",
26
+ "heart",
27
+ "diamond",
28
+ "oblong",
29
+ "triangle",
30
+ ];
31
+ /** Number of classes — convenience constant. */
32
+ export const LOGREG_NUM_CLASSES = LOGREG_CLASS_NAMES.length;
33
+ // ---------------------------------------------------------------------------
34
+ // Model loading & validation
35
+ // ---------------------------------------------------------------------------
36
+ export class ModelValidationError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "ModelValidationError";
40
+ }
41
+ }
42
+ /**
43
+ * Validate a parsed JSON object against the `LogRegModel` contract.
44
+ * Throws `ModelValidationError` on any mismatch.
45
+ */
46
+ export function validateModel(model) {
47
+ const m = model;
48
+ if (!m || typeof m !== "object") {
49
+ throw new ModelValidationError("Model must be an object");
50
+ }
51
+ const required = [
52
+ "modelVersion", "featureSchemaVersion", "classNames", "featureNames",
53
+ "numClasses", "numFeatures", "weights", "bias", "mean", "std",
54
+ "missingValueStrategy", "calibration", "trainingMeta",
55
+ ];
56
+ for (const key of required) {
57
+ if (!(key in m)) {
58
+ throw new ModelValidationError(`Missing required field: ${key}`);
59
+ }
60
+ }
61
+ if (m.featureSchemaVersion !== LOGREG_FEATURE_SCHEMA_VERSION) {
62
+ throw new ModelValidationError(`Feature schema version mismatch: model has "${m.featureSchemaVersion}", ` +
63
+ `expected "${LOGREG_FEATURE_SCHEMA_VERSION}"`);
64
+ }
65
+ const numClasses = m.numClasses;
66
+ const numFeatures = m.numFeatures;
67
+ if (numClasses !== LOGREG_NUM_CLASSES) {
68
+ throw new ModelValidationError(`numClasses mismatch: ${numClasses} vs expected ${LOGREG_NUM_CLASSES}`);
69
+ }
70
+ if (numFeatures !== LOGREG_NUM_FEATURES) {
71
+ throw new ModelValidationError(`numFeatures mismatch: ${numFeatures} vs expected ${LOGREG_NUM_FEATURES}`);
72
+ }
73
+ const weights = m.weights;
74
+ if (!Array.isArray(weights) || weights.length !== numClasses) {
75
+ throw new ModelValidationError(`weights must have ${numClasses} rows`);
76
+ }
77
+ for (let i = 0; i < weights.length; i++) {
78
+ if (!Array.isArray(weights[i]) || weights[i].length !== numFeatures) {
79
+ throw new ModelValidationError(`weights[${i}] must have ${numFeatures} columns`);
80
+ }
81
+ }
82
+ const bias = m.bias;
83
+ if (!Array.isArray(bias) || bias.length !== numClasses) {
84
+ throw new ModelValidationError(`bias must have length ${numClasses}`);
85
+ }
86
+ const mean = m.mean;
87
+ if (!Array.isArray(mean) || mean.length !== numFeatures) {
88
+ throw new ModelValidationError(`mean must have length ${numFeatures}`);
89
+ }
90
+ const std = m.std;
91
+ if (!Array.isArray(std) || std.length !== numFeatures) {
92
+ throw new ModelValidationError(`std must have length ${numFeatures}`);
93
+ }
94
+ const featureNames = m.featureNames;
95
+ if (!Array.isArray(featureNames) || featureNames.length !== numFeatures) {
96
+ throw new ModelValidationError(`featureNames must have length ${numFeatures}`);
97
+ }
98
+ for (let i = 0; i < featureNames.length; i++) {
99
+ if (featureNames[i] !== LOGREG_FEATURE_NAMES[i]) {
100
+ throw new ModelValidationError(`featureNames[${i}] mismatch: "${featureNames[i]}" vs expected "${LOGREG_FEATURE_NAMES[i]}"`);
101
+ }
102
+ }
103
+ const classNames = m.classNames;
104
+ if (!Array.isArray(classNames) || classNames.length !== numClasses) {
105
+ throw new ModelValidationError(`classNames must have length ${numClasses}`);
106
+ }
107
+ for (let i = 0; i < classNames.length; i++) {
108
+ if (classNames[i] !== LOGREG_CLASS_NAMES[i]) {
109
+ throw new ModelValidationError(`classNames[${i}] mismatch: "${classNames[i]}" vs expected "${LOGREG_CLASS_NAMES[i]}"`);
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Parse and validate a model JSON string. Returns a ready-to-use `LogRegModel`.
115
+ */
116
+ export function loadModel(json) {
117
+ const parsed = JSON.parse(json);
118
+ validateModel(parsed);
119
+ return parsed;
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Inference
123
+ // ---------------------------------------------------------------------------
124
+ /** Round to 3 decimal places for stable, readable output. */
125
+ function round3(n) {
126
+ return Math.round(n * 1000) / 1000;
127
+ }
128
+ /**
129
+ * Apply isotonic regression calibration to a single value.
130
+ * Uses linear interpolation between stored breakpoints.
131
+ */
132
+ function applyIsotonic(value, map) {
133
+ const { inputs, outputs } = map;
134
+ if (inputs.length === 0)
135
+ return value;
136
+ // Below the first breakpoint
137
+ if (value <= inputs[0])
138
+ return outputs[0];
139
+ // Above the last breakpoint
140
+ if (value >= inputs[inputs.length - 1])
141
+ return outputs[outputs.length - 1];
142
+ // Binary search for the interval
143
+ let lo = 0;
144
+ let hi = inputs.length - 1;
145
+ while (hi - lo > 1) {
146
+ const mid = (lo + hi) >> 1;
147
+ if (inputs[mid] <= value)
148
+ lo = mid;
149
+ else
150
+ hi = mid;
151
+ }
152
+ // Linear interpolation
153
+ const x0 = inputs[lo];
154
+ const x1 = inputs[hi];
155
+ const y0 = outputs[lo];
156
+ const y1 = outputs[hi];
157
+ const t = x1 > x0 ? (value - x0) / (x1 - x0) : 0;
158
+ return y0 + t * (y1 - y0);
159
+ }
160
+ /**
161
+ * Standardize a feature vector: (x - mean) / std.
162
+ * Features with std == 0 (constant in training) are centered but not scaled.
163
+ */
164
+ function standardize(features, mean, std) {
165
+ return features.map((x, i) => {
166
+ const s = std[i];
167
+ return s > 1e-9 ? (x - mean[i]) / s : x - mean[i];
168
+ });
169
+ }
170
+ /**
171
+ * Compute raw logits: z = W·x + b.
172
+ * `weights` is [numClasses][numFeatures], `bias` is [numClasses].
173
+ */
174
+ function computeLogits(features, weights, bias) {
175
+ return weights.map((row, c) => {
176
+ let z = bias[c];
177
+ for (let i = 0; i < features.length; i++) {
178
+ z += row[i] * features[i];
179
+ }
180
+ return z;
181
+ });
182
+ }
183
+ /**
184
+ * Apply calibration to raw softmax probabilities.
185
+ *
186
+ * - `none`: return raw softmax (relative match, NOT a probability).
187
+ * - `temperature`: re-softmax with `logits / temperature`.
188
+ * - `platt`: apply per-class sigmoid `sigmoid(a * logit + b)`, then renormalize.
189
+ * - `isotonic`: apply per-class isotonic regression on the softmax output.
190
+ *
191
+ * Note: Platt and isotonic calibration are validated on an independent
192
+ * calibration set. Until ECE ≤ 0.10 (spec §9.4), `calibrated` stays `false`.
193
+ */
194
+ function applyCalibration(logits, softmaxProbs, calibration) {
195
+ switch (calibration.type) {
196
+ case "none":
197
+ return softmaxProbs;
198
+ case "temperature": {
199
+ const T = calibration.temperature ?? 1;
200
+ return softmax(logits, T);
201
+ }
202
+ case "platt": {
203
+ const a = calibration.plattA ?? 1;
204
+ const b = calibration.plattB ?? 0;
205
+ const calibrated = logits.map((z) => {
206
+ const w = a * z + b;
207
+ return 1 / (1 + Math.exp(-w));
208
+ });
209
+ const sum = calibrated.reduce((s, v) => s + v, 0);
210
+ return sum > 1e-9 ? calibrated.map((v) => v / sum) : calibrated;
211
+ }
212
+ case "isotonic": {
213
+ const maps = calibration.isotonic;
214
+ if (!maps || maps.length !== logits.length)
215
+ return softmaxProbs;
216
+ const calibrated = softmaxProbs.map((p, i) => applyIsotonic(p, maps[i]));
217
+ const sum = calibrated.reduce((s, v) => s + v, 0);
218
+ return sum > 1e-9 ? calibrated.map((v) => v / sum) : calibrated;
219
+ }
220
+ default:
221
+ return softmaxProbs;
222
+ }
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // Classifier
226
+ // ---------------------------------------------------------------------------
227
+ /**
228
+ * Logistic-regression face shape classifier (spec §9.3).
229
+ *
230
+ * Loads a trained model JSON and produces `FaceShapeCandidate[]` with
231
+ * `matchStrength` values. The `matchStrength` is the (optionally calibrated)
232
+ * softmax probability — but it is NOT advertised as a probability until
233
+ * `calibrated` is `true` and ECE/Brier validation passes.
234
+ */
235
+ export class LogRegClassifier {
236
+ constructor(model) {
237
+ Object.defineProperty(this, "model", {
238
+ enumerable: true,
239
+ configurable: true,
240
+ writable: true,
241
+ value: void 0
242
+ });
243
+ Object.defineProperty(this, "modelVersion", {
244
+ enumerable: true,
245
+ configurable: true,
246
+ writable: true,
247
+ value: void 0
248
+ });
249
+ Object.defineProperty(this, "featureSchemaVersion", {
250
+ enumerable: true,
251
+ configurable: true,
252
+ writable: true,
253
+ value: void 0
254
+ });
255
+ Object.defineProperty(this, "calibrated", {
256
+ enumerable: true,
257
+ configurable: true,
258
+ writable: true,
259
+ value: void 0
260
+ });
261
+ validateModel(model);
262
+ this.model = model;
263
+ this.modelVersion = model.modelVersion;
264
+ this.featureSchemaVersion = model.featureSchemaVersion;
265
+ this.calibrated = model.calibration.type !== "none";
266
+ }
267
+ /**
268
+ * Classify face metrics into shape candidates.
269
+ *
270
+ * Returns candidates sorted by `matchStrength` (descending). The
271
+ * `matchStrength` values sum to 1.0 (softmax normalization).
272
+ */
273
+ classify(metrics) {
274
+ // 1. Extract features in canonical order
275
+ const features = extractFeatures(metrics);
276
+ // 2. Standardize using training-set mean/std
277
+ const standardized = standardize(features, this.model.mean, this.model.std);
278
+ // 3. Compute raw logits
279
+ const logits = computeLogits(standardized, this.model.weights, this.model.bias);
280
+ // 4. Softmax → raw probabilities (relative match strength)
281
+ const rawProbs = softmax(logits);
282
+ // 5. Apply calibration (if any)
283
+ const calibrated = applyCalibration(logits, rawProbs, this.model.calibration);
284
+ // 6. Build candidates sorted by match strength
285
+ const candidates = this.model.classNames.map((shape, i) => ({
286
+ shape,
287
+ matchStrength: round3(clamp01(calibrated[i])),
288
+ }));
289
+ candidates.sort((a, b) => b.matchStrength - a.matchStrength);
290
+ return candidates;
291
+ }
292
+ /**
293
+ * Metadata for the `FaceShapeResult.model` field.
294
+ */
295
+ modelMeta() {
296
+ return {
297
+ engine: "logreg",
298
+ version: this.modelVersion,
299
+ featureSchemaVersion: this.featureSchemaVersion,
300
+ calibrated: this.calibrated,
301
+ };
302
+ }
303
+ }
304
+ //# sourceMappingURL=logreg-classifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logreg-classifier.js","sourceRoot":"","sources":["../../src/face/logreg-classifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Feature flag support for gradual rollout and rollback (spec §12, §14.1).
3
+ *
4
+ * The SDK is the single production classification core. A feature flag
5
+ * controls whether the SDK path is active for a given request and which
6
+ * engine (rules / logreg) runs. Rollback = disable the flag or pin a
7
+ * previous SDK package version — no parallel classifier is kept.
8
+ */
9
+ /**
10
+ * Feature-flag configuration. Passed into the SDK factory or evaluated
11
+ * independently by the host application.
12
+ */
13
+ export interface FeatureFlagConfig {
14
+ /** Master switch. When `false`, the host must fall back to its previous
15
+ * behaviour (e.g. hide the free detector). Default `false`. */
16
+ enabled: boolean;
17
+ /** 0–100. Percentage of users (bucketed by `stableId`) that get the SDK
18
+ * path. `100` = everyone. Ignored when `enabled` is `false`. Default `100`. */
19
+ rolloutPercentage?: number;
20
+ /** Override the classification engine. Defaults to the SDK's built-in
21
+ * rule scorer (`"rules"`). Set to `"logreg"` once PR 6 ships. */
22
+ engine?: "rules" | "logreg";
23
+ /** Optional SDK version pin for audit / rollback tracking. */
24
+ sdkVersion?: string;
25
+ }
26
+ /**
27
+ * Determine whether the SDK face-shape path should be active for a given
28
+ * request.
29
+ *
30
+ * - `enabled: false` → always `false`.
31
+ * - `rolloutPercentage >= 100` (or unset) → `true`.
32
+ * - Otherwise, hash `stableId` to a 0–99 bucket and compare.
33
+ *
34
+ * When no `stableId` is provided, the flag is all-or-nothing: `enabled`
35
+ * alone decides.
36
+ */
37
+ export declare function evaluateFeatureFlag(config: FeatureFlagConfig, stableId?: string): boolean;
38
+ /**
39
+ * Resolve the effective engine, honouring the flag override.
40
+ * Falls back to `"rules"` when the flag is disabled or no override is set.
41
+ */
42
+ export declare function resolveEngine(config: FeatureFlagConfig | undefined): "rules" | "logreg";
43
+ //# sourceMappingURL=feature-flag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.d.ts","sourceRoot":"","sources":["../src/feature-flag.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;oEACgE;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB;oFACgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;sEACkE;IAClE,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoBD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAWT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,SAAS,GACpC,OAAO,GAAG,QAAQ,CAGpB"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Feature flag support for gradual rollout and rollback (spec §12, §14.1).
3
+ *
4
+ * The SDK is the single production classification core. A feature flag
5
+ * controls whether the SDK path is active for a given request and which
6
+ * engine (rules / logreg) runs. Rollback = disable the flag or pin a
7
+ * previous SDK package version — no parallel classifier is kept.
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Evaluation
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * FNV-1a 32-bit hash — deterministic, fast, no dependencies.
14
+ * Used for stable percentage bucketing.
15
+ */
16
+ function fnv1aHash(input) {
17
+ let hash = 0x811c9dc5;
18
+ for (let i = 0; i < input.length; i++) {
19
+ hash ^= input.charCodeAt(i);
20
+ hash = Math.imul(hash, 0x01000193);
21
+ }
22
+ // Force unsigned 32-bit
23
+ return hash >>> 0;
24
+ }
25
+ /**
26
+ * Determine whether the SDK face-shape path should be active for a given
27
+ * request.
28
+ *
29
+ * - `enabled: false` → always `false`.
30
+ * - `rolloutPercentage >= 100` (or unset) → `true`.
31
+ * - Otherwise, hash `stableId` to a 0–99 bucket and compare.
32
+ *
33
+ * When no `stableId` is provided, the flag is all-or-nothing: `enabled`
34
+ * alone decides.
35
+ */
36
+ export function evaluateFeatureFlag(config, stableId) {
37
+ if (!config.enabled)
38
+ return false;
39
+ const pct = config.rolloutPercentage;
40
+ if (pct == null || pct >= 100)
41
+ return true;
42
+ if (pct <= 0)
43
+ return false;
44
+ if (!stableId)
45
+ return true; // no bucketing key → treat as enabled
46
+ const bucket = fnv1aHash(stableId) % 100;
47
+ return bucket < pct;
48
+ }
49
+ /**
50
+ * Resolve the effective engine, honouring the flag override.
51
+ * Falls back to `"rules"` when the flag is disabled or no override is set.
52
+ */
53
+ export function resolveEngine(config) {
54
+ if (!config?.enabled)
55
+ return "rules";
56
+ return config.engine ?? "rules";
57
+ }
58
+ //# sourceMappingURL=feature-flag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.js","sourceRoot":"","sources":["../src/feature-flag.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwBH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;GAGG;AACH,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACrC,CAAC;IACD,wBAAwB;IACxB,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAyB,EACzB,QAAiB;IAEjB,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACrC,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3B,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC,CAAC,sCAAsC;IAElE,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;IACzC,OAAO,MAAM,GAAG,GAAG,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAqC;IAErC,IAAI,CAAC,MAAM,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC;AAClC,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * VisuTry SDK — lightweight i18n for user-facing error messages.
3
+ *
4
+ * The SDK keeps all hard-coded user-facing strings behind the `t()` lookup so
5
+ * that platform adapters (web / wechat) and host applications can localise the
6
+ * error messages surfaced to end users. The message catalogue is intentionally
7
+ * small: it only covers the canonical SDK error strings emitted via
8
+ * `createSDKError`. Callers can switch the active locale at runtime with
9
+ * `setLocale()`; unknown keys fall back to English, then to the key itself.
10
+ */
11
+ export type Locale = "en" | "zh-CN" | "ja" | "ko";
12
+ export declare function setLocale(locale: Locale): void;
13
+ export declare function getLocale(): Locale;
14
+ export declare function t(key: string): string;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/i18n/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC;AA2ElD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE9C;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErC"}