wifi-densepose/docs/adr/ADR-151-room-calibration-sp...

29 KiB
Raw Blame History

ADR-151: RuView Per-Room Calibration & Specialized Model Training System

Field Value
Status Accepted — Stages 15 implemented (statistical specialists); HF-backbone distillation pending
Date 2026-06-09
Deciders ruv
Codebase target New wifi-densepose-calibration crate (orchestration); wifi-densepose-train (rapid_adapt.rs, signal_features.rs, trainer.rs); wifi-densepose-ruvector (RVF specialist storage); wifi-densepose-signal/ruvsense/* (feature extractors); wifi-densepose-cli (enroll, train-room, room-status subcommands)
Relates to ADR-135 (Empty-Room Baseline Calibration), ADR-030 (Persistent Field Model), ADR-134 (CIR), ADR-024 (Contrastive CSI Embedding / AETHER), ADR-027 (Cross-Environment Domain Generalization / MERIDIAN), ADR-070 (Self-Supervised Pretraining), ADR-105 (Federated CSI Training), ADR-149 (AetherArena / Hugging Face), ADR-150 (RF Foundation Encoder)

1. Context

1.1 The thesis — teach the room before you teach the model

RuView's deployment frontier is not a better generic model. ADR-150 documents the wall directly: an MM-Fi pose head scores 81.63% torso-PCK@20 in-domain but ~11.6% leakage-free cross-subject, and bigger capacity hurts cross-subject (transformer 24.8% < conv 27.3%). A single oversized model that "understands the world" overfits the rooms and bodies it has seen. The lever is the opposite of scale: a small model that understands one room and one person, calibrated in minutes, run locally, and specialised per biological signal.

This positions RuView between the two incumbents in ambient sensing:

  • Wearables — high fidelity, but people forget to wear them, and they only measure the wearer.
  • Cameras — powerful, but invasive, store identifiable video, and fail in the dark / under covers.

RuView sits in the middle: it learns the space, learns the person, and tracks biological rhythm (breathing, heartbeat, restlessness, posture, presence) without seeing skin or storing video. Heartbeat and breathing are not visual problems — they are tiny, repeating disturbances in the RF field. Capturing them well is a calibration problem, not a model-size problem.

1.2 What already exists (and what is missing)

The pieces of a calibration→training pipeline exist as disconnected modules. There is no system that runs them end to end and emits a per-room model bank.

Capability Status today Gap
Empty-room baseline (environmental fingerprint) ADR-135 BaselineCalibration (Proposed): per-subcarrier amplitude + circular-phase stats, ruvcal NVS namespace Captures the room, but there is no step that captures guided human anchors on top of it
Field eigenstructure ADR-030 field_model.rs (SVD room eigenmodes) Consumes calibration; not wired to a training trigger
Shared invariant backbone ADR-150 RF Foundation Encoder (pose-preserving, subject/room/device-invariant) Defined as a foundation embedding; nothing distills it into per-room specialists
Few-shot adaptation train/src/rapid_adapt.rs — test-time training → LoRA weight deltas (MERIDIAN P5) Produces a single pose-adaptation delta, not a bank of per-modality specialists
Feature extractors ruvsense/{bvp,longitudinal,intention,gesture,pose_tracker,adversarial}.rs, train/src/signal_features.rs Each emits a signal; none is packaged as a labelled training source for enrollment
Small-model storage wifi-densepose-ruvector (RVF cognitive containers, HNSW, sketch) No schema for "a bank of specialist models scoped to a room_id"
HF publishing ADR-149 AetherArena (Hugging Face Space + signed scorer), sensing-server from_pretrained path Publishes/評価s a global model; no notion of a published base + private local heads

The missing system is the connective tissue: a guided enrollment protocol, a feature-extraction-to-label bridge, a specialist-bank trainer that reuses the frozen HF backbone, and a runtime that fuses the specialists with confidence gating. This ADR defines that system.

1.3 The four-step user model (and where each step lands)

The system is deliberately presented to operators as four plain steps. Each maps to existing or new code:

  1. Capture a quiet baseline — no people, just room/router/reflections/noise/drift → the environmental fingerprint. → Reuse ADR-135 BaselineCalibration + ADR-030 field eigenmodes. No new capture code; the calibration crate calls it.
  2. Capture guided samples — stand, sit, lie down, slow vs normal breathing, small movement, sleep posture. Clean anchors, not hours of data. → NEW EnrollmentProtocol (Section 2.2).
  3. Extract the useful signal — CSI phase, amplitude, Doppler shift, micro-motion, periodicity, variance, timing. → Reuse signal_features.rs + ruvsense extractors, packaged as labelled AnchorFeature records (Section 2.3).
  4. Compress patterns into small ruVector modelsspecialised per signal: breathing, heartbeat, sleep restlessness, posture, presence, anomaly. → NEW SpecialistBank trained via rapid_adapt LoRA heads over the frozen ADR-150 backbone, stored as RVF (Section 2.4).

2. Decision

Build the RuView Per-Room Calibration & Specialized Model Training System: a four-stage, local-first pipeline (baseline → enroll → extract → train) that produces a versioned bank of small specialised ruVector models scoped to one room_id, each a lightweight head distilled/adapted from the frozen, Hugging-Face-published RF Foundation Encoder (ADR-150). Big model understands the world; small ruVector models understand your room.

Two invariants govern every design choice below:

(A) Specialisation over scale. One small model per biological signal, not one large model for all of them. Each specialist is faster, cheaper, more private, and — because it is calibrated to the room's actual fingerprint — often more accurate than a general model.

(B) Local-first, base-shared. The frozen room/subject/device-invariant backbone is the only artifact published to Hugging Face. Per-room baselines and per-specialist heads never leave the device unless the operator opts into federation (ADR-105).

2.1 System architecture

                       HUGGING FACE HUB (public, room-agnostic)
                       ┌───────────────────────────────────────┐
                       │  RF Foundation Encoder (ADR-150)       │
                       │  pose-preserving · subject/room/device │
                       │  -invariant · frozen · safetensors     │
                       └───────────────┬───────────────────────┘
                                       │  from_pretrained() once, cached on device
                                       ▼
  STAGE 1 baseline        STAGE 2 enroll        STAGE 3 extract         STAGE 4 train (per room_id)
  ┌──────────────┐        ┌──────────────┐      ┌────────────────┐      ┌─────────────────────────┐
  │ ADR-135      │        │ Enrollment   │      │ signal_features│      │ SpecialistBank          │
  │ Baseline-    │──fp──► │ Protocol     │─clip►│ + ruvsense     │─AF──►│  frozen backbone        │
  │ Calibration  │        │ guided       │      │ extractors     │      │   │  ┌────────────────┐  │
  │ (env finger- │        │ anchors:     │      │ → AnchorFeature│      │   ├─►│ breathing head │  │
  │  print)      │        │ stand/sit/   │      │ (phase, amp,   │      │   ├─►│ heartbeat head │  │
  │ ADR-030      │        │ lie/breathe/ │      │  doppler,      │      │   ├─►│ restless head  │  │
  │ field eigen  │        │ move/sleep   │      │  micromotion,  │      │   ├─►│ posture head   │  │
  └──────────────┘        └──────────────┘      │  periodicity,  │      │   ├─►│ presence head  │  │
        │                                        │  variance,     │      │   └─►│ anomaly head   │  │
        │  baseline drift > τ → invalidate bank  │  timing)       │      │     (LoRA / ruVector    │
        └───────────────────────────────────────┴────────────────┴──────┤      small models)      │
                                                                          └───────────┬─────────────┘
                                                                                      │ RVF container
                                                                                      ▼
                                                              RUNTIME: Mixture-of-Specialists
                                                              each head emits {value, confidence};
                                                              coherence_gate (ADR-135) + anomaly
                                                              head veto → fused RoomState

The shared backbone is loaded once per device and frozen. Every specialist is a small head over its embedding — so the marginal cost of a sixth specialist is kilobytes of LoRA weights, not another full model.

2.2 Stage 2 — the guided enrollment protocol (NEW)

EnrollmentProtocol is a CLI-driven state machine that walks the operator through a fixed sequence of labelled anchors. The design rule from the user vision is explicit: clean anchors, not hours of data. Each anchor is a short (default 20 s @ 20 Hz = 400 frames) labelled clip captured against the already-recorded baseline.

Anchor Label Duration Primary signal taught Feature emphasis
empty presence=0 (reuse ADR-135 baseline) absence reference amplitude variance floor
stand_still posture=standing, presence=1 20 s static human load amplitude mean shift, eigenmode delta
sit posture=sitting 20 s lower static load amplitude profile
lie_down posture=lying 20 s sleep-position load amplitude profile, low Doppler
breathe_slow resp≈0.10.15 Hz 30 s slow respiration periodicity, micro-Doppler
breathe_normal resp≈0.20.3 Hz 30 s normal respiration periodicity, BVP phase
small_move motion=1 20 s limb micro-motion Doppler spread, variance
sleep_posture posture=lying, restless=0 30 s quiescent sleep baseline long-window variance, timing

The protocol is adaptive: an anchor is only accepted when its captured features pass a quality gate (coherence ≥ threshold from coherence_gate.rs, sufficient SNR vs baseline, no saturation). A failed anchor is re-prompted rather than silently kept — bad anchors poison small models far more than large ones. Total guided enrollment is ~4 minutes of wall-clock, producing 8 clean anchors. This is intentionally far below the "hours of data" that a from-scratch model needs, because the backbone already carries world knowledge; enrollment only teaches this room's offsets.

Anchors are persisted as an append-only EnrollmentSession (event-sourced, per CLAUDE.md state rules) under room_id, so re-enrollment is incremental and auditable.

2.3 Stage 3 — feature extraction to labelled records (REUSE + bridge)

Each accepted anchor clip is run through the existing extractor stack, baseline-subtracted per ADR-135, and packaged into an AnchorFeature record. No new DSP is invented — this stage is a bridge, not a new algorithm.

Feature group Source module Used by specialists
CSI amplitude mean/variance ADR-135 baseline subtraction + signal_features.rs presence, posture
CSI phase (sanitised, LO-aligned) phase_sanitizerphase_align posture, heartbeat
Doppler shift / micro-Doppler ruvsense/bvp.rs, breathing path breathing, small-move
Micro-motion / intention lead ruvsense/intention.rs restlessness, anomaly
Periodicity / spectral peaks bvp.rs autocorrelation + FFT breathing, heartbeat
Long-window variance / drift ruvsense/longitudinal.rs (Welford) restlessness, presence
Timing / inter-frame epoch c6_timesync epoch, frame Δt all (rhythm alignment)
Field eigenmode coefficients ADR-030 field_model.rs posture, presence

AnchorFeature = { room_id, anchor_label, t_epoch_us, embedding: [f32; D] (backbone output), aux: { resp_hz?, doppler_spread, variance, periodicity_score, eigen_coeffs } }. The backbone embedding is the shared representation; aux carries the cheap hand-features that let small heads specialise without re-learning DSP.

2.4 Stage 4 — the specialist bank (NEW, the core contribution)

A SpecialistBank is a versioned collection of small models scoped to one room_id, persisted as a single RVF cognitive container (wifi-densepose-ruvector). Each specialist is a head over the frozen backbone embedding, trained from the labelled AnchorFeature records via the existing rapid_adapt.rs LoRA machinery (test-time/few-shot training, contrastive + entropy losses), not a from-scratch network.

Specialist Model type Params (typ.) Label source Output
breathing 1-D temporal head + periodicity regressor ~8 KB LoRA + aux breathe_slow/breathe_normal resp rate (Hz) + confidence
heartbeat narrowband phase head (harmonic-aware) ~12 KB quiescent anchors + periodicity HR (bpm) + confidence
sleep restlessness variance/drift classifier ~4 KB sleep_posture vs small_move restlessness score [0,1]
posture k-way prototype classifier (HNSW NN) prototypes only stand/sit/lie anchors posture class + margin
presence binary energy/eigenmode gate ~2 KB empty vs occupied anchors presence prob
anomaly one-class / physically-impossible detector (adversarial.rs) ~6 KB baseline + all anchors (novelty) anomaly score + veto flag

Design properties that follow from invariant (A):

  • Independently versioned & swappable. Re-enrolling breathing does not retrain posture. A specialist carries its own {trained_at, anchor_set_hash, baseline_hash, backbone_rev}.
  • HNSW prototype storage for the classifiers. Posture and presence are nearest-prototype lookups in the RVF index — no inference engine, microsecond latency, and new postures are added by inserting a prototype, not retraining.
  • SONA online adaptation. Each specialist may carry a SONA/MicroLoRA online-adaptation slot (ruvllm_sona_* / microlora primitives) so it tracks slow drift (furniture moved, seasonal RF change) between full re-enrollments, gated by ADR-135 baseline drift.
  • Teacherstudent distillation (optional, offline). Where a labelled public corpus exists (MM-Fi, Wi-Pose), the ADR-150 backbone acts as teacher to pre-shape a head before per-room fine-tuning, improving cold-start. The teacher is global/HF; the student head is local.

Invalidation contract. The bank stores the baseline_id (the baseline UUID) it was trained against. As implemented, the runtime marks the bank STALE whenever the current baseline id differs from the trained one — a conservative trigger that catches re-calibration (room rearranged, AP moved, band changed) because any of those produces a new baseline. A finer drift-threshold trigger (mark STALE when ADR-135's per-subcarrier deviation exceeds τ without a full re-baseline) is a planned refinement (P6). Either way the runtime prompts re-enrollment rather than emitting silently wrong vitals — the calibration analogue of the #954 DEGRADED honesty rule: never report confident numbers from an invalid model.

2.5 Runtime — mixture of specialists with confidence gating

At inference, the frozen backbone embeds each CSI window once; every specialist consumes that shared embedding and emits {value, confidence}. Fusion rules:

  • The anomaly specialist holds a veto: a high anomaly score (physically-impossible signal per adversarial.rs, or a coherence-gate Reject) suppresses positive vitals/posture output and raises a flag, rather than propagating a hallucinated reading.
  • presence=0 short-circuits breathing/heartbeat/posture to null (you cannot have a respiration rate in an empty room).
  • Each emitted reading is tagged with the specialist's confidence and the baseline_hash/backbone_rev provenance, so downstream consumers (sensing-server, MQTT, Home Assistant) can gate on quality — consistent with ADR-135 coherence-gate semantics.

2.6 Crate & module layout

New bounded-context crate wifi-densepose-calibration (orchestration only; files < 500 lines, typed public APIs, event-sourced sessions — per CLAUDE.md):

wifi-densepose-calibration/
  src/
    lib.rs                 # public API: CalibrationSystem facade
    enrollment.rs          # EnrollmentProtocol state machine (Stage 2)
    anchor.rs              # Anchor, EnrollmentSession (event-sourced)
    extract.rs             # AnchorFeature bridge over signal_features + ruvsense (Stage 3)
    specialist.rs          # Specialist trait, SpecialistKind enum
    bank.rs                # SpecialistBank (RVF container, versioning, invalidation)
    runtime.rs             # MixtureOfSpecialists fusion + veto (Stage 5)
    backbone.rs            # frozen ADR-150 encoder loader (hf_hub from_pretrained, cached)
    error.rs

Dependencies (no duplication — orchestrates existing crates): wifi-densepose-signal (ruvsense extractors, ADR-135 baseline), wifi-densepose-train (rapid_adapt, signal_features, trainer), wifi-densepose-ruvector (RVF, HNSW), wifi-densepose-nn (backbone inference). The wifi-densepose-cli gains enroll, train-room, and room-status subcommands, sequenced after the existing ADR-135 calibrate.

2.7 CLI flow (operator-facing)

# Stage 1 — environmental fingerprint (ADR-135, existing)
wifi-densepose calibrate --room living-room --duration 60s     # empty room

# Stage 2+3 — guided enrollment (NEW); prompts through 8 anchors, ~4 min
wifi-densepose enroll --room living-room
#   → "Stand still in view of the sensor…"  [✓ anchor accepted: coherence 0.91]
#   → "Sit down…"                            [✗ low SNR, retrying]
#   ...

# Stage 4 — train the specialist bank (NEW); reuses cached HF backbone
wifi-densepose train-room --room living-room \
    --specialists breathing,heartbeat,restlessness,posture,presence,anomaly

# Status / invalidation
wifi-densepose room-status --room living-room
#   baseline: fresh (drift 0.04 < 0.20) · backbone: rf-foundation@1.2.0
#   breathing  ✓ trained 2026-06-09  conf p50 0.88
#   heartbeat  ✓ trained 2026-06-09  conf p50 0.71
#   posture    ✓ 3 prototypes (stand/sit/lie)
#   anomaly    ✓  · presence ✓  · restlessness ✓

3. Consequences

3.1 Positive

  • Fidelity through specialisation. Six small calibrated heads beat one oversized general model on the cross-room/cross-subject frontier that ADR-150 quantified — and each runs in microseconds-to-milliseconds, on-device.
  • Privacy by construction. Only the room-agnostic backbone is public (HF). The environmental fingerprint and the person-specific heads stay local; no video, no skin, no cloud round-trip. This is the core differentiator vs cameras and the convenience differentiator vs wearables.
  • Minutes, not hours. Because the backbone carries world knowledge, ~4 minutes of clean anchors calibrates a room. Re-enrollment is incremental.
  • Honest degradation. The baseline_hash invalidation + anomaly veto mean an out-of-calibration room reports STALE/flagged rather than confidently wrong — the same honesty principle as the firmware DEGRADED flag.
  • Composable & cheap to extend. A new biological signal = a new small head over the same embedding, not a new model.

3.2 Negative / risks

  • Backbone dependency. Every specialist rides on ADR-150's encoder; its quality and revision compatibility (backbone_rev) are a single point of leverage. Mitigation: pin backbone_rev in each specialist; distillation cold-start reduces sensitivity.
  • Enrollment burden. 4 minutes is small but non-zero, and anchor quality depends on the operator following prompts. Mitigation: adaptive re-prompting + quality gates; ship sane defaults so a partial bank (presence+posture) works after just the static anchors.
  • Heartbeat is hard. Sub-mm chest displacement at HR frequencies is near the ESP32-S3 noise floor; the heartbeat specialist will have lower and more variable confidence than breathing. The confidence-gated runtime surfaces this rather than faking it.
  • Per-room storage proliferation. A bank per room per person; needs a clear RVF lifecycle (list/prune/export) — handled by bank.rs versioning and the room-status CLI.

3.3 Alternatives considered

Alternative Verdict Reason
One large general model for all signals Rejected The ADR-150 evidence: scale overfits rooms/subjects and collapses cross-domain; also slower, costlier, less private. Directly contradicts invariant (A).
Cloud training of per-room models Rejected Violates invariant (B): would ship raw CSI of a person's home/sleep to a server. Local-first is the privacy promise. Federation (ADR-105) is the opt-in path for shared improvement, exchanging gradients/deltas, never raw CSI.
Skip the backbone; train each specialist from scratch Rejected Reintroduces the "hours of data" requirement the user vision explicitly rejects, and loses cross-room priors.
Fold this into ADR-135 Rejected ADR-135 is room calibration (no humans). This ADR is human-anchor enrollment + model training on top of it. Distinct lifecycles, distinct invalidation; kept as separate bounded contexts.

4. Implementation phases

Phase Scope Exit criterion Status
P1 Scaffold wifi-densepose-calibration crate; AnchorFeature schema; (backbone via hf_hub deferred) Crate + schema; unit tests Done (crate + Stage-1 baseline via calibrate/calibrate-serve; HF backbone deferred)
P2 EnrollmentProtocol + anchor.rs (event-sourced sessions) + CLI enroll with quality gates 8-anchor enrollment; bad anchors re-prompt Done (anchor.rs, enrollment.rs, CLI enroll)
P3 extract.rs bridge → labelled records; baseline subtraction (ADR-135) AnchorFeature records persisted per room_id Done (extract.rs; autocorr periodicity + variance/motion)
P4 SpecialistBank + presence/posture (prototype) + breathing (periodicity); persistence + versioning train-room produces a bank; room-status reads it back Done (specialist.rs, bank.rs, CLI train-room/room-status; JSON persistence — RVF/HNSW = future)
P5 heartbeat + restlessness + anomaly specialists; runtime.rs mixture + veto + confidence gating End-to-end RoomState on hardware; anomaly veto verified Done (runtime.rs, CLI room-watch; breathing read live on COM8 ESP32)
P6 Baseline-drift STALE invalidation; SONA online adaptation; optional ADR-105 federation; HF teacherstudent distillation Drift marks bank STALE; AetherArena entry ◐ Partial (STALE done; SONA/federation/HF-backbone = follow-ups)

Current status (2026-06-10): Stages 15 implemented with statistical specialists (threshold/prototype/autocorrelation). 55 tests (35 unit incl. multistatic + 1 full-loop integration + 19 CLI), all passing under qemu-aarch64. Validation scope is precise: baseline capture + HTTP API + auth are proven on real CSI (Pi-5 nexmon, 6,813 frames; and an ESP32-S3). The complete baseline → enroll → train-room → infer loop is now proven in-process on deterministic synthetic CSI (tests/full_loop.rs: clean baseline with zero motion flags, 8/8 anchors through the quality gate, 6 specialists trained, JSON bank round-trip, trained-bank inference 18±2 BPM positive / absent negative / foreign-baseline STALE; seed-robust). The one live runtime signal (breathing ~1631 BPM via room-watch) used the stateless breathing head, not a trained bank; the clean empty-room loop has not yet run on-target — the remaining gap is strictly the hardware session (empty room + operator anchors). The four behavioral findings from the full-loop test (z-band squeeze, variance-only presence, ungated hz embedding, heart-band lag-floor leakage) are FIXED and regression-guarded — see the integration doc §7. SOTA-intake decisions affecting this system (geometry conditioning, checkerboard alignment) are recorded in ADR-152. Open refinements: --source-format adr018v6 (drive from the Pi's own nexmon), phase-based breathing carrier, RVF/HNSW storage, and the ADR-150 frozen HF backbone the specialists would distill from.

Validation per CLAUDE.md: cargo test --workspace --no-default-features green; hardware verification on the ESP32-S3 (currently COM8) before any release; witness bundle regenerated if the proof surface changes.


6. Review notes

6.1 Correctness + security review (2026-06-14)

Beyond-SOTA correctness+security review of wifi-densepose-calibration (this ADR's pipeline), un-covered by the ADR-154159 sweep.

Finding (FIXED) — NaN-poisoning of the feature path (numerical / fail-closed). Features::from_series — the carrier for both live inference and training-anchor extraction — computed mean/variance/motion over the raw scalar series with no non-finite guard. A single NaN/±inf sample (corrupt CSI frame) yielded mean=NaN, variance=NaN and an all-NaN prototype embedding. Persisted into a PresenceSpecialist::threshold/empty_mean at train time, the NaN silently disabled presence detection for the bank's lifetime (every > / |·| comparison against NaN is false → always reads absent, confidence 0), with no error — and an asymmetry against the rigorously NaN-guarded geometry_embedding. Fixed at the production boundary: non-finite samples are dropped (a corrupt frame counts as no frame), an all-non-finite series degrades to Features::ZERO like the empty series. Value-identical for all-finite input (full-loop + extract tests unchanged); pinned by non_finite_samples_do_not_poison_features and all_non_finite_series_is_zero (both fail on the old code).

Clean dimensions (evidence, no invented issues).

  • File/path handling: the crate performs zero file/path I/O (no std::fs/Path/File/read/write in src/; only in-memory serde_json). Path-traversal / unbounded-read / artifact-path handling live entirely in the wifi-densepose-cli consumer (room.rs), outside this crate's boundary.
  • Untrusted-load: SpecialistBank::from_json shape-validates via serde (malformed → CalibrationError::Serde); banks are local-first (invariant B), never network-received. A well-formed bank with adversarial numerics is trusted as-is — acceptable under the local-first threat model; a validate-on-load defense-in-depth pass is a possible future hardening, not a present bug.
  • Receipt/hash integrity: the crate emits no hash/receipt/witness/signature, so the unframed-concatenation bug class (cf. the engine witness_of fix) is structurally absent.
  • Other numerical paths: geometry_embedding sanitizes every input and sweeps to finite; presence/restlessness/anomaly divisions are .max(1e-3)-guarded; autocorr_dominant guards r0, short signals, and empty bands; train rejects empty anchors; anomaly requires ≥2 anchors.

De-magicked the bare specialist threshold literals (breathing/heartbeat default min-scores, anomaly outlier-spread multiple + label cutoff) into named documented consts, value-identical, pinned by const-equality tests. Tests 58→62 unit + 1 integration, 0 failed; Python deterministic proof unchanged (off the signal proof path).


5. Summary

Big models understand the world. Small ruVector models understand your room.

ADR-151 makes that operational: a local-first baseline → enroll → extract → train pipeline that turns ~4 minutes of clean human anchors — layered on ADR-135's empty-room fingerprint and ADR-150's Hugging-Face-published invariant backbone — into a versioned bank of tiny, specialised, privacy-preserving models for breathing, heartbeat, restlessness, posture, presence, and anomaly. Specialisation over scale; local heads over a shared base; honest STALE degradation over confident error.