@camstack/addon-post-analysis 1.2.177 → 1.2.178

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.
@@ -35839,16 +35839,43 @@ function positionsIntersectZone(positions, frameWidth, frameHeight, zone) {
35839
35839
  }, zone.points)) return true;
35840
35840
  return false;
35841
35841
  }
35842
+ //#endregion
35843
+ //#region src/pipeline-analytics/track-lifecycle.ts
35844
+ /** Minimum confidence increase (vs last emit) that counts as a material
35845
+ * improvement. */
35846
+ var UPDATE_MIN_CONF_DELTA = .1;
35847
+ /** Debounce floor between two `update` emits for the same track (ms). */
35848
+ var UPDATE_MIN_INTERVAL_MS = 4e3;
35849
+ /** Minimum fractional bbox-area growth (vs last emit) counted as a
35850
+ * materially larger representative crop (e.g. 0.15 = 15% bigger). */
35851
+ var UPDATE_MIN_CROP_AREA_GROWTH = .15;
35852
+ /**
35853
+ * The label a `phase:'update'` SIGNAL and PAYLOAD carry, resolved from its
35854
+ * two possible sources in order: this frame's tiered patch (the frame path
35855
+ * wrote a label this very frame), then the track ROW's stored display label
35856
+ * (`subLabel ?? label`, as `trackStore.peekActive` exposes it).
35857
+ *
35858
+ * The fallback is not a nicety. The face recognizer assigns an identity over
35859
+ * the async detail plane and writes it to the row (`applyLabelCandidate`) —
35860
+ * never into a frame patch — so a signal reading only the patch can never
35861
+ * fire the identity trigger for a recognised face. Measured on the live hub
35862
+ * (2026-09-05, device 590): six of six face identities reached Home
35863
+ * Assistant only on the `end` event at track close + sweep (~lastSeen+33 s),
35864
+ * zero during the track's life, while the subject was still on camera.
35865
+ */
35866
+ function resolveLifecycleUpdateLabel(labelPatch, storedDisplayLabel) {
35867
+ return patchDisplayLabel(labelPatch) ?? storedDisplayLabel;
35868
+ }
35842
35869
  var DEFAULT_UPDATE_GATE_CONFIG = {
35843
- minConfDelta: .1,
35844
- minIntervalMs: 4e3,
35845
- minCropAreaGrowth: .15
35870
+ minConfDelta: UPDATE_MIN_CONF_DELTA,
35871
+ minIntervalMs: UPDATE_MIN_INTERVAL_MS,
35872
+ minCropAreaGrowth: UPDATE_MIN_CROP_AREA_GROWTH
35846
35873
  };
