feat(adr-105): kill synthetic pose + hard-coded confidence — only real data

Operator inspected the rich Docker UI tied to our backend and noticed
the dashboard showed a 17-keypoint skeleton even with no DensePose
model loaded. Tracing it: `derive_pose_from_sensing` synthesized
geometric placeholders, `pose_stats.average_confidence` was hard-coded
0.87, `pose_zones_summary` invented zones 2/3/4 as "clear", and
`/api/v1/info.features.pose_estimation` claimed `true` regardless.
All cosmetic noise that hid the real capability gap.

Changes:
* `derive_pose_from_sensing` is now an inert `Vec::new()` stub.
  Heuristic logic kept in `derive_single_person_pose` (dead-code-warned
  out by the rustc unused-fn lint) for the day someone wires a real
  trained pose model in.
* `pose_current` returns persons only when `model_loaded == true`; the
  endpoint always includes `model_loaded` so the UI can decide what
  to render.
* `pose_stats` drops the fake `average_confidence: 0.87`.
* `pose_zones_summary` reports `zones_configured: 0` and an empty
  `zones {}` instead of fabricating four zones.
* `api_info.features.pose_estimation` now mirrors `s.model_loaded`.

Sensing endpoints (`/api/v1/sensing/latest`, `/ws/sensing`) are
unchanged — they always carried real ESP32-derived data per ADR-101.
This commit is contained in:
arsen 2026-05-17 11:28:36 +07:00
parent d28a1834d4
commit 9aa027e95e
1 changed files with 32 additions and 23 deletions

View File

@ -3457,18 +3457,16 @@ fn derive_single_person_pose(
} }
} }
fn derive_pose_from_sensing(update: &SensingUpdate) -> Vec<PersonDetection> { fn derive_pose_from_sensing(_update: &SensingUpdate) -> Vec<PersonDetection> {
let cls = &update.classification; // ADR-105: heuristic 17-keypoint synthesis disabled. It produced a
if !cls.presence { // believable-looking skeleton whose joint positions were geometric
return vec![]; // placeholders, not real pose estimation — confidence stayed at 0.0
} // and the body never moved with the operator. Operator asked for
// boots-on-the-ground honesty: only return persons when a trained
// Use estimated_persons if set by the tick loop; otherwise default to 1. // DensePose model is actually loaded and populates `update.persons`.
let person_count = update.estimated_persons.unwrap_or(1).max(1); // All call sites still compile but get an empty vector when there
// is no model.
(0..person_count) Vec::new()
.map(|idx| derive_single_person_pose(update, idx, person_count))
.collect()
} }
// ── RuVector Phase 2: Temporal EMA smoothing for keypoints ────────────────── // ── RuVector Phase 2: Temporal EMA smoothing for keypoints ──────────────────
@ -3624,6 +3622,10 @@ async fn health_metrics(State(state): State<SharedState>) -> Json<serde_json::Va
async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> { async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await; let s = state.read().await;
// ADR-105: features must reflect real capability — no DensePose model
// loaded ⇒ pose_estimation is `false`. Operator asked for honesty over
// marketing.
let pose_loaded = s.model_loaded;
Json(serde_json::json!({ Json(serde_json::json!({
"version": env!("CARGO_PKG_VERSION"), "version": env!("CARGO_PKG_VERSION"),
"environment": "production", "environment": "production",
@ -3631,7 +3633,7 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
"source": s.effective_source(), "source": s.effective_source(),
"features": { "features": {
"wifi_sensing": true, "wifi_sensing": true,
"pose_estimation": true, "pose_estimation": pose_loaded,
"signal_processing": true, "signal_processing": true,
"ruvector": true, "ruvector": true,
"streaming": true, "streaming": true,
@ -3641,39 +3643,46 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Value> { async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await; let s = state.read().await;
let persons = match &s.latest_update { // ADR-105: only return persons when a trained pose model is loaded.
Some(update) => update.persons.clone().unwrap_or_else(|| derive_pose_from_sensing(update)), // Without a model we used to synthesise placeholder 17-keypoint
None => vec![], // skeletons from `derive_pose_from_sensing` so the UI looked alive;
// that's a lie about capability. Empty array now if no model.
let persons = if s.model_loaded {
s.latest_update.as_ref().and_then(|u| u.persons.clone()).unwrap_or_default()
} else {
Vec::new()
}; };
Json(serde_json::json!({ Json(serde_json::json!({
"timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0, "timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
"persons": persons, "persons": persons,
"total_persons": persons.len(), "total_persons": persons.len(),
"source": s.effective_source(), "source": s.effective_source(),
"model_loaded": s.model_loaded,
})) }))
} }
async fn pose_stats(State(state): State<SharedState>) -> Json<serde_json::Value> { async fn pose_stats(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await; let s = state.read().await;
// ADR-105: drop the hard-coded `average_confidence: 0.87`. Report
// only counters that come from real frame ingest.
Json(serde_json::json!({ Json(serde_json::json!({
"total_detections": s.total_detections, "total_detections": s.total_detections,
"average_confidence": 0.87,
"frames_processed": s.tick, "frames_processed": s.tick,
"source": s.effective_source(), "source": s.effective_source(),
"model_loaded": s.model_loaded,
})) }))
} }
async fn pose_zones_summary(State(state): State<SharedState>) -> Json<serde_json::Value> { async fn pose_zones_summary(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await; let s = state.read().await;
// ADR-105: drop synthetic "zone_2/3/4 clear" entries — the operator
// never configured any zones. Report only what we actually know.
let presence = s.latest_update.as_ref() let presence = s.latest_update.as_ref()
.map(|u| u.classification.presence).unwrap_or(false); .map(|u| u.classification.presence).unwrap_or(false);
Json(serde_json::json!({ Json(serde_json::json!({
"zones": { "presence": presence,
"zone_1": { "person_count": if presence { 1 } else { 0 }, "status": "monitored" }, "zones_configured": 0,
"zone_2": { "person_count": 0, "status": "clear" }, "zones": {},
"zone_3": { "person_count": 0, "status": "clear" },
"zone_4": { "person_count": 0, "status": "clear" },
}
})) }))
} }