From 98bf8c4726d05e37c1e09ed35e68ccf06ff0b25b Mon Sep 17 00:00:00 2001 From: ruv Date: Thu, 11 Jun 2026 23:10:00 -0400 Subject: [PATCH] fix(cog-pose-estimation): emit frames under default config (ADR-159 A1) pose_v1 has no confidence head, so infer() emits a constant 0.185 per frame. The config default_min_confidence was 0.3 and the runtime gates on confidence >= min_confidence, so a default install silently emitted ZERO pose.frame events while health reported healthy. - Add inference::MODEL_TYPICAL_CONFIDENCE (0.185, the validation PCK@50) as the single published per-frame confidence. - Pin default_min_confidence() to MODEL_TYPICAL_CONFIDENCE so a default install clears its own gate and emits. - Warn at run.started when min_confidence exceeds the model typical confidence (disclosed, not silent); document the trade-off in the config field, the JSON schema, and inference.rs. Failing-on-old test: default_config_emits_frames_with_real_model (with old 0.3 it panics: "default install would emit zero pose.frame events"). Co-Authored-By: claude-flow --- .../cog/config.schema.json | 4 +- v2/crates/cog-pose-estimation/src/config.rs | 11 +++- .../cog-pose-estimation/src/inference.rs | 21 ++++++-- v2/crates/cog-pose-estimation/src/main.rs | 12 +++++ v2/crates/cog-pose-estimation/tests/smoke.rs | 53 +++++++++++++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/v2/crates/cog-pose-estimation/cog/config.schema.json b/v2/crates/cog-pose-estimation/cog/config.schema.json index 023ebaec..a5ebf85b 100644 --- a/v2/crates/cog-pose-estimation/cog/config.schema.json +++ b/v2/crates/cog-pose-estimation/cog/config.schema.json @@ -26,8 +26,8 @@ "type": "number", "minimum": 0, "maximum": 1, - "default": 0.3, - "description": "Drop frames where the inferred pose confidence is below this threshold." + "default": 0.185, + "description": "Drop frames where the inferred pose confidence is below this threshold. pose_v1 has no confidence head, so every frame carries the model's published per-frame confidence (0.185 = validation PCK@50); the default is pinned to that value so a default install actually emits frames. Raising it above 0.185 suppresses ALL pose.frame events (the runtime warns when this happens)." } }, "required": ["model_path"] diff --git a/v2/crates/cog-pose-estimation/src/config.rs b/v2/crates/cog-pose-estimation/src/config.rs index d5976f9c..605cc5ae 100644 --- a/v2/crates/cog-pose-estimation/src/config.rs +++ b/v2/crates/cog-pose-estimation/src/config.rs @@ -23,6 +23,13 @@ pub struct CogConfig { pub poll_ms: u64, /// Confidence threshold below which a frame's keypoints are not emitted. + /// + /// Defaults to [`crate::inference::MODEL_TYPICAL_CONFIDENCE`] (0.185) — the + /// model's published per-frame confidence. `pose_v1` has no confidence head, + /// so every frame carries this same value; a default above it would silently + /// suppress *all* `pose.frame` events while health still reports healthy. + /// The runtime warns at `run.started` if this is raised above the model's + /// typical confidence rather than dropping frames quietly. #[serde(default = "default_min_confidence")] pub min_confidence: f32, } @@ -36,7 +43,9 @@ fn default_poll_ms() -> u64 { } fn default_min_confidence() -> f32 { - 0.3 + // Pinned to the model's typical/published confidence so a default install + // actually emits frames. See `min_confidence` doc and ADR-159 §A1. + crate::inference::MODEL_TYPICAL_CONFIDENCE } impl CogConfig { diff --git a/v2/crates/cog-pose-estimation/src/inference.rs b/v2/crates/cog-pose-estimation/src/inference.rs index fc675e2c..314d75e5 100644 --- a/v2/crates/cog-pose-estimation/src/inference.rs +++ b/v2/crates/cog-pose-estimation/src/inference.rs @@ -27,6 +27,16 @@ pub const INPUT_SUBCARRIERS: usize = 56; pub const INPUT_TIMESTEPS: usize = 20; pub const OUTPUT_KEYPOINTS: usize = 17; +/// The model's typical self-reported confidence. `pose_v1` has **no confidence +/// head** (the head emits 34 keypoint coordinates only), so per-frame confidence +/// is not available from the network. This is the validation-set PCK@50 (18.5%) +/// the training run reported, used as the published per-frame confidence floor. +/// +/// Surfaced as a public constant so the runtime can warn when a configured +/// `min_confidence` threshold exceeds it — otherwise a default install would +/// silently emit zero `pose.frame` events while health reports healthy. +pub const MODEL_TYPICAL_CONFIDENCE: f32 = 0.185; + #[derive(Debug, Clone)] pub struct CsiWindow { pub data: Vec, // length INPUT_SUBCARRIERS * INPUT_TIMESTEPS @@ -283,12 +293,15 @@ impl InferenceEngine { let out = model.net.forward(&t)?; // [1, 34] let flat: Vec = out.flatten_all()?.to_vec1()?; // Confidence from pose_v1 is a published constant rather than per-frame — - // the trained model didn't emit a confidence head. Use the validation-set - // PCK@50 (18.5%) as the published self-reported confidence so downstream - // consumers can gate display decisions on it. + // the trained model has no confidence head (the head emits 34 keypoint + // coordinates only), so a real per-frame value is genuinely unavailable. + // We surface the validation-set PCK@50 (`MODEL_TYPICAL_CONFIDENCE`) as the + // honest self-reported confidence. The runtime's `min_confidence` default + // is pinned at or below this so a default install actually emits frames + // (and warns if an operator raises the threshold above the model's reach). Ok(PoseOutput { keypoints: flat, - confidence: 0.185, + confidence: MODEL_TYPICAL_CONFIDENCE, }) } } diff --git a/v2/crates/cog-pose-estimation/src/main.rs b/v2/crates/cog-pose-estimation/src/main.rs index 99e12b85..669eedfd 100644 --- a/v2/crates/cog-pose-estimation/src/main.rs +++ b/v2/crates/cog-pose-estimation/src/main.rs @@ -113,6 +113,18 @@ fn cmd_run( let cfg = CogConfig::load(&config_path)?; emit_event(&Event::run_started(COG_ID, &cfg)); + // Disclosure: pose_v1 has no confidence head, so every frame carries the + // same `MODEL_TYPICAL_CONFIDENCE`. A `min_confidence` above that silently + // suppresses *all* pose.frame events. Warn loudly rather than drop quietly. + if cfg.min_confidence > cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE { + tracing::warn!( + min_confidence = cfg.min_confidence, + model_typical_confidence = cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE, + "configured min_confidence exceeds the model's typical confidence; \ + no pose.frame events will be emitted until this is lowered" + ); + } + let engine = InferenceEngine::with_adapter(adapter.as_deref())?; if engine.is_calibrated() { tracing::info!("per-room calibration adapter loaded"); diff --git a/v2/crates/cog-pose-estimation/tests/smoke.rs b/v2/crates/cog-pose-estimation/tests/smoke.rs index 395e51c3..c79a559c 100644 --- a/v2/crates/cog-pose-estimation/tests/smoke.rs +++ b/v2/crates/cog-pose-estimation/tests/smoke.rs @@ -172,3 +172,56 @@ fn manifest_roundtrips() { assert_eq!(back.id, "pose-estimation"); assert_eq!(back.version, "0.0.1"); } + +/// ADR-159 §A1 — the default-config min_confidence threshold must not silently +/// suppress every `pose.frame`. With the old `default_min_confidence()=0.3` and +/// the model's per-frame confidence pinned at 0.185, the runtime gate +/// (`out.confidence >= cfg.min_confidence`) never fired, so a default install +/// emitted ZERO frames while health reported healthy. This asserts the default +/// install actually clears its own gate. +#[test] +fn default_config_emits_frames_with_real_model() { + use cog_pose_estimation::config::CogConfig; + + // A minimal config (only the required model_path) exercises every + // `#[serde(default)]` path — i.e. the *default* install threshold. + let cfg: CogConfig = + serde_json::from_value(serde_json::json!({ "model_path": "pose_v1.safetensors" })) + .expect("default config parse"); + + // Real model when present; stub otherwise. Either way the per-frame + // confidence the runtime gates on must clear the default threshold, + // OR (stub case) the gate must still let the model's typical confidence + // through. We assert against the same value the runtime emits. + let weights = std::path::Path::new("cog/artifacts/pose_v1.safetensors"); + let engine = if weights.exists() { + InferenceEngine::with_weights(Some(weights)).expect("load real weights") + } else { + InferenceEngine::new().expect("engine init") + }; + + // Core regression assertion (fails on the old `default_min_confidence()=0.3`): + // the default threshold must not exceed the model's published per-frame + // confidence (0.185), which is the exact value `infer()` emits for the real + // model. With 0.3 the runtime gate `out.confidence >= min_confidence` never + // fired → zero pose.frame events on a default install. + assert!( + cfg.min_confidence <= cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE, + "default min_confidence {} exceeds model typical confidence {} — \ + a default install would emit zero pose.frame events", + cfg.min_confidence, + cog_pose_estimation::inference::MODEL_TYPICAL_CONFIDENCE + ); + + // End-to-end: when the real model is loaded, the value it actually emits + // must clear the default gate (i.e. the runtime would emit this frame). + if engine.backend().starts_with("candle-") { + let out = engine.infer(&SyntheticInput.as_window()).expect("infer"); + assert!( + out.confidence >= cfg.min_confidence, + "default install must emit: infer confidence {} < default min_confidence {}", + out.confidence, + cfg.min_confidence + ); + } +}