WhistleBloom / frontend /face_features.js
ZhaoJingzi's picture
Upload 27 files
a1772f6 verified
Raw
History Blame Contribute Delete
1.64 kB
window.WhistleCoach = window.WhistleCoach || {};
window.WhistleCoach.FaceFeatures = (() => {
const clamp = (value, minimum = 0, maximum = 1) =>
Math.min(maximum, Math.max(minimum, value));
function toFeaturePayload(face) {
return {
face_visible: Boolean(face?.visible),
face_centered: Boolean(face?.centered),
mouth_opening_ratio: Number(face?.openingRatio || 0),
lip_roundness_score: Number(face?.puckerScore || 0),
upper_lip_lift_score: Number(face?.upperLipLiftScore || 0),
jaw_stability_score: Number(face?.jawScore || 0),
mouth_symmetry_score: Number(face?.symmetry || 0),
mouth_shape_score: Number(face?.score || 0),
};
}
function classifyMouthShape(features) {
if (!features.face_visible || !features.face_centered) return "no_face";
if (features.mouth_opening_ratio > 0.34 || features.jaw_stability_score < 0.45) {
return "mouth_too_open";
}
if (features.lip_roundness_score < 0.52) return "not_rounded";
if (features.mouth_symmetry_score < 0.58) return "asymmetric_mouth";
if (features.mouth_shape_score >= 0.62) return "mouth_ready";
return "adjust_mouth";
}
function smoothFeature(previous, next, alpha = 0.28) {
if (!previous) return next;
const output = { ...next };
for (const [key, value] of Object.entries(next)) {
if (typeof value === "number") {
output[key] = clamp(previous[key] * (1 - alpha) + value * alpha);
}
}
return output;
}
return {
toFeaturePayload,
classifyMouthShape,
smoothFeature,
};
})();