@camstack/addon-post-analysis 1.2.295 → 1.2.297

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-Dxlc9JV3.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-BTpYUJzK.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -47053,10 +47053,127 @@ function pairRiders(detections) {
47053
47053
  }
47054
47054
  return pairs;
47055
47055
  }
47056
+ /**
47057
+ * Floor on the predictive-drift clamp, in box sides.
47058
+ *
47059
+ * The scaled term collapses at short intervals — at the 56 ms minimum measured
47060
+ * on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
47061
+ * detector box jitter into a refused match. A quarter of a side is slack, not
47062
+ * travel.
47063
+ */
47064
+ var DRIFT_MIN_SIDES = .25;
47065
+ /**
47066
+ * Re-acquisition reach, in box DIAGONALS per second.
47067
+ *
47068
+ * Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
47069
+ * centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
47070
+ * is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
47071
+ * physically bounded — 1.5 of a subject's own body-diagonal per second is a
47072
+ * brisk run, and being scale-free it means the same thing for a far-field
47073
+ * person with a 90-px box as for a near one with a 900-px box.
47074
+ *
47075
+ * The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
47076
+ * doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
47077
+ * a walking subject, and at a 150 ms burst interval it was loose enough to hand
47078
+ * one subject's id to another 452 px away (track `27855a56`, a second car, at
47079
+ * IoU 0.058). One number, two opposite failures.
47080
+ */
47081
+ var REACH_DIAGONALS_PER_SEC = 1.5;
47082
+ /**
47083
+ * Floor on the re-acquisition reach, in box diagonals.
47084
+ *
47085
+ * Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
47086
+ * interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
47087
+ * is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
47088
+ * bought that track a second car. A short gap is evidence that the subject
47089
+ * cannot have gone far, and the gate now says so.
47090
+ */
47091
+ var REACH_MIN_DIAGONALS = .3;
47092
+ /**
47093
+ * Ceiling on the re-acquisition reach, in box diagonals.
47094
+ *
47095
+ * `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
47096
+ * `maxMissedMs` gets 1.5 diagonals of reach, not 6.
47097
+ */
47098
+ var REACH_MAX_DIAGONALS = 1.5;
47099
+ function clampRange(value, min, max) {
47100
+ return Math.max(min, Math.min(max, value));
47101
+ }
47102
+ /**
47103
+ * How far a coasting box's velocity extrapolation may carry it on one axis.
47104
+ *
47105
+ * `side` is the box's width for the x drift and its height for the y drift, so
47106
+ * the allowance keeps the same meaning for a tall person and a wide car.
47107
+ */
47108
+ function driftAllowancePx(side, elapsedMs) {
47109
+ const elapsed = Math.max(0, elapsedMs);
47110
+ return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
47111
+ }
47112
+ /**
47113
+ * How far from where a track is EXPECTED a detection may sit and still be
47114
+ * claimed as that track by the loose (rescue / resurrection) reach clause.
47115
+ *
47116
+ * Scale-free: the budget is a multiple of the track's own diagonal, so it does
47117
+ * not have to know the frame size, the camera, or how far away the subject is.
47118
+ */
47119
+ function reacquireReachPx(bbox, elapsedMs) {
47120
+ const diag = Math.hypot(bbox.w, bbox.h);
47121
+ const elapsed = Math.max(0, elapsedMs);
47122
+ return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
47123
+ }
47124
+ /**
47125
+ * Resurrection reach, in box DIAGONALS per second of gap (D665).
47126
+ *
47127
+ * A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
47128
+ * rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
47129
+ * nothing useful here. A subject the detector has not boxed for four seconds
47130
+ * is, on this fleet's evidence, one that stood still or stood half-hidden,
47131
+ * not one striding across the frame (a striding person is detected): 0.25
47132
+ * diagonals per second is a slow walk for a mid-field person and a pause for
47133
+ * a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
47134
+ * camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
47135
+ * i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
47136
+ */
47137
+ var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
47138
+ /** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
47139
+ var RESURRECTION_REACH_MIN_DIAGONALS = .75;
47140
+ /**
47141
+ * How far from a corpse's last box a detection may sit and still resurrect it
47142
+ * on the proximity clause, for a corpse last seen `gapMs` ago.
47143
+ */
47144
+ function resurrectionReachPx(bbox, gapMs) {
47145
+ const diag = Math.hypot(bbox.w, bbox.h);
47146
+ return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
47147
+ }
47148
+ /**
47149
+ * The classes D665's two relaxations are for: the low-score continuation tier
47150
+ * and the time-scaled resurrection gate. Defined ONCE, here, and read by both
47151
+ * (`low-score-continuation.ts`, `SortTracker.resurrectionTimeScaledFor`),
47152
+ * because the argument for each is the same argument about the same subjects
47153
+ * (D665, D678).
47154
+ *
47155
+ * D665 grew the graveyard reach with the gap on the argument that a subject
47156
+ * nobody has boxed for more than 4 s stood still or stood half-hidden — true
47157
+ * of a person and of an animal, and what the refused returns on camera 590
47158
+ * were. A car does not stand half-hidden for ten seconds and then reappear
47159
+ * one and a half diagonals away: the next car arrives. Camera 617,
47160
+ * 2026-09-28 08:47:32Z: a car at the gate inherited the id of the car that
47161
+ * had left the lot 9.85 s earlier, 1.51 diagonals away. A `vehicle` corpse
47162
+ * keeps the flat `rescueCentroidFactor` reach and the plausible-birth veto
47163
+ * it had before D665 — the same exclusion D665 made for the low-score tier,
47164
+ * where a sub-floor box on a parked car beside a passing one is the D410 hop.
47165
+ */
47166
+ var LINGERING_SUBJECT_CLASSES = new Set(["person", "animal"]);
47167
+ /** Larger area / smaller area; `Infinity` when either box is empty. */
47168
+ function boxAreaRatio(a, b) {
47169
+ const areaA = a.w * a.h;
47170
+ const areaB = b.w * b.h;
47171
+ const smaller = Math.min(areaA, areaB);
47172
+ return smaller > 0 ? Math.max(areaA, areaB) / smaller : Number.POSITIVE_INFINITY;
47173
+ }
47056
47174
  //#endregion
