11import { PosePoint } from '@domain/landmarks/value-objects/BodyLandmarks' ;
22import { HandPoint , type Landmark } from '@domain/landmarks/value-objects/Landmark' ;
33import type { LandmarkFrame } from '@domain/landmarks/value-objects/LandmarkFrame' ;
4+ import type {
5+ SignatureBlock ,
6+ SignatureProfile ,
7+ } from '@domain/recognition/value-objects/RecognitionDiagnostics' ;
48
59/**
610 * What the trained LSE vocabulary model reads.
@@ -25,6 +29,14 @@ const FACE_FLOATS = 6;
2529const FRAME_FLOATS = HAND_FLOATS * 2 + TORSO_FLOATS + FACE_FLOATS ;
2630export const VOCABULARY_SIGNATURE_LENGTH = VOCABULARY_FRAMES * FRAME_FLOATS ;
2731
32+ /** Where each part lives inside one frame's slice, for reading a built signature back. */
33+ const BLOCKS = {
34+ rightHand : [ 0 , HAND_FLOATS ] ,
35+ leftHand : [ HAND_FLOATS , HAND_FLOATS * 2 ] ,
36+ torso : [ HAND_FLOATS * 2 , HAND_FLOATS * 2 + TORSO_FLOATS ] ,
37+ face : [ HAND_FLOATS * 2 + TORSO_FLOATS , FRAME_FLOATS ] ,
38+ } as const ;
39+
2840/**
2941 * Face Mesh indices for the landmarks that carry grammar while signing. MediaPipe's
3042 * FaceLandmarker returns 478 points; 0–467 are the same mesh these indices refer to.
@@ -70,6 +82,44 @@ export function vocabularySignature(window: readonly LandmarkFrame[]): Float32Ar
7082 return signature ;
7183}
7284
85+ /**
86+ * Reads a built signature back as four per-part summaries.
87+ *
88+ * The model scores near-noise in the browser while measuring 0.741 offline, and the feature
89+ * code has verified parity with the trainer — so what differs is the input, not the maths.
90+ * Comparing these four numbers against the same statistics over SWL-LSE's test split says
91+ * which part is wrong without guessing: a part that is empty here and never empty in
92+ * training, or an order-of-magnitude gap, is the answer.
93+ */
94+ export function profileSignature ( signature : Float32Array ) : SignatureProfile {
95+ const read = ( from : number , to : number ) : SignatureBlock => {
96+ let empty = 0 ;
97+ let total = 0 ;
98+ let count = 0 ;
99+ for ( let slot = 0 ; slot < VOCABULARY_FRAMES ; slot += 1 ) {
100+ const base = slot * FRAME_FLOATS ;
101+ let magnitude = 0 ;
102+ for ( let i = from ; i < to ; i += 1 ) magnitude += Math . abs ( signature [ base + i ] ?? 0 ) ;
103+ if ( magnitude === 0 ) empty += 1 ;
104+ else {
105+ total += magnitude / ( to - from ) ;
106+ count += 1 ;
107+ }
108+ }
109+ return {
110+ emptyFrames : empty / VOCABULARY_FRAMES ,
111+ meanMagnitude : count ? total / count : 0 ,
112+ } ;
113+ } ;
114+
115+ return {
116+ rightHand : read ( ...BLOCKS . rightHand ) ,
117+ leftHand : read ( ...BLOCKS . leftHand ) ,
118+ torso : read ( ...BLOCKS . torso ) ,
119+ face : read ( ...BLOCKS . face ) ,
120+ } ;
121+ }
122+
73123function sampleIndex ( slot : number , length : number ) : number {
74124 if ( length === 1 ) return 0 ;
75125 return Math . round ( ( slot / ( VOCABULARY_FRAMES - 1 ) ) * ( length - 1 ) ) ;
0 commit comments