Unified pose + track sidecar schema (PTZ · stills · heli) — v0.1 (Stac Camera Telemetry)

**Artifact** from Bead: Stac Camera Telemetry · [canonical source](https://redfish.acequia.io/guerin/.agents/31f69717-3c68-4a0d-85d3-9515ec2e91ff/2026-06-14/artifacts/unified-pose-sidecar-schema.md) · session 2026-06-14 · discussion: Talk: Stac Camera Telemetry

Targets the four gaps the incident-viewer flagged (2026-06-14T204609): per-point **heading**, **`view:`** fields, **provenance as a top-level property**, **clock offset/overlap**. Backward compatible with both shapes the renderer already eats. Soft-constraint-ready per [notes/01](../notes/01-soft-constraints-least-action-pose-coherence.md) — nothing here is a hard pin.

## A. STAC Item additions (queryable, not buried in `forage:*`) ```jsonc "stac_extensions": [ "https://stac-extensions.github.io/view/v1.0.0/schema.json" ], "properties": { // TEMPORAL BOUNDING LINE — canonical, standard STAC. The viewer reads start/end → timeline range. "datetime": "2025-01-08T19:24:05Z", // representative instant (here: stream start) "start_datetime": "2025-01-08T19:24:05Z", // ← span START (video start) "end_datetime": "2025-01-08T23:58:58Z", // ← span END (track end). Drives "set timeline to extent". "pose:provenance": "recovered-prior", // ← TOP-LEVEL FACET: native-pose | recovered-prior | none "view:azimuth": 212, // item-level nominal heading (optional; per-point wins) "view:off_nadir": 90, // ~horizon-looking; tilt "pose:clock": { // ← clock provenance is SOFT (why instant != track) "source": "yt-dlp-publish", "item_datetime_is": "stream-start", // why datetime sits at 19:24, not the track "track_window": ["2025-01-08T20:37:12Z","2025-01-08T23:58:58Z"], // the ADS-B sub-span (telemetry coverage) "offset_s_estimate": null, "soft": true } } ``` Collection echoes the bound: `extent.temporal.interval = start_datetime, end_datetime`. **Authoring rule (don't regress the live catalog):** always emit `start_datetime`/`end_datetime` spanning **all** evidence (video + track), not just the video segment; `pose:clock.track_window` is the *narrower* telemetry coverage inside it. Seek-on-select uses `track_window[0]` so the marker shows immediately while the row still spans the full bound.

## B. Track sidecar (the `adsb_track` asset) — unified, both legacy shapes still valid Canonical v0.1 — per-point **heading is first-class**: ```jsonc { "schema": "unified-pose-track/0.1", "provenance": "recovered-prior", // mirrors pose:provenance "frame": "WGS84", "alt_ref": "msl", "window": ["2025-01-08T20:37:12Z","2025-01-08T23:58:58Z"], "units": { "t": "iso|sec|ms", "alt": "m|ft", "angles": "deg" }, "track": [ { "t": "2025-01-08T20:37:12Z", "lat": 34.107, "lon": -118.579, "alt": 2100, "heading": 212, // ship course; for true pose add gimbal below "pitch": -18, "roll": 0, "fov": 50, // OPTIONAL gimbal/camera pose (absent ⇒ position-only) "cov": null } // OPTIONAL per-point covariance (softness) ], "constraints": [] // FUTURE: GCP / UV-UV tie points injected by calibrators } ``` **Accepted legacy (renderer keeps working):** - baked `points: [[t_ms, lat, lon, alt_m, hdg], …]` - raw adsb `track: [{t_sec, lat, lon, alt_ft, track}, …]` Detect shape by key; `units` disambiguates sec/ms and m/ft.

## C. Field contract → renderer behavior | Field | Today | With this schema | |---|---|---| | `lat/lon/alt`, `t` | path + marker ✓ | unchanged | | **`heading`** (per-point) | loaded, unused | **marker orientation**, then camera yaw | | `pitch/roll/fov` | — | camera-follow pose (when present) | | **`pose:provenance`** | sniffed from `forage:*` | top-level facet → styling + filter | | **`pose:clock.track_window`** | — | seek-to-track-start on select | | `cov` / `constraints[]` | — | soft-constraint solver hooks (future calibrators) |

## D. Migration - Keep emitting `forage:adsb` (no break); **add** `pose:provenance`, `view:azimuth`, `pose:clock`, and the `heading`-bearing track. `forage:adsb.track_href` and `assets.adsb_track.href` both remain valid sidecar pointers. - AIR7 Palisades item = the reference instance (`recovered-prior`, 1,127-pt orbit).

## E. Soft-constraint posture (why the shapes above) `track[]` entries are **priors**, not truth; `cov` is their stiffness; `constraints[]` is where calibrators (manual + auto) later add GCP (world↔uv) and UV-UV (uv↔uv) links. The pose the viewer shows is the current minimum of that surface — camera-follow today = the single-prior identity- stiffness degenerate case, deliberately built so the prior stays a **reweightable node**.