Spaces:
Sleeping
Sleeping
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.
|
|
|
|
|
|
|
| 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:
|
| 171 |
-
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.
|
| 21 |
if (v.BRI > -0.2 && v.EAR < 0.18) return "FRUSTRATED";
|
| 22 |
-
if (v.LCP > 0.
|
| 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.
|
| 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 |
|