Skip to content

Commit 314c604

Browse files
Partha-dev01claude
andcommitted
Tune action detection sensitivity, fix behavior label display
Stage 6: REQUIRED_CONSECUTIVE 8→12, decay -2 on miss, confidence gate >0.4, tighten clap (0.3×scale) and touch_head (0.25×scale). Dot counter updated to 12. Stage 8: Behavior label now recomputed from actual probabilities — shows "Normal Activity" when P(non_autistic) > 50% instead of always showing hand_flapping due to model bias. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f907e68 commit 314c604

4 files changed

Lines changed: 76 additions & 26 deletions

File tree

DOCS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,32 @@ npx playwright test # Run all 30 tests
612612

613613
**Files:**
614614
- Modified: `app/intake/video-capture/page.tsx` (setCamReady moved, stream re-attach effect, description text fix)
615+
616+
### v1.6.2 — 2026-03-05 (Action Detection Tuning, Behavior Label Fix)
617+
618+
**Fixed:**
619+
- **Stage 6 (Action Challenge) hypersensitive detection**: Actions were confirming too quickly (~0.5s) due to low `REQUIRED_CONSECUTIVE` (8) and slow decay (-1 per miss). False positives from YOLO keypoint noise accumulated faster than they decayed.
620+
- `REQUIRED_CONSECUTIVE`: 8 → 12 (~0.8s sustained detection at 15fps)
621+
- Decay on miss: -1 → -2 (noise drops out faster)
622+
- Added confidence gate: only count hits with confidence > 0.4
623+
- Tightened clap threshold: 0.4×scale → 0.3×scale
624+
- Tightened touch_head threshold: 0.3×scale → 0.25×scale (less overlap with touch_nose)
625+
- **Stage 8 (Video Capture) "Hand Flapping" always shown as behavior label**: The body TCN model is biased toward hand_flapping (F1: 0.68) due to training data class imbalance (non_autistic F1 only 0.33). The video overlay now recomputes the display label from actual probabilities:
626+
- When P(non_autistic) > 50%: shows "Normal Activity" with a green badge
627+
- Otherwise: shows the highest ASD-related behavior class (indices 0-4)
628+
- Eliminates frame-mismatch display inconsistencies
629+
630+
**Training Model Notes (from `Autism_code/` analysis):**
631+
- Body TCN best F1: 0.384 (macro-averaged, 6 classes)
632+
- Classes 2 (head_banging) and 4 (toe_walking): 0.0 F1 — zero validation samples
633+
- Class 0 (hand_flapping): 0.68 F1 — strongest, causes bias
634+
- Class 5 (non_autistic): 0.33 F1 — weak recognition
635+
- Long-term fix: retrain with balanced data. Current UI-side fix handles the display bias.
636+
637+
**CI Playwright Warnings (NOT a bug):**
638+
- `CredentialsProviderError` messages in CI logs are expected — CI lacks AWS credentials. Bedrock/Polly APIs fail gracefully and tests verify fallback/mock responses. All 31 tests pass.
639+
640+
**Files:**
641+
- Modified: `app/lib/actions/actionDetector.ts` (REQUIRED_CONSECUTIVE 12, decay -2, confidence gate, tighter thresholds)
642+
- Modified: `app/intake/preparation/page.tsx` (12-dot counter, status text thresholds)
643+
- Modified: `app/components/DetectorVideoCanvas.tsx` (behavior label recomputed from probs, "Normal Activity" display)

app/components/DetectorVideoCanvas.tsx

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"use client";
77
import { useRef, useEffect } from "react";
88
import type { PipelineResult } from "../types/inference";
9+
import { BEHAVIOR_CLASSES } from "../types/inference";
910

1011
// COCO-17 skeleton connections
1112
const SKELETON: [number, number][] = [
@@ -163,21 +164,41 @@ export default function DetectorVideoCanvas({ videoRef, canvasRef, result, isCam
163164
</div>
164165
)}
165166

166-
{/* Behavior label */}
167-
{result?.behavior && (
168-
<div style={{
169-
position: "absolute", top: 8, left: 8, padding: "6px 14px",
170-
borderRadius: "var(--r-md)", fontSize: "0.85rem", fontWeight: 700,
171-
fontFamily: "'Fredoka',sans-serif",
172-
background: "rgba(0,0,0,0.65)", color: "white",
173-
border: "1px solid rgba(255,255,255,0.15)",
174-
}}>
175-
{fmt(result.behavior.className)}
176-
<span style={{ marginLeft: 8, fontSize: "0.75rem", opacity: 0.7 }}>
177-
{(result.behavior.probabilities[result.behavior.predictedClass] * 100).toFixed(0)}%
178-
</span>
179-
</div>
180-
)}
167+
{/* Behavior label — show "Normal Activity" when non_autistic > 50%, otherwise top ASD class */}
168+
{result?.behavior && result.behavior.probabilities.length >= 6 && (() => {
169+
const probs = result.behavior!.probabilities;
170+
const nonAutisticProb = probs[5];
171+
let displayClass: string;
172+
let displayProb: number;
173+
if (nonAutisticProb > 0.5) {
174+
displayClass = "Normal Activity";
175+
displayProb = nonAutisticProb;
176+
} else {
177+
// Find highest ASD behavior (indices 0-4)
178+
let maxIdx = 0, maxP = probs[0];
179+
for (let i = 1; i < 5; i++) {
180+
if (probs[i] > maxP) { maxP = probs[i]; maxIdx = i; }
181+
}
182+
displayClass = fmt(BEHAVIOR_CLASSES[maxIdx]);
183+
displayProb = maxP;
184+
}
185+
const isNormal = nonAutisticProb > 0.5;
186+
return (
187+
<div style={{
188+
position: "absolute", top: 8, left: 8, padding: "6px 14px",
189+
borderRadius: "var(--r-md)", fontSize: "0.85rem", fontWeight: 700,
190+
fontFamily: "'Fredoka',sans-serif",
191+
background: isNormal ? "rgba(58,99,68,0.75)" : "rgba(0,0,0,0.65)",
192+
color: "white",
193+
border: "1px solid rgba(255,255,255,0.15)",
194+
}}>
195+
{displayClass}
196+
<span style={{ marginLeft: 8, fontSize: "0.75rem", opacity: 0.7 }}>
197+
{(displayProb * 100).toFixed(0)}%
198+
</span>
199+
</div>
200+
);
201+
})()}
181202
</div>
182203
);
183204
}

