@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.
- package/README.md +58 -0
- package/dist/__fixtures__/faceFixtures.d.ts +13 -0
- package/dist/__fixtures__/faceFixtures.d.ts.map +1 -0
- package/dist/__fixtures__/faceFixtures.js +206 -0
- package/dist/__fixtures__/faceFixtures.js.map +1 -0
- package/dist/adapter/visutry-site.d.ts +76 -0
- package/dist/adapter/visutry-site.d.ts.map +1 -0
- package/dist/adapter/visutry-site.js +172 -0
- package/dist/adapter/visutry-site.js.map +1 -0
- package/dist/analytics.d.ts +89 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +49 -0
- package/dist/analytics.js.map +1 -0
- package/dist/coordinate/CoordinateSystem.d.ts +46 -0
- package/dist/coordinate/CoordinateSystem.d.ts.map +1 -0
- package/dist/coordinate/CoordinateSystem.js +88 -0
- package/dist/coordinate/CoordinateSystem.js.map +1 -0
- package/dist/face/FaceMetricsCalculator.d.ts +52 -0
- package/dist/face/FaceMetricsCalculator.d.ts.map +1 -0
- package/dist/face/FaceMetricsCalculator.js +375 -0
- package/dist/face/FaceMetricsCalculator.js.map +1 -0
- package/dist/face/FaceSemanticMapper.d.ts +54 -0
- package/dist/face/FaceSemanticMapper.d.ts.map +1 -0
- package/dist/face/FaceSemanticMapper.js +129 -0
- package/dist/face/FaceSemanticMapper.js.map +1 -0
- package/dist/face/FaceShapeScorer.d.ts +35 -0
- package/dist/face/FaceShapeScorer.d.ts.map +1 -0
- package/dist/face/FaceShapeScorer.js +265 -0
- package/dist/face/FaceShapeScorer.js.map +1 -0
- package/dist/face/feature-extractor.d.ts +46 -0
- package/dist/face/feature-extractor.d.ts.map +1 -0
- package/dist/face/feature-extractor.js +106 -0
- package/dist/face/feature-extractor.js.map +1 -0
- package/dist/face/logreg-classifier.d.ts +117 -0
- package/dist/face/logreg-classifier.d.ts.map +1 -0
- package/dist/face/logreg-classifier.js +304 -0
- package/dist/face/logreg-classifier.js.map +1 -0
- package/dist/feature-flag.d.ts +43 -0
- package/dist/feature-flag.d.ts.map +1 -0
- package/dist/feature-flag.js +58 -0
- package/dist/feature-flag.js.map +1 -0
- package/dist/i18n/index.d.ts +15 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +91 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/ManifestValidator.d.ts +23 -0
- package/dist/manifest/ManifestValidator.d.ts.map +1 -0
- package/dist/manifest/ManifestValidator.js +129 -0
- package/dist/manifest/ManifestValidator.js.map +1 -0
- package/dist/pose/GlassesPoseSolver.d.ts +64 -0
- package/dist/pose/GlassesPoseSolver.d.ts.map +1 -0
- package/dist/pose/GlassesPoseSolver.js +299 -0
- package/dist/pose/GlassesPoseSolver.js.map +1 -0
- package/dist/privacy/PrivacyGuard.d.ts +44 -0
- package/dist/privacy/PrivacyGuard.d.ts.map +1 -0
- package/dist/privacy/PrivacyGuard.js +105 -0
- package/dist/privacy/PrivacyGuard.js.map +1 -0
- package/dist/quality/QualityGate.d.ts +22 -0
- package/dist/quality/QualityGate.d.ts.map +1 -0
- package/dist/quality/QualityGate.js +161 -0
- package/dist/quality/QualityGate.js.map +1 -0
- package/dist/smoothing/PoseSmoothing.d.ts +37 -0
- package/dist/smoothing/PoseSmoothing.d.ts.map +1 -0
- package/dist/smoothing/PoseSmoothing.js +146 -0
- package/dist/smoothing/PoseSmoothing.js.map +1 -0
- package/dist/types/index.d.ts +444 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +20 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/math.d.ts +40 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +149 -0
- package/dist/utils/math.js.map +1 -0
- 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"}
|