35847
35874
  /**
35848
35875
  * Decide whether a `phase:'update'` event should fire this frame.
35849
35876
  *
35850
35877
  * Priority of triggers: newly-resolved identity/plate > confidence
35851
- * improvement > materially larger crop. All triggers are debounced by
35878
+ * improvement > newly-entered zone > materially larger crop. All triggers are debounced by
35852
35879
  * `minIntervalMs` relative to the last emit. Deltas are measured against
35853
35880
  * the LAST-EMITTED memory, so a run of sub-threshold new-bests accumulates
35854
35881
  * and fires once it crosses `minConfDelta`.
@@ -35856,22 +35883,32 @@ var DEFAULT_UPDATE_GATE_CONFIG = {
35856
35883
  function evaluateTrackLifecycleUpdate(signal, memory, config) {
35857
35884
  const identityTrigger = signal.label !== void 0 && signal.label !== "" && (memory === void 0 || memory.lastLabel !== signal.label);
35858
35885
  const confidenceTrigger = signal.isNewBest && (memory === void 0 || signal.confidence - memory.lastConfidence >= config.minConfDelta);
35886
+ const zoneTrigger = signal.zonesVisitedCount !== void 0 && memory?.lastZonesVisitedCount !== void 0 && signal.zonesVisitedCount > memory.lastZonesVisitedCount;
35859
35887
  const cropTrigger = signal.bboxArea !== void 0 && memory?.lastBboxArea !== void 0 && memory.lastBboxArea > 0 && signal.bboxArea >= memory.lastBboxArea * (1 + config.minCropAreaGrowth);
35860
- const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : cropTrigger ? "crop" : void 0;
35861
- const keptMemory = memory ?? {
35888
+ const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : zoneTrigger ? "zone" : cropTrigger ? "crop" : void 0;
35889
+ const keptMemory = memory === void 0 ? {
35862
35890
  lastConfidence: signal.confidence,
35863
- lastEmitAt: signal.now
35864
- };
35891
+ lastEmitAt: signal.now,
35892
+ ...signal.zonesVisitedCount !== void 0 ? { lastZonesVisitedCount: signal.zonesVisitedCount } : {}
35893
+ } : memory.lastZonesVisitedCount === void 0 && signal.zonesVisitedCount !== void 0 ? {
35894
+ ...memory,
35895
+ lastZonesVisitedCount: signal.zonesVisitedCount
35896
+ } : memory;
35865
35897
  if (reason === void 0) return {
35866
35898
  emit: false,
35867
35899
  memory: keptMemory
35868
35900
  };
35869
35901
  if (memory !== void 0 && signal.now - memory.lastEmitAt < config.minIntervalMs) return {
35870
35902
  emit: false,
35871
- memory
35903
+ suppressed: reason,
35904
+ memory: {
35905
+ ...keptMemory,
35906
+ suppressedSinceEmit: (keptMemory.suppressedSinceEmit ?? 0) + 1
35907
+ }
35872
35908
  };
35873
35909
  const nextLabel = signal.label ?? memory?.lastLabel;
35874
35910
  const nextBboxArea = signal.bboxArea ?? memory?.lastBboxArea;
35911
+ const nextZonesCount = signal.zonesVisitedCount ?? memory?.lastZonesVisitedCount;
35875
35912
  return {
35876
35913
  emit: true,
35877
35914
  reason,
@@ -35879,7 +35916,8 @@ function evaluateTrackLifecycleUpdate(signal, memory, config) {
35879
35916
  lastConfidence: memory === void 0 ? signal.confidence : Math.max(memory.lastConfidence, signal.confidence),
35880
35917
  lastEmitAt: signal.now,
35881
35918
  ...nextLabel !== void 0 ? { lastLabel: nextLabel } : {},
35882
- ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {}
35919
+ ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {},
35920
+ ...nextZonesCount !== void 0 ? { lastZonesVisitedCount: nextZonesCount } : {}
35883
35921
  }
35884
35922
  };
35885
35923
  }
@@ -39921,7 +39959,8 @@ var TrackStore = class {
39921
39959
  * Cheap read of an active track's promotion-relevant fields WITHOUT the deep
39922
39960
  * clone `getActiveByTrack` does — the returned `positions` is the live
39923
39961
  * internal array (read-only; callers must not mutate). Feeds the per-frame
39924
- * stationary-promotion check, called at inference fps. Null if unknown/expired.
39962
+ * stationary-promotion check AND the lifecycle update gate, both called at
39963
+ * inference fps. Null if unknown/expired.
39925
39964
  */
