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, }; })();