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> {
let cls = &update.classification;
if !cls.presence {
return vec![];
}
// Use estimated_persons if set by the tick loop; otherwise default to 1.
let person_count = update.estimated_persons.unwrap_or(1).max(1);
(0..person_count)
.map(|idx| derive_single_person_pose(update, idx, person_count))
.collect()
fn derive_pose_from_sensing(_update: &SensingUpdate) -> Vec<PersonDetection> {
// ADR-105: heuristic 17-keypoint synthesis disabled. It produced a
// believable-looking skeleton whose joint positions were geometric
// 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
// DensePose model is actually loaded and populates `update.persons`.
// All call sites still compile but get an empty vector when there
// is no model.
Vec::new()
}
// ── 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> {
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!({
"version": env!("CARGO_PKG_VERSION"),
"environment": "production",
@ -3631,7 +3633,7 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
"source": s.effective_source(),
"features": {
"wifi_sensing": true,
"pose_estimation": true,
"pose_estimation": pose_loaded,
"signal_processing": true,
"ruvector": 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> {
let s = state.read().await;
let persons = match &s.latest_update {
Some(update) => update.persons.clone().unwrap_or_else(|| derive_pose_from_sensing(update)),
None => vec![],
// ADR-105: only return persons when a trained pose model is loaded.
// Without a model we used to synthesise placeholder 17-keypoint
// 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!({
"timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
"persons": persons,
"total_persons": persons.len(),
"source": s.effective_source(),
"model_loaded": s.model_loaded,
}))
}
async fn pose_stats(State(state): State<SharedState>) -> Json<serde_json::Value> {
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!({
"total_detections": s.total_detections,
"average_confidence": 0.87,
"frames_processed": s.tick,
"source": s.effective_source(),
"model_loaded": s.model_loaded,
}))
}
async fn pose_zones_summary(State(state): State<SharedState>) -> Json<serde_json::Value> {
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()
.map(|u| u.classification.presence).unwrap_or(false);
Json(serde_json::json!({
"zones": {
"zone_1": { "person_count": if presence { 1 } else { 0 }, "status": "monitored" },
"zone_2": { "person_count": 0, "status": "clear" },
"zone_3": { "person_count": 0, "status": "clear" },
"zone_4": { "person_count": 0, "status": "clear" },
}
"presence": presence,
"zones_configured": 0,
"zones": {},
}))
}