app/intake/preparation/page.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,12 @@ export default function PreparationPage() {
410410
</div>
411411
)}
412412

413-
{/* 8-dot frame counter (matches REQUIRED_CONSECUTIVE = 8) */}
413+
{/* 12-dot frame counter (matches REQUIRED_CONSECUTIVE = 12) */}
414414
{actionPhase === "detecting" && (
415-
<div style={{ display: "flex", gap: 4, justifyContent: "center", marginBottom: 8 }}>
416-
{Array.from({ length: 8 }, (_, i) => (
415+
<div style={{ display: "flex", gap: 3, justifyContent: "center", marginBottom: 8 }}>
416+
{Array.from({ length: 12 }, (_, i) => (
417417
<div key={i} style={{
418-
width: 16, height: 16, borderRadius: "50%",
418+
width: 14, height: 14, borderRadius: "50%",
419419
background: i < consecutiveHits ? "var(--sage-500)" : "var(--bg-elevated)",
420420
border: "2px solid var(--sage-300)",
421421
transition: "background 0.2s",
@@ -429,12 +429,12 @@ export default function PreparationPage() {
429429
<p style={{
430430
fontSize: "0.9rem", fontWeight: 700, marginBottom: 8,
431431
color: !hasKeypoints ? "var(--text-muted)"
432-
: consecutiveHits >= 3 ? "var(--sage-600)"
432+
: consecutiveHits >= 5 ? "var(--sage-600)"
433433
: (actionResult?.confidence || 0) > 0 ? "var(--sky-400)"
434434
: "var(--text-muted)",
435435
}}>
436436
{!hasKeypoints ? "Step into view so we can see you!"
437-
: consecutiveHits >= 5 ? "Almost there! Keep holding..."
437+
: consecutiveHits >= 7 ? "Almost there! Keep holding..."
438438
: (actionResult?.confidence || 0) > 0.3 ? "Getting closer!"
439439
: `Looking for: ${meta.label}...`}
440440
</p>

app/lib/actions/actionDetector.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function detectClap(
128128
if (!confOk(conf, L_WRIST, R_WRIST))
129129
return { hit: false, proximity: 0 };
130130
const d = dist(kp(kps, L_WRIST), kp(kps, R_WRIST));
131-
const threshold = 0.4 * scale; // Relaxed: hands near each other (YOLO wrist keypoints are imprecise during clap)
131+
const threshold = 0.3 * scale; // Tightened: require hands closer together
132132
return { hit: d < threshold, proximity: Math.max(0, 1 - d / threshold) };
133133
}
134134

@@ -156,7 +156,7 @@ function detectTouchHead(
156156
const dL = confOk(conf, L_WRIST) ? dist(kp(kps, L_WRIST), nose) : Infinity;
157157
const dR = confOk(conf, R_WRIST) ? dist(kp(kps, R_WRIST), nose) : Infinity;
158158
const minD = Math.min(dL, dR);
159-
const threshold = 0.3 * scale;
159+
const threshold = 0.25 * scale;
160160
return { hit: minD < threshold, proximity: Math.max(0, 1 - minD / threshold) };
161161
}
162162

@@ -229,7 +229,7 @@ export function detectAction(
229229

230230
// ── Sustained detection tracker ─────────────────────────────────────
231231

232-
const REQUIRED_CONSECUTIVE = 8;
232+
const REQUIRED_CONSECUTIVE = 12;
233233

234234
export class ActionTracker {
235235
private consecutiveHits = 0;
@@ -253,10 +253,10 @@ export class ActionTracker {
253253

254254
const result = detectAction(keypoints, confidence, action, this.history);
255255

256-
if (result.detected) {
256+
if (result.detected && result.confidence > 0.4) {
257257
this.consecutiveHits++;
258258
} else {
259-
this.consecutiveHits = Math.max(0, this.consecutiveHits - 1);
259+
this.consecutiveHits = Math.max(0, this.consecutiveHits - 2);
260260
}
261261

262262
if (this.consecutiveHits >= REQUIRED_CONSECUTIVE) {

0 commit comments

Comments
 (0)