39926
39965
  peekActive(trackId) {
39927
39966
  const t = this.active.get(trackId);
@@ -39929,6 +39968,7 @@ var TrackStore = class {
39929
39968
  return {
39930
39969
  firstSeen: t.firstSeen,
39931
39970
  positions: t.positions,
39971
+ zonesVisitedCount: t.zonesVisited.length,
39932
39972
  ...displayLabel(t) !== void 0 ? { label: displayLabel(t) } : {}
39933
39973
  };
39934
39974
  }
@@ -71158,15 +71198,41 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71158
71198
  */
71159
71199
  maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest) {
71160
71200
  const memory = this.residents.lifecycleUpdateMem(t.trackId);
71201
+ /**
71202
+ * The signal's label reads the frame patch AND the track row: the face
71203
+ * recognizer writes a recognised identity to the ROW over the async
71204
+ * detail plane, never into a frame patch, and a patch-only read left the
71205
+ * identity trigger unable to fire for a face — every face name reached
71206
+ * the export surfaces only on the `end` event, ~30 s after the subject
71207
+ * left the frame (measured live 2026-09-05).
71208
+ */
71209
+ const peeked = this.trackStore?.peekActive(t.trackId);
71210
+ const label = resolveLifecycleUpdateLabel(t.labelPatch, peeked?.label);
71161
71211
  const decision = evaluateTrackLifecycleUpdate({
71162
71212
  isNewBest,
71163
71213
  confidence: t.confidence,
71164
71214
  now: timestamp,
71165
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71215
+ ...label !== void 0 ? { label } : {},
71216
+ ...peeked !== null && peeked !== void 0 ? { zonesVisitedCount: peeked.zonesVisitedCount } : {},
71166
71217
  bboxArea: t.bbox.w * t.bbox.h
71167
71218
  }, memory, DEFAULT_UPDATE_GATE_CONFIG);
71168
71219
  this.residents.setLifecycleUpdateMem(deviceId, t.trackId, decision.memory);
71169
- if (!decision.emit) return;
71220
+ if (!decision.emit) {
71221
+ /**
71222
+ * Dropped for cadence (debounce) — counted, never silent. The count
71223
+ * since the last emit rides the memory and this line; a throttle
71224
+ * with no ledger is how a 30 s delivery delay stayed invisible.
71225
+ */
71226
+ if (decision.suppressed !== void 0) this.ctx.logger.debug("lifecycle update suppressed (debounce)", {
71227
+ tags: { deviceId },
71228
+ meta: {
71229
+ trackId: t.trackId,
71230
+ reason: decision.suppressed,
71231
+ suppressedSinceEmit: decision.memory.suppressedSinceEmit ?? 0
71232
+ }
71233
+ });
71234
+ return;
71235
+ }
71170
71236
  const track = this.trackStore?.getActiveByTrack(t.trackId);
71171
71237
  const keyFrameMediaKey = this.residents.keyFrameKey(t.trackId);
71172
71238
  const payload = buildTrackLifecyclePayload({
@@ -71178,7 +71244,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71178
71244
  bestConfidence: t.confidence,
71179
71245
  firstSeen: track?.firstSeen ?? timestamp,
71180
71246
  lastSeen: track?.lastSeen ?? timestamp,
71181
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71247
+ ...label !== void 0 ? { label } : {},
71182
71248
  ...t.plateText !== void 0 ? { plateText: t.plateText } : {},
71183
71249
  ...track?.zonesVisited !== void 0 ? { zonesVisited: track.zonesVisited } : {},
71184
71250
  ...track?.totalDistance !== void 0 ? { totalDistance: track.totalDistance } : {},
@@ -35814,16 +35814,43 @@ function positionsIntersectZone(positions, frameWidth, frameHeight, zone) {
35814
35814
  }, zone.points)) return true;
35815
35815
  return false;
35816
35816
  }
35817
+ //#endregion
35818
+ //#region src/pipeline-analytics/track-lifecycle.ts
35819
+ /** Minimum confidence increase (vs last emit) that counts as a material
35820
+ * improvement. */
35821
+ var UPDATE_MIN_CONF_DELTA = .1;
35822
+ /** Debounce floor between two `update` emits for the same track (ms). */
35823
+ var UPDATE_MIN_INTERVAL_MS = 4e3;
35824
+ /** Minimum fractional bbox-area growth (vs last emit) counted as a
35825
+ * materially larger representative crop (e.g. 0.15 = 15% bigger). */
35826
+ var UPDATE_MIN_CROP_AREA_GROWTH = .15;
35827
+ /**
35828
+ * The label a `phase:'update'` SIGNAL and PAYLOAD carry, resolved from its
35829
+ * two possible sources in order: this frame's tiered patch (the frame path
35830
+ * wrote a label this very frame), then the track ROW's stored display label
35831
+ * (`subLabel ?? label`, as `trackStore.peekActive` exposes it).
35832
+ *
35833
+ * The fallback is not a nicety. The face recognizer assigns an identity over
35834
+ * the async detail plane and writes it to the row (`applyLabelCandidate`) —
35835
+ * never into a frame patch — so a signal reading only the patch can never
35836
+ * fire the identity trigger for a recognised face. Measured on the live hub
35837
+ * (2026-09-05, device 590): six of six face identities reached Home
35838
+ * Assistant only on the `end` event at track close + sweep (~lastSeen+33 s),
35839
+ * zero during the track's life, while the subject was still on camera.
35840
+ */
35841
+ function resolveLifecycleUpdateLabel(labelPatch, storedDisplayLabel) {
35842
+ return patchDisplayLabel(labelPatch) ?? storedDisplayLabel;
35843
+ }
35817
35844
  var DEFAULT_UPDATE_GATE_CONFIG = {
35818
- minConfDelta: .1,
35819
- minIntervalMs: 4e3,
35820
- minCropAreaGrowth: .15
35845
+ minConfDelta: UPDATE_MIN_CONF_DELTA,
35846
+ minIntervalMs: UPDATE_MIN_INTERVAL_MS,
35847
+ minCropAreaGrowth: UPDATE_MIN_CROP_AREA_GROWTH
35821
35848
  };
