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 <ruv@ruv.net>
This commit is contained in:
ruv 2026-06-11 23:10:00 -04:00
parent 2e4461d64d
commit 98bf8c4726
5 changed files with 94 additions and 7 deletions

View File

@ -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"]

View File

@ -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 {

View File

@ -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<f32>, // length INPUT_SUBCARRIERS * INPUT_TIMESTEPS
@ -283,12 +293,15 @@ impl InferenceEngine {
let out = model.net.forward(&t)?; // [1, 34]
let flat: Vec<f32> = 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,
})
}
}

View File

@ -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");

View File

@ -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
);
}
}