akashkolte Claude Sonnet 4.6 commited on
Commit
1f17a48
·
1 Parent(s): 623fcd9

Add debounce + tighten sensing thresholds

Browse files

- Debounce affect (8 frames) and gesture (5 frames) so single noisy
frames no longer flip the UI label
- Gesture clears to null when unstable instead of sticking at last value
- EMA alpha 0.4 → 0.2 for smoother affect signal
- HAPPY LCP 0.008 → 0.012, FRUSTRATED LCP -0.015 → -0.018
- WAVING requires all 5 fingers extended at 0.7 norm (was 4 fingers at 0.5)
so a relaxed open hand no longer triggers it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

frontend/src/hooks/useSensing.ts CHANGED
@@ -15,7 +15,9 @@ import {
15
  } from "../lib/sensing";
16
  import { DEFAULT_AIR_TEMPLATES } from "../lib/airTemplates";
17
 
18
- const EMA_ALPHA = 0.4;
 
 
19
 
20
  export function useSensing() {
21
  const faceLandmarkerRef = useRef<FaceLandmarker | null>(null);
@@ -28,6 +30,8 @@ export function useSensing() {
28
  const neutralLCPRef = useRef<number | null>(null);
29
  const calibBufferRef = useRef<number[]>([]);
30
  const smoothedRef = useRef({ MAR: 0, EAR: 0.3, BRI: -0.3, LCP: 0 });
 
 
31
  const initingRef = useRef(false);
32
  const [ready, setReady] = useState(false);
33
  const [initError, setInitError] = useState<string | null>(null);
@@ -166,9 +170,27 @@ export function useSensing() {
166
 
167
  const newAirText = airWriterRef.current.getText();
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  setSensing((prev) => ({
170
- affect: affect ?? prev.affect,
171
- gestureTag: gestureTag ?? prev.gestureTag,
172
  gazeBucket: gazeBucket ?? prev.gazeBucket,
173
  airWrittenText: newAirText
174
  ? prev.airWrittenText + newAirText
@@ -198,6 +220,8 @@ export function useSensing() {
198
  neutralLCPRef.current = null;
199
  calibBufferRef.current = [];
200
  smoothedRef.current = { MAR: 0, EAR: 0.3, BRI: -0.3, LCP: 0 };
 
 
201
  gazeTrackerRef.current.reset();
202
  headTrackerRef.current.reset();
203
  setSensing({
 
15
  } from "../lib/sensing";
16
  import { DEFAULT_AIR_TEMPLATES } from "../lib/airTemplates";
17
 
18
+ const EMA_ALPHA = 0.2;
19
+ const GESTURE_DEBOUNCE_FRAMES = 5;
20
+ const AFFECT_DEBOUNCE_FRAMES = 8;
21
 
22
  export function useSensing() {
23
  const faceLandmarkerRef = useRef<FaceLandmarker | null>(null);
 
30
  const neutralLCPRef = useRef<number | null>(null);
31
  const calibBufferRef = useRef<number[]>([]);
32
  const smoothedRef = useRef({ MAR: 0, EAR: 0.3, BRI: -0.3, LCP: 0 });
33
+ const gestureCountRef = useRef<{ tag: SensingState["gestureTag"]; count: number }>({ tag: null, count: 0 });
34
+ const affectCountRef = useRef<{ affect: SensingState["affect"]; count: number }>({ affect: null, count: 0 });
35
  const initingRef = useRef(false);
36
  const [ready, setReady] = useState(false);
37
  const [initError, setInitError] = useState<string | null>(null);
 
170
 
171
  const newAirText = airWriterRef.current.getText();
172
 
173
+ if (gestureTag === gestureCountRef.current.tag) {
174
+ gestureCountRef.current.count++;
175
+ } else {
176
+ gestureCountRef.current = { tag: gestureTag, count: 1 };
177
+ }
178
+ const stableGesture = gestureCountRef.current.count >= GESTURE_DEBOUNCE_FRAMES
179
+ ? gestureTag
180
+ : null;
181
+
182
+ if (affect === affectCountRef.current.affect) {
183
+ affectCountRef.current.count++;
184
+ } else {
185
+ affectCountRef.current = { affect, count: 1 };
186
+ }
187
+ const stableAffect = affectCountRef.current.count >= AFFECT_DEBOUNCE_FRAMES
188
+ ? affect
189
+ : null;
190
+
191
  setSensing((prev) => ({
192
+ affect: stableAffect ?? prev.affect,
193
+ gestureTag: stableGesture,
194
  gazeBucket: gazeBucket ?? prev.gazeBucket,
195
  airWrittenText: newAirText
196
  ? prev.airWrittenText + newAirText
 
220
  neutralLCPRef.current = null;
221
  calibBufferRef.current = [];
222
  smoothedRef.current = { MAR: 0, EAR: 0.3, BRI: -0.3, LCP: 0 };
223
+ gestureCountRef.current = { tag: null, count: 0 };
224
+ affectCountRef.current = { affect: null, count: 0 };
225
  gazeTrackerRef.current.reset();
226
  headTrackerRef.current.reset();
227
  setSensing({
frontend/src/lib/sensing.ts CHANGED
@@ -17,9 +17,9 @@ export function classifyAffect(v: AffectVector): Affect {
17
  // EAR is absolute ratio — lower = eyes more closed / squinting
18
  if (v.BRI < -0.35 && v.MAR > 0.4) return "SURPRISED";
19
  // FRUSTRATED: a clear frown, OR brows lowered + squinting — either signals displeasure
20
- if (v.LCP < -0.015) return "FRUSTRATED";
21
  if (v.BRI > -0.2 && v.EAR < 0.18) return "FRUSTRATED";
22
- if (v.LCP > 0.008) return "HAPPY";
23
  return "NEUTRAL";
24
  }
25
 
@@ -123,8 +123,8 @@ export function classifyGesture(landmarks: Point3D[]): GestureName | null {
123
  if (thumbTip.y < -0.3 && fingersCurled) return "THUMBS_UP";
124
  if (thumbTip.y > 0.3 && fingersCurled) return "THUMBS_DOWN";
125
 
126
- const allExtended = [indexTip, middleTip, ringTip, pinkyTip].every(
127
- (tip) => norm3(tip) > 0.5
128
  );
129
  if (allExtended) return "WAVING";
130
 
 
17
  // EAR is absolute ratio — lower = eyes more closed / squinting
18
  if (v.BRI < -0.35 && v.MAR > 0.4) return "SURPRISED";
19
  // FRUSTRATED: a clear frown, OR brows lowered + squinting — either signals displeasure
20
+ if (v.LCP < -0.018) return "FRUSTRATED";
21
  if (v.BRI > -0.2 && v.EAR < 0.18) return "FRUSTRATED";
22
+ if (v.LCP > 0.012) return "HAPPY";
23
  return "NEUTRAL";
24
  }
25
 
 
123
  if (thumbTip.y < -0.3 && fingersCurled) return "THUMBS_UP";
124
  if (thumbTip.y > 0.3 && fingersCurled) return "THUMBS_DOWN";
125
 
126
+ const allExtended = [indexTip, middleTip, ringTip, pinkyTip, thumbTip].every(
127
+ (tip) => norm3(tip) > 0.7
128
  );
129
  if (allExtended) return "WAVING";
130