35822
35849
  /**
35823
35850
  * Decide whether a `phase:'update'` event should fire this frame.
35824
35851
  *
35825
35852
  * Priority of triggers: newly-resolved identity/plate > confidence
35826
- * improvement > materially larger crop. All triggers are debounced by
35853
+ * improvement > newly-entered zone > materially larger crop. All triggers are debounced by
35827
35854
  * `minIntervalMs` relative to the last emit. Deltas are measured against
35828
35855
  * the LAST-EMITTED memory, so a run of sub-threshold new-bests accumulates
35829
35856
  * and fires once it crosses `minConfDelta`.
@@ -35831,22 +35858,32 @@ var DEFAULT_UPDATE_GATE_CONFIG = {
35831
35858
  function evaluateTrackLifecycleUpdate(signal, memory, config) {
35832
35859
  const identityTrigger = signal.label !== void 0 && signal.label !== "" && (memory === void 0 || memory.lastLabel !== signal.label);
35833
35860
  const confidenceTrigger = signal.isNewBest && (memory === void 0 || signal.confidence - memory.lastConfidence >= config.minConfDelta);
35861
+ const zoneTrigger = signal.zonesVisitedCount !== void 0 && memory?.lastZonesVisitedCount !== void 0 && signal.zonesVisitedCount > memory.lastZonesVisitedCount;
35834
35862
  const cropTrigger = signal.bboxArea !== void 0 && memory?.lastBboxArea !== void 0 && memory.lastBboxArea > 0 && signal.bboxArea >= memory.lastBboxArea * (1 + config.minCropAreaGrowth);
35835
- const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : cropTrigger ? "crop" : void 0;
35836
- const keptMemory = memory ?? {
35863
+ const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : zoneTrigger ? "zone" : cropTrigger ? "crop" : void 0;
35864
+ const keptMemory = memory === void 0 ? {
35837
35865
  lastConfidence: signal.confidence,
35838
- lastEmitAt: signal.now
35839
- };
35866
+ lastEmitAt: signal.now,
35867
+ ...signal.zonesVisitedCount !== void 0 ? { lastZonesVisitedCount: signal.zonesVisitedCount } : {}
35868
+ } : memory.lastZonesVisitedCount === void 0 && signal.zonesVisitedCount !== void 0 ? {
35869
+ ...memory,
35870
+ lastZonesVisitedCount: signal.zonesVisitedCount
35871
+ } : memory;
35840
35872
  if (reason === void 0) return {
35841
35873
  emit: false,
35842
35874
  memory: keptMemory
35843
35875
  };
35844
35876
  if (memory !== void 0 && signal.now - memory.lastEmitAt < config.minIntervalMs) return {
35845
35877
  emit: false,
35846
- memory
35878
+ suppressed: reason,
35879
+ memory: {
35880
+ ...keptMemory,
35881
+ suppressedSinceEmit: (keptMemory.suppressedSinceEmit ?? 0) + 1
35882
+ }
35847
35883
  };
35848
35884
  const nextLabel = signal.label ?? memory?.lastLabel;
35849
35885
  const nextBboxArea = signal.bboxArea ?? memory?.lastBboxArea;
35886
+ const nextZonesCount = signal.zonesVisitedCount ?? memory?.lastZonesVisitedCount;
35850
35887
  return {
35851
35888
  emit: true,
35852
35889
  reason,
@@ -35854,7 +35891,8 @@ function evaluateTrackLifecycleUpdate(signal, memory, config) {
35854
35891
  lastConfidence: memory === void 0 ? signal.confidence : Math.max(memory.lastConfidence, signal.confidence),
35855
35892
  lastEmitAt: signal.now,
35856
35893
  ...nextLabel !== void 0 ? { lastLabel: nextLabel } : {},
35857
- ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {}
35894
+ ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {},
35895
+ ...nextZonesCount !== void 0 ? { lastZonesVisitedCount: nextZonesCount } : {}
35858
35896
  }
35859
35897
  };
35860
35898
  }
@@ -39896,7 +39934,8 @@ var TrackStore = class {
39896
39934
  * Cheap read of an active track's promotion-relevant fields WITHOUT the deep
39897
39935
  * clone `getActiveByTrack` does — the returned `positions` is the live
39898
39936
  * internal array (read-only; callers must not mutate). Feeds the per-frame
39899
- * stationary-promotion check, called at inference fps. Null if unknown/expired.
39937
+ * stationary-promotion check AND the lifecycle update gate, both called at
39938
+ * inference fps. Null if unknown/expired.
39900
39939
  */
