Built the Sentinel Tab
This commit is contained in:
107
app/src/utils/landmarkPacketEncoder.ts
Normal file
107
app/src/utils/landmarkPacketEncoder.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* landmarkPacketEncoder — Converts a MediaPipe FaceLandmarkerResult into
|
||||
* the compact BiometricPacket format sent over the WebSocket to FastAPI.
|
||||
*
|
||||
* Only the 52 blend shapes relevant to micro-expression analysis are forwarded.
|
||||
* Raw landmark coordinates are excluded to minimise payload size.
|
||||
*/
|
||||
|
||||
import type { BiometricPacket } from '@/types';
|
||||
|
||||
// The MediaPipe canonical blend shape keys we care about for QD scoring.
|
||||
// These cover eye blinks, smiles, brow raises, and jaw movements.
|
||||
export const QD_BLEND_SHAPES: readonly string[] = [
|
||||
'eyeBlinkLeft',
|
||||
'eyeBlinkRight',
|
||||
'eyeWideLeft',
|
||||
'eyeWideRight',
|
||||
'eyeLookUpLeft',
|
||||
'eyeLookUpRight',
|
||||
'eyeLookDownLeft',
|
||||
'eyeLookDownRight',
|
||||
'eyeLookInLeft',
|
||||
'eyeLookInRight',
|
||||
'eyeLookOutLeft',
|
||||
'eyeLookOutRight',
|
||||
'eyeSquintLeft',
|
||||
'eyeSquintRight',
|
||||
'browDownLeft',
|
||||
'browDownRight',
|
||||
'browInnerUp',
|
||||
'browOuterUpLeft',
|
||||
'browOuterUpRight',
|
||||
'cheekPuff',
|
||||
'cheekSquintLeft',
|
||||
'cheekSquintRight',
|
||||
'noseSneerLeft',
|
||||
'noseSneerRight',
|
||||
'mouthSmileLeft',
|
||||
'mouthSmileRight',
|
||||
'mouthFrownLeft',
|
||||
'mouthFrownRight',
|
||||
'mouthDimpleLeft',
|
||||
'mouthDimpleRight',
|
||||
'mouthStretchLeft',
|
||||
'mouthStretchRight',
|
||||
'mouthRollLower',
|
||||
'mouthRollUpper',
|
||||
'mouthShrugLower',
|
||||
'mouthShrugUpper',
|
||||
'mouthPressLeft',
|
||||
'mouthPressRight',
|
||||
'mouthLowerDownLeft',
|
||||
'mouthLowerDownRight',
|
||||
'mouthUpperUpLeft',
|
||||
'mouthUpperUpRight',
|
||||
'jawOpen',
|
||||
'jawForward',
|
||||
'jawLeft',
|
||||
'jawRight',
|
||||
'mouthClose',
|
||||
'mouthFunnel',
|
||||
'mouthPucker',
|
||||
'mouthLeft',
|
||||
'mouthRight',
|
||||
'tongueOut',
|
||||
];
|
||||
|
||||
/**
|
||||
* Encodes a FaceLandmarkerResult blend shapes array into a compact packet.
|
||||
*
|
||||
* @param rawBlendShapes The `faceBlendshapes[0].categories` array from MediaPipe
|
||||
* @param videoTsMs Current marketing video playback position in milliseconds
|
||||
* @param leadId The active lead being tracked
|
||||
* @param sessionId UUID identifying this perception session
|
||||
*/
|
||||
export function encodeLandmarkPacket(
|
||||
rawBlendShapes: Array<{ categoryName: string; score: number }>,
|
||||
videoTsMs: number,
|
||||
leadId: string,
|
||||
sessionId: string,
|
||||
): BiometricPacket {
|
||||
const blend_shapes: Record<string, number> = {};
|
||||
|
||||
for (const shape of rawBlendShapes) {
|
||||
if (QD_BLEND_SHAPES.includes(shape.categoryName)) {
|
||||
// Round to 4dp — sufficient precision, reduces JSON size ~30%
|
||||
blend_shapes[shape.categoryName] = Math.round(shape.score * 10_000) / 10_000;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lead_id: leadId,
|
||||
session_id: sessionId,
|
||||
video_ts_ms: videoTsMs,
|
||||
blend_shapes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is meaningful facial expression activity in the packet
|
||||
* (i.e., at least one key blend shape exceeds the noise threshold).
|
||||
* Used to skip sending "blank face" packets.
|
||||
*/
|
||||
export function hasSignificantActivity(packet: BiometricPacket): boolean {
|
||||
const KEY_SHAPES = ['mouthSmileLeft', 'mouthSmileRight', 'browInnerUp', 'eyeWideLeft', 'eyeWideRight', 'jawOpen'];
|
||||
return KEY_SHAPES.some((key) => (packet.blend_shapes[key] ?? 0) > 0.12);
|
||||
}
|
||||
Reference in New Issue
Block a user