47057
47175
  //#region src/pipeline-analytics/pipeline/tracker/low-score-continuation.ts
47058
47176
  var LOW_SCORE_CONTINUATION_FLOOR_DEFAULT = .35;
47059
- var LOW_SCORE_CONTINUATION_CLASSES = new Set(["person", "animal"]);
47060
47177
  /** `[x1, y1, x2, y2]` (the discard trail's tuple) → the tracker's rectangle. */
47061
47178
  function toRect(bbox) {
47062
47179
  const [x1, y1, x2, y2] = bbox;
@@ -47077,7 +47194,7 @@ function selectLowScoreContinuations(discarded, floor) {
47077
47194
  for (const d of discarded) {
47078
47195
  if (d.reason !== "below-threshold") continue;
47079
47196
  if (d.score < floor) continue;
47080
- if (!LOW_SCORE_CONTINUATION_CLASSES.has(d.macroClass)) continue;
47197
+ if (!LINGERING_SUBJECT_CLASSES.has(d.macroClass)) continue;
47081
47198
  const bbox = toRect(d.bbox);
47082
47199
  if (bbox.w <= 0 || bbox.h <= 0) continue;
47083
47200
  out.push({
@@ -47158,10 +47275,12 @@ function newTrackId() {
47158
47275
  * the counts are per camera and per detection source. */
47159
47276
  var LooseBindSampler = class {
47160
47277
  counts = /* @__PURE__ */ new Map();
47161
- /** Increment the (stage, gate) counter and answer with the running total when
47162
- * this decision should be reported, or `undefined` when it is sampled out. */
47163
- next(stage, gate) {
47164
- const key = `${stage}|${gate}`;
47278
+ /** Increment the (stage, gate, refusal) counter and answer with the running
47279
+ * total when this decision should be reported, or `undefined` when it is
47280
+ * sampled out. The refusal is part of the key so a rare refusal is not
47281
+ * sampled away behind a frequent one on the same gate. */
47282
+ next(stage, gate, refusal) {
47283
+ const key = `${stage}|${gate}|${refusal ?? ""}`;
47165
47284
  const count = (this.counts.get(key) ?? 0) + 1;
47166
47285
  this.counts.set(key, count);
47167
47286
  if (count === 1 || count % 50 === 0) return count;
@@ -47170,98 +47289,6 @@ var LooseBindSampler = class {
47170
47289
  this.counts.clear();
47171
47290
  }
47172
47291
  };
47173
- /**
47174
- * Floor on the predictive-drift clamp, in box sides.
47175
- *
47176
- * The scaled term collapses at short intervals — at the 56 ms minimum measured
47177
- * on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
47178
- * detector box jitter into a refused match. A quarter of a side is slack, not
47179
- * travel.
47180
- */
47181
- var DRIFT_MIN_SIDES = .25;
47182
- /**
47183
- * Re-acquisition reach, in box DIAGONALS per second.
47184
- *
47185
- * Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
47186
- * centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
47187
- * is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
47188
- * physically bounded — 1.5 of a subject's own body-diagonal per second is a
47189
- * brisk run, and being scale-free it means the same thing for a far-field
47190
- * person with a 90-px box as for a near one with a 900-px box.
47191
- *
47192
- * The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
47193
- * doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
47194
- * a walking subject, and at a 150 ms burst interval it was loose enough to hand
47195
- * one subject's id to another 452 px away (track `27855a56`, a second car, at
47196
- * IoU 0.058). One number, two opposite failures.
47197
- */
47198
- var REACH_DIAGONALS_PER_SEC = 1.5;
47199
- /**
47200
- * Floor on the re-acquisition reach, in box diagonals.
47201
- *
47202
- * Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
47203
- * interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
47204
- * is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
47205
- * bought that track a second car. A short gap is evidence that the subject
47206
- * cannot have gone far, and the gate now says so.
47207
- */
47208
- var REACH_MIN_DIAGONALS = .3;
47209
- /**
47210
- * Ceiling on the re-acquisition reach, in box diagonals.
47211
- *
47212
- * `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
47213
- * `maxMissedMs` gets 1.5 diagonals of reach, not 6.
47214
- */
47215
- var REACH_MAX_DIAGONALS = 1.5;
47216
- function clampRange(value, min, max) {
47217
- return Math.max(min, Math.min(max, value));
47218
- }
47219
- /**
47220
- * How far a coasting box's velocity extrapolation may carry it on one axis.
47221
- *
47222
- * `side` is the box's width for the x drift and its height for the y drift, so
47223
- * the allowance keeps the same meaning for a tall person and a wide car.
47224
- */
47225
- function driftAllowancePx(side, elapsedMs) {
47226
- const elapsed = Math.max(0, elapsedMs);
47227
- return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
47228
- }
47229
- /**
47230
- * How far from where a track is EXPECTED a detection may sit and still be
47231
- * claimed as that track by the loose (rescue / resurrection) reach clause.
47232
- *
47233
- * Scale-free: the budget is a multiple of the track's own diagonal, so it does
47234
- * not have to know the frame size, the camera, or how far away the subject is.
47235
- */
47236
- function reacquireReachPx(bbox, elapsedMs) {
47237
- const diag = Math.hypot(bbox.w, bbox.h);
47238
- const elapsed = Math.max(0, elapsedMs);
47239
- return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
47240
- }
47241
- /**
47242
- * Resurrection reach, in box DIAGONALS per second of gap (D665).
47243
- *
47244
- * A corpse is reached only after `maxMissedMs` (4 s) without a box, so the
47245
- * rescue rate above — which is at its 1.5-diagonal ceiling by 1 s — says
47246
- * nothing useful here. A subject the detector has not boxed for four seconds
47247
- * is, on this fleet's evidence, one that stood still or stood half-hidden,
47248
- * not one striding across the frame (a striding person is detected): 0.25
47249
- * diagonals per second is a slow walk for a mid-field person and a pause for
47250
- * a near one. At the 4 s floor of the graveyard band it gives 1.0 diagonal;
47251
- * camera 590's refused returns sat at 1.05–1.82× the old flat 0.75 reach,
47252
- * i.e. 0.79–1.37 diagonals, which this covers from about 5.5 s of gap.
47253
- */
47254
- var RESURRECTION_REACH_DIAGONALS_PER_SEC = .25;
47255
- /** Never tighter than the flat `rescueCentroidFactor` default it replaces. */
47256
- var RESURRECTION_REACH_MIN_DIAGONALS = .75;
47257
- /**
47258
- * How far from a corpse's last box a detection may sit and still resurrect it
47259
- * on the proximity clause, for a corpse last seen `gapMs` ago.
47260
- */
47261
- function resurrectionReachPx(bbox, gapMs) {
47262
- const diag = Math.hypot(bbox.w, bbox.h);
47263
- return clampRange(diag * RESURRECTION_REACH_DIAGONALS_PER_SEC * (Math.max(0, gapMs) / 1e3), diag * RESURRECTION_REACH_MIN_DIAGONALS, diag * 2);
47264
- }
47265
47292
  //#endregion
47266
47293
  //#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
47267
47294
  /**
@@ -47714,10 +47741,10 @@ var SortTracker = class SortTracker {
47714
47741
  }
47715
47742
  /** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
47716
47743
  * Nothing is computed when no observer is wired. */
47717
- reportBind(stage, gate, track, det, timestamp) {
47744
+ reportBind(stage, gate, track, det, timestamp, refusal) {
47718
47745
  const observer = this.bindObserver;
47719
47746
  if (!observer) return;
47720
- const bindCount = this.bindSampler.next(stage, gate);
47747
+ const bindCount = this.bindSampler.next(stage, gate, refusal);
47721
47748
  if (bindCount === void 0) return;
47722
47749
  const tc = bboxCentroid(track.bbox);
47723
47750
  const dc = bboxCentroid(det.bbox);
@@ -47751,6 +47778,7 @@ var SortTracker = class SortTracker {
47751
47778
  frameHeight: this.frameHeightPx,
47752
47779
  dtMs: timestamp - track.lastSeen,
47753
47780
  trackAgeMs: timestamp - track.firstSeen,
47781
+ ...refusal !== void 0 ? { refusal } : {},
47754
47782
  bindCount,
47755
47783
  sampledEvery: 50
47756
47784
  });
@@ -48091,15 +48119,45 @@ var SortTracker = class SortTracker {
48091
48119
  * was written for; not of the pass as it runs: on camera 590 (2026-09-27)
48092
48120
  * 38% of all re-splits were PEOPLE returning after 4–12 s, further than the
48093
48121
  * flat gate, with frames flowing the whole time. `resurrectionTimeScaled`
48094
- * off restores the flat gate.
48122
+ * off restores the flat gate — and a `vehicle` corpse always has it (D678,
48123
+ * {@link resurrectionTimeScaledFor}).
48095
48124
  */
48096
48125
  reacquireReach(track, timestamp, pass) {
48097
48126
  const diag = Math.hypot(track.bbox.w, track.bbox.h);
48098
- if (pass === "resurrection") return this.config.resurrectionTimeScaled ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
48127
+ if (pass === "resurrection") return this.resurrectionTimeScaledFor(track.class) ? resurrectionReachPx(track.bbox, timestamp - track.lastSeen) : this.config.rescueCentroidFactor * diag;
48099
48128
  if (!this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
48100
48129
  return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
48101
48130
  }
48102
48131
  /**
48132
+ * Whether D665's graveyard gate — the reach that grows with the gap, and the
48133
+ * plausible-birth veto lifted for a sole subject — applies to a corpse of
48134
+ * `cls`. People and animals only (D678): a car gone for ten seconds did not
48135
+ * stand half-hidden, the next one arrived.
48136
+ */
48137
+ resurrectionTimeScaledFor(cls) {
48138
+ return this.config.resurrectionTimeScaled && LINGERING_SUBJECT_CLASSES.has(cls);
48139
+ }
48140
+ /**
48141
+ * The size check a PROXIMITY-only resurrection needs (D678), for a pair the
48142
+ * loose gate accepted on its `centroid` clause and the plausible-birth veto
48143
+ * let through.
48144
+ *
48145
+ * The `centroid` clause says where a box is and nothing about what it is;
48146
+ * a box more than {@link RESURRECTION_MAX_AREA_RATIO} larger or smaller than
48147
+ * the corpse's last box is refused on it, and the refusal is returned so the
48148
+ * caller can name it. The pair may still bind on `growth`, the partial-crop
48149
+ * rule whose whole subject IS a size change and which `looseMatch` would
48150
+ * have reached had the centroid clause not answered first. With the default
48151
+ * coast budget that never happens — `growth` refuses a pair more than
48152
+ * `GROWTH_MAX_GAP_MS` (1 s) apart and a corpse has been unseen for
48153
+ * `maxMissedMs` (4 s) — but `maxMissedMs` is a per-camera setting down to 0.
48154
+ */
48155
+ proximitySizeVerdict(corpse, det, timestamp) {
48156
+ if (boxAreaRatio(corpse.bbox, det.bbox) <= 2.5) return "centroid";
48157
+ if (growthAdjacentMatch(corpse.bbox, det.bbox, this.growthContext(corpse, timestamp))) return "growth";
48158
+ return "size-incompatible";
48159
+ }
48160
+ /**
48103
48161
  * Whether a REACH bind would be unambiguous: this coasting track is the only
48104
48162
  * subject of its association group the scene holds, and `det` is the only box
48105
48163
  * of that group on the frame.
@@ -48513,13 +48571,26 @@ var SortTracker = class SortTracker {
48513
48571
  let refused;
48514
48572
  let refusedGate = "iou";
48515
48573
  let refusedScore = -1;
48574
+ let refusedBy = "crowd-guard";
48516
48575
  for (const lost of this.lostTracks) {
48517
48576
  if (!lost.resurrectable) continue;
48518
48577
  if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
48519
- const gate = this.looseMatch(lost, det, timestamp, "resurrection");
48520
- if (gate === null) continue;
48521
- const vetoLifted = gate === "centroid" && this.isPlausibleBirth(det, frameArea);
48522
- if (vetoLifted && !(this.config.resurrectionTimeScaled && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
48578
+ const loose = this.looseMatch(lost, det, timestamp, "resurrection");
48579
+ if (loose === null) continue;
48580
+ const vetoApplies = loose === "centroid" && this.isPlausibleBirth(det, frameArea);
48581
+ if (vetoApplies && !(this.resurrectionTimeScaledFor(lost.class) && this.isSoleSubjectResurrection(lost, det, detections, surviving, lowScore))) continue;
48582
+ const gate = loose === "centroid" ? this.proximitySizeVerdict(lost, det, timestamp) : loose;
48583
+ if (gate === "size-incompatible") {
48584
+ const score = iou$2(lost.bbox, det.bbox);
48585
+ if (score > refusedScore) {
48586
+ refused = lost;
48587
+ refusedGate = "centroid";
48588
+ refusedScore = score;
48589
+ refusedBy = "size-incompatible";
48590
+ }
48591
+ continue;
48592
+ }
48593
+ const vetoLifted = vetoApplies && gate === "centroid";
48523
48594
  const score = iou$2(lost.bbox, det.bbox);
48524
48595
  const peers = this.sameClassPeerCount(lost, surviving);
48525
48596
  if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
@@ -48527,6 +48598,7 @@ var SortTracker = class SortTracker {
48527
48598
  refused = lost;
48528
48599
  refusedGate = gate;
48529
48600
  refusedScore = score;
48601
+ refusedBy = "crowd-guard";
48530
48602
  }
48531
48603
  continue;
48532
48604
  }
@@ -48539,7 +48611,7 @@ var SortTracker = class SortTracker {
48539
48611
  }
48540
48612
  }
48541
48613
  if (!best) {
48542
- if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp);
48614
+ if (refused !== void 0) this.reportBind("resurrection-refused", refusedGate, refused, det, timestamp, refusedBy);
48543
48615
  continue;
48544
48616
  }
48545
48617
  resurrectedScene.set(best, bestPeers > 0 ? "multi-subject" : bestVetoLifted ? "sole-subject-veto-lifted" : "sole-subject");
@@ -53218,6 +53290,17 @@ var MOTION_COLUMNS = [
53218
53290
  {
53219
53291
  name: "edges",
53220
53292
  type: "TEXT"
53293
+ }),
53294
+ (
53295
+ /**
53296
+ * The first trigger of an ONBOARD episode (D672), JSON `{kind:'camera'} |
53297
+ * {kind:'sensor',deviceId}`. Declared, or the backend drops it on write (see
53298
+ * `durationMs`). Additive: the collection gains the column in place. Absent on
53299
+ * a legacy / analyzer / device-activity row means NOT RECORDED.
53300
+ */
53301
+ {
53302
+ name: "triggeredBy",
53303
+ type: "JSON"
53221
53304
  })
53222
53305
  ];
53223
53306
  var OBJECT_COLUMNS = [
@@ -53353,7 +53436,8 @@ var AUDIO_COLUMNS = [
53353
53436
  var SLIM_MOTION_COLUMNS = [
53354
53437
  "deviceId",
53355
53438
  "timestamp",
53356
- "regionCount"
53439
+ "regionCount",
53440
+ "triggeredBy"
53357
53441
  ];
53358
53442
  /**
53359
53443
  * The columns `slimObject` reads — and ONLY those.
@@ -53706,13 +53790,14 @@ var EventStore = class {
53706
53790
  });
53707
53791
  if (q.projection === "slim") return rows.map((r) => slimMotion(Number(r.id), stripNulls$1(r.data)));
53708
53792
  return rows.map((r) => {
53709
- const { edges: rawEdges, durationMs: rawDurationMs, ...data } = stripNulls$1(r.data);
53793
+ const { edges: rawEdges, durationMs: rawDurationMs, triggeredBy: rawTriggeredBy, ...data } = stripNulls$1(r.data);
53710
53794
  return {
53711
53795
  id: Number(r.id),
53712
53796
  kind: "motion",
53713
53797
  ...data,
53714
53798
  ...rawDurationMs !== void 0 ? { durationMs: rawDurationMs === OPEN_EPISODE_SENTINEL ? null : rawDurationMs } : {},
53715
- ...rawEdges !== void 0 ? { edges: decodeMotionEdges(rawEdges) } : {}
53799
+ ...rawEdges !== void 0 ? { edges: decodeMotionEdges(rawEdges) } : {},
53800
+ ...storedTriggeredBy(rawTriggeredBy)
53716
53801
  };
53717
53802
  });
53718
53803
  }
@@ -54432,13 +54517,20 @@ var EventStore = class {
54432
54517
  }];
54433
54518
  }
54434
54519
  };
54520
+ /** A stored trigger that parses, or nothing — never a guessed camera (D393). */
54521
+ function storedTriggeredBy(raw) {
54522
+ if (raw === void 0) return {};
54523
+ const parsed = require_clip_model_registry.MotionTriggeredBySchema.safeParse(raw);
54524
+ return parsed.success ? { triggeredBy: parsed.data } : {};
54525
+ }
54435
54526
  function slimMotion(id, data) {
54436
54527
  return {
54437
54528
  id,
54438
54529
  kind: "motion",
54439
54530
  deviceId: data["deviceId"],
54440
54531
  timestamp: data["timestamp"],
54441
- regionCount: data["regionCount"]
54532
+ regionCount: data["regionCount"],
54533
+ ...storedTriggeredBy(data["triggeredBy"])
54442
54534
  };
54443
54535
  }
54444
54536
  function slimObject(id, data) {
@@ -95472,6 +95564,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
95472
95564
  });
95473
95565
  const regions = payload.regions ?? [];
95474
95566
  if (detected) this.eventStore?.noteMotionSample(deviceId, regions.length, timestamp);
95567
+ const provenance = payload.source === "onboard" ? require_clip_model_registry.readMotionTriggeredBy(payload.triggeredBy) : null;
95568
+ if (detected && provenance?.malformed === true) this.ctx.logger.warn("onboard motion: triggeredBy malformed — read as the camera", {
95569
+ tags: { deviceId },
95570
+ meta: { triggeredBy: payload.triggeredBy }
95571
+ });
95475
95572
  await this.motionEpisodes?.observe(deviceId, payload.source, detected, timestamp, {
95476
95573
  regionCount: regions.length,
95477
95574
  regions: regions.map((r) => ({
@@ -95485,7 +95582,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
95485
95582
  intensity: r.intensity
95486
95583
  })),
95487
95584
  frameWidth: 0,
95488
- frameHeight: 0
95585
+ frameHeight: 0,
95586
+ ...provenance !== null ? { triggeredBy: provenance.triggeredBy } : {}
95489
95587
  });
95490
95588
  }
95491
95589
  /**