39901
39940
  peekActive(trackId) {
39902
39941
  const t = this.active.get(trackId);
@@ -39904,6 +39943,7 @@ var TrackStore = class {
39904
39943
  return {
39905
39944
  firstSeen: t.firstSeen,
39906
39945
  positions: t.positions,
39946
+ zonesVisitedCount: t.zonesVisited.length,
39907
39947
  ...displayLabel(t) !== void 0 ? { label: displayLabel(t) } : {}
39908
39948
  };
39909
39949
  }
@@ -71084,15 +71124,41 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71084
71124
  */
71085
71125
  maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest) {
71086
71126
  const memory = this.residents.lifecycleUpdateMem(t.trackId);
71127
+ /**
71128
+ * The signal's label reads the frame patch AND the track row: the face
71129
+ * recognizer writes a recognised identity to the ROW over the async
71130
+ * detail plane, never into a frame patch, and a patch-only read left the
71131
+ * identity trigger unable to fire for a face — every face name reached
71132
+ * the export surfaces only on the `end` event, ~30 s after the subject
71133
+ * left the frame (measured live 2026-09-05).
71134
+ */
71135
+ const peeked = this.trackStore?.peekActive(t.trackId);
71136
+ const label = resolveLifecycleUpdateLabel(t.labelPatch, peeked?.label);
71087
71137
  const decision = evaluateTrackLifecycleUpdate({
71088
71138
  isNewBest,
71089
71139
  confidence: t.confidence,
71090
71140
  now: timestamp,
71091
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71141
+ ...label !== void 0 ? { label } : {},
71142
+ ...peeked !== null && peeked !== void 0 ? { zonesVisitedCount: peeked.zonesVisitedCount } : {},
71092
71143
  bboxArea: t.bbox.w * t.bbox.h
71093
71144
  }, memory, DEFAULT_UPDATE_GATE_CONFIG);
71094
71145
  this.residents.setLifecycleUpdateMem(deviceId, t.trackId, decision.memory);
71095
- if (!decision.emit) return;
71146
+ if (!decision.emit) {
71147
+ /**
71148
+ * Dropped for cadence (debounce) — counted, never silent. The count
71149
+ * since the last emit rides the memory and this line; a throttle
71150
+ * with no ledger is how a 30 s delivery delay stayed invisible.
71151
+ */
71152
+ if (decision.suppressed !== void 0) this.ctx.logger.debug("lifecycle update suppressed (debounce)", {
71153
+ tags: { deviceId },
71154
+ meta: {
71155
+ trackId: t.trackId,
71156
+ reason: decision.suppressed,
71157
+ suppressedSinceEmit: decision.memory.suppressedSinceEmit ?? 0
71158
+ }
71159
+ });
71160
+ return;
71161
+ }
71096
71162
  const track = this.trackStore?.getActiveByTrack(t.trackId);
71097
71163
  const keyFrameMediaKey = this.residents.keyFrameKey(t.trackId);
71098
71164
  const payload = buildTrackLifecyclePayload({
@@ -71104,7 +71170,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71104
71170
  bestConfidence: t.confidence,
71105
71171
  firstSeen: track?.firstSeen ?? timestamp,
71106
71172
  lastSeen: track?.lastSeen ?? timestamp,
71107
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71173
+ ...label !== void 0 ? { label } : {},
71108
71174
  ...t.plateText !== void 0 ? { plateText: t.plateText } : {},
71109
71175
  ...track?.zonesVisited !== void 0 ? { zonesVisited: track.zonesVisited } : {},
71110
71176
  ...track?.totalDistance !== void 0 ? { totalDistance: track.totalDistance } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.177",
3
+ "version": "1.2.178",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",