@camstack/addon-post-analysis 1.2.192 → 1.2.194

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.
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg$1, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-CoI1xhsw.mjs";
1
+ import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg$1, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-DLVNGF8I.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -33992,6 +33992,18 @@ var LooseBindSampler = class {
33992
33992
  this.counts.clear();
33993
33993
  }
33994
33994
  };
33995
+ /** Per-reason counter + sampling decision. One instance per tracker, so the
33996
+ * counts are per camera and per detection source. */
33997
+ var SpawnRefusalSampler = class {
33998
+ counts = /* @__PURE__ */ new Map();
33999
+ /** The running count when this refusal should be reported, else `undefined`. */
34000
+ next(reason) {
34001
+ const count = (this.counts.get(reason) ?? 0) + 1;
34002
+ this.counts.set(reason, count);
34003
+ if (count <= 10) return count;
34004
+ return count % 10 === 0 ? count : void 0;
34005
+ }
34006
+ };
33995
34007
  //#endregion
33996
34008
  //#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
33997
34009
  var DEFAULT_TRACKER_CONFIG = {
@@ -34076,6 +34088,10 @@ var SortTracker = class SortTracker {
34076
34088
  /** Sink for salvage-bind diagnostics. Null until an owner that knows the
34077
34089
  * `deviceId` wires one — see {@link setBindObserver}. */
34078
34090
  bindObserver = null;
34091
+ /** Per-reason counters behind the sampled dropped-detection diagnostics. */
34092
+ refusalSampler = new SpawnRefusalSampler();
34093
+ /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34094
+ spawnRefusalObserver = null;
34079
34095
  constructor(config = {}) {
34080
34096
  this.config = {
34081
34097
  ...DEFAULT_TRACKER_CONFIG,
@@ -34091,6 +34107,37 @@ var SortTracker = class SortTracker {
34091
34107
  setBindObserver(observer) {
34092
34108
  this.bindObserver = observer;
34093
34109
  }
34110
+ /**
34111
+ * Wire the diagnostics sink for detections the tracker DROPS — a spawn a
34112
+ * gate refused, or a box a track of another class absorbed. Same contract as
34113
+ * {@link setBindObserver}: the tracker has no `deviceId`, the caller stamps
34114
+ * it. Nothing is computed when no observer is wired.
34115
+ */
34116
+ setSpawnRefusalObserver(observer) {
34117
+ this.spawnRefusalObserver = observer;
34118
+ }
34119
+ /** Emit one dropped-detection record, subject to the refusal sampler. */
34120
+ reportRefusal(reason, det, extra = {}) {
34121
+ const observer = this.spawnRefusalObserver;
34122
+ if (!observer) return;
34123
+ const refusalCount = this.refusalSampler.next(reason);
34124
+ if (refusalCount === void 0) return;
34125
+ observer({
34126
+ reason,
34127
+ detClass: det.class,
34128
+ detScore: det.score,
34129
+ bbox: det.bbox,
34130
+ ...det.nativeRecovery !== void 0 ? { nativeRecoveryScore: det.nativeRecovery.score } : {},
34131
+ ...extra.threshold !== void 0 ? { threshold: extra.threshold } : {},
34132
+ ...extra.measured !== void 0 ? { measured: extra.measured } : {},
34133
+ ...extra.against !== void 0 ? {
34134
+ againstTrackId: extra.against.id,
34135
+ againstTrackClass: extra.against.class
34136
+ } : {},
34137
+ refusalCount,
34138
+ sampledEvery: 10
34139
+ });
34140
+ }
34094
34141
  /** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
34095
34142
  * Nothing is computed when no observer is wired. */
34096
34143
  reportBind(stage, gate, track, det, timestamp) {
@@ -34150,9 +34197,49 @@ var SortTracker = class SortTracker {
34150
34197
  * exact class when `classGating` is off (no gate at all) — matching the legacy
34151
34198
  * contract — otherwise same association group (group == exact class unless
34152
34199
  * `classGroupAssoc` widened it to {person,animal}). */
34153
- associable(trackClass, detClass) {
34200
+ associable(trackClass, det) {
34201
+ if (!this.sameGroupAsMeasured(trackClass, det)) return false;
34154
34202
  if (!this.config.classGating) return true;
34155
- return this.assocGroup(trackClass) === this.assocGroup(detClass);
34203
+ return this.assocGroup(trackClass) === this.assocGroup(det.class);
34204
+ }
34205
+ /**
34206
+ * A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378).
34207
+ *
34208
+ * `classGroupAssoc` collapses {person, animal} into one association group for
34209
+ * one failure of the COARSE full-frame pass: a bending person re-labelled
34210
+ * `animal` for a frame, over the person's own box. Letting that box re-match
34211
+ * the person track is strictly better than minting a concurrent id.
34212
+ *
34213
+ * The runner's second pass makes the opposite measurement — the same detector
34214
+ * on a 640-800 px native square around a motion region the full-frame boxes
34215
+ * do NOT account for. Such a box is, by construction, not the subject the
34216
+ * full pass already has; absorbing it into a person track means the animal is
34217
+ * never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
34218
+ * an `animal` at `nativeRecoveryScore=0.833`.
34219
+ *
34220
+ * So the widening does not apply to a detection carrying `nativeRecovery`:
34221
+ * it associates to its OWN class or it spawns. Every other guard on that
34222
+ * spawn still runs — the per-class score floor, the area floor, the
34223
+ * `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
34224
+ * still beats a cross-class animal.
34225
+ */
34226
+ sameGroupAsMeasured(trackClass, det) {
34227
+ if (det.nativeRecovery === void 0) return true;
34228
+ return trackClass === det.class;
34229
+ }
34230
+ /**
34231
+ * Whether a matched track's box refuses `det` a birth. The gate exists for a
34232
+ * detector double-firing INSIDE a subject it already has, so it is a
34233
+ * same-subject claim: for a box the native pass measured (D378), a
34234
+ * cross-class occluder is not that claim — a dog at its owner's knee is 100%
34235
+ * contained in the person's box and is not the person. 2026-09-07 07:56:37,
34236
+ * an `animal` at `nativeRecoveryScore=0.5143` refused on `threshold=0.6`.
34237
+ * A SAME-class occluder still refuses it: that is the torso the crop cut out
34238
+ * of a person the full pass already has.
34239
+ */
34240
+ occludesSpawn(det, occluder) {
34241
+ if (!this.sameGroupAsMeasured(occluder.class, det)) return false;
34242
+ return containment(det.bbox, occluder.bbox) >= this.config.occlusionContainment;
34156
34243
  }
34157
34244
  /** Whether a spawn candidate clears its per-class minimum bbox-area floor
34158
34245
  * (fraction of frame area). Ungated when disabled, when the class has no floor,
@@ -34174,14 +34261,30 @@ var SortTracker = class SortTracker {
34174
34261
  isDuplicateSpawn(det, live, timestamp) {
34175
34262
  for (const t of live) {
34176
34263
  if (!this.dedupCompatible(t.class, det.class)) continue;
34177
- if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
34264
+ const overlap = iou$3(det.bbox, t.bbox);
34265
+ if (overlap >= this.config.dedupSpawnIou) {
34266
+ this.reportRefusal("dedup-iou", det, {
34267
+ threshold: this.config.dedupSpawnIou,
34268
+ measured: overlap,
34269
+ against: t
34270
+ });
34271
+ return true;
34272
+ }
34178
34273
  if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
34179
34274
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
34180
34275
  return true;
34181
34276
  }
34182
34277
  }
34183
- if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
34184
- for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
34278
+ if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.nativeRecovery === void 0 && det.score < this.config.animalOverPersonMaxScore) for (const t of live) {
34279
+ const overlap = iou$3(det.bbox, t.bbox);
34280
+ if (t.class === PERSON_CLASS && overlap >= this.config.animalOverPersonIou) {
34281
+ this.reportRefusal("animal-over-person", det, {
34282
+ threshold: this.config.animalOverPersonMaxScore,
34283
+ measured: overlap,
34284
+ against: t
34285
+ });
34286
+ return true;
34287
+ }
34185
34288
  }
34186
34289
  return false;
34187
34290
  }
@@ -34296,6 +34399,7 @@ var SortTracker = class SortTracker {
34296
34399
  * class.
34297
34400
  */
34298
34401
  looseMatch(track, det, timestamp) {
34402
+ if (!this.sameGroupAsMeasured(track.class, det)) return null;
34299
34403
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34300
34404
  if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34301
34405
  const tc = bboxCentroid(track.bbox);
@@ -34382,7 +34486,7 @@ var SortTracker = class SortTracker {
34382
34486
  for (const di of candidateIdxs) {
34383
34487
  if (used.has(di)) continue;
34384
34488
  const det = detections[di];
34385
- if (!this.associable(track.class, det.class)) continue;
34489
+ if (!this.associable(track.class, det)) continue;
34386
34490
  const score = iou$3(pbox, det.bbox);
34387
34491
  if (score >= iouThreshold) pairs.push({
34388
34492
  track,
@@ -34461,6 +34565,7 @@ var SortTracker = class SortTracker {
34461
34565
  }
34462
34566
  if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
34463
34567
  for (const [track, det] of matched) {
34568
+ if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
34464
34569
  const prevCenter = bboxCentroid({
34465
34570
  x: track.bbox.x,
34466
34571
  y: track.bbox.y,
@@ -34489,15 +34594,18 @@ var SortTracker = class SortTracker {
34489
34594
  if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
34490
34595
  this.addClassVote(track, det);
34491
34596
  }
34492
- const occluderBoxes = [];
34493
- for (const track of matchedTracks) occluderBoxes.push(track.bbox);
34597
+ const occluders = [];
34598
+ for (const track of matchedTracks) occluders.push({
34599
+ bbox: track.bbox,
34600
+ class: track.class
34601
+ });
34494
34602
  const surviving = [];
34495
34603
  for (const track of this.tracks) {
34496
34604
  if (matchedTracks.has(track)) {
34497
34605
  surviving.push(track);
34498
34606
  continue;
34499
34607
  }
34500
- const occluded = this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(this.predicted(track), ob) >= this.config.occlusionContainment);
34608
+ const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track), ob.bbox) >= this.config.occlusionContainment);
34501
34609
  track.age++;
34502
34610
  if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
34503
34611
  track.lost = true;
@@ -34576,14 +34684,25 @@ var SortTracker = class SortTracker {
34576
34684
  for (const di of unmatchedIdx) {
34577
34685
  const det = detections[di];
34578
34686
  if (!this.meetsClassScoreFloor(det)) {
34687
+ this.reportRefusal("score-floor", det, {
34688
+ threshold: this.config.classMinScores[det.class] ?? 0,
34689
+ measured: det.score
34690
+ });
34579
34691
  used.add(di);
34580
34692
  continue;
34581
34693
  }
34582
34694
  if (!this.meetsSpawnAreaFloor(det, frameArea)) {
34695
+ this.reportRefusal("area-floor", det, {
34696
+ threshold: this.config.classSpawnAreaFracs[det.class] ?? 0,
34697
+ measured: frameArea > 0 ? det.bbox.w * det.bbox.h / frameArea : 0
34698
+ });
34583
34699
  used.add(di);
34584
34700
  continue;
34585
34701
  }
34586
- if (this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(det.bbox, ob) >= this.config.occlusionContainment)) continue;
34702
+ if (this.config.occlusionEnabled && occluders.some((ob) => this.occludesSpawn(det, ob))) {
34703
+ this.reportRefusal("occlusion", det, { threshold: this.config.occlusionContainment });
34704
+ continue;
34705
+ }
34587
34706
  if (this.config.dedupEnabled && this.isDuplicateSpawn(det, surviving, timestamp)) {
34588
34707
  used.add(di);
34589
34708
  continue;
@@ -35071,6 +35190,9 @@ var FrameProcessor = class {
35071
35190
  * Only the rescue / resurrection / growth branches report, and they report
35072
35191
  * sampled: stage 1 runs per track per frame at frame-rate.
35073
35192
  */
35193
+ setSpawnRefusalObserver(observer) {
35194
+ this.tracker.setSpawnRefusalObserver(observer);
35195
+ }
35074
35196
  setBindObserver(observer) {
35075
35197
  this.tracker.setBindObserver(observer);
35076
35198
  }
@@ -35093,6 +35215,8 @@ var FrameProcessor = class {
35093
35215
  const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
35094
35216
  const plateByBbox = /* @__PURE__ */ new Map();
35095
35217
  const maskByBbox = /* @__PURE__ */ new Map();
35218
+ const nativeRecoveryByBbox = /* @__PURE__ */ new Map();
35219
+ const birthEvidenceByBbox = /* @__PURE__ */ new Map();
35096
35220
  const flatDetections = frame.detections.filter((d) => d.kind === "first-level").map((det) => {
35097
35221
  const bbox = {
35098
35222
  x: det.bbox.x,
@@ -35104,8 +35228,11 @@ var FrameProcessor = class {
35104
35228
  class: det.macroClass,
35105
35229
  originalClass: det.debug?.originalClass ?? det.macroClass,
35106
35230
  score: det.score,
35107
- bbox
35231
+ bbox,
35232
+ ...det.nativeRecovery !== void 0 ? { nativeRecovery: det.nativeRecovery } : {}
35108
35233
  };
35234
+ if (det.nativeRecovery !== void 0) nativeRecoveryByBbox.set(bbox, det.nativeRecovery);
35235
+ if (det.birthEvidence !== void 0) birthEvidenceByBbox.set(bbox, det.birthEvidence);
35109
35236
  if (det.labels.length > 0) labelsByBbox.set(bbox, det.labels);
35110
35237
  if (det.debug?.originalClassTier !== void 0) originalClassTierByBbox.set(bbox, det.debug.originalClassTier);
35111
35238
  if (det.embedding !== void 0) embeddingByBbox.set(bbox, {
@@ -35330,6 +35457,8 @@ var FrameProcessor = class {
35330
35457
  const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
35331
35458
  const plate = plateByBbox.get(td.bbox);
35332
35459
  const sourceDetectionId = sourceIdByBbox.get(td.bbox);
35460
+ const nativeRecovery = td.matchedThisFrame !== false ? nativeRecoveryByBbox.get(td.bbox) : void 0;
35461
+ const birthEvidence = td.matchedThisFrame !== false ? birthEvidenceByBbox.get(td.bbox) : void 0;
35333
35462
  return {
35334
35463
  trackId: td.trackId,
35335
35464
  ...sourceDetectionId !== void 0 ? { sourceDetectionId } : {},
@@ -35352,7 +35481,9 @@ var FrameProcessor = class {
35352
35481
  plateText: plate.text,
35353
35482
  plateScore: plate.score,
35354
35483
  plateBbox: plate.bbox
35355
- } : {}
35484
+ } : {},
35485
+ ...nativeRecovery !== void 0 ? { nativeRecovery } : {},
35486
+ ...birthEvidence !== void 0 ? { birthEvidence } : {}
35356
35487
  };
35357
35488
  });
35358
35489
  this.lastLabelWriteRefusals = refusals;
@@ -45154,9 +45285,28 @@ function withTimeout$2(promise, timeoutMs) {
45154
45285
  });
45155
45286
  }
45156
45287
  async function runConfirmation(candidate, config, deps, exhausted) {
45288
+ const carried = candidate.evidenceCrop ?? null;
45289
+ if (carried) {
45290
+ const verdict = await measure(candidate, carried, config, deps, "carried");
45291
+ if (verdict.verdict === "confirmed" || verdict.reason === "redetect-error") return verdict;
45292
+ }
45157
45293
  let crop = await deps.fetchCrop(candidate);
45158
- if (!crop && exhausted && deps.fetchFallbackCrop) crop = await deps.fetchFallbackCrop(candidate);
45294
+ let cropSource = "native";
45295
+ if (!crop && exhausted && deps.fetchFallbackCrop) {
45296
+ crop = await deps.fetchFallbackCrop(candidate);
45297
+ cropSource = "fallback";
45298
+ }
45159
45299
  if (!crop) return undecided(candidate, "no-crop");
45300
+ return measure(candidate, crop, config, deps, cropSource);
45301
+ }
45302
+ /**
45303
+ * Re-detect on ONE crop and turn the result into a verdict.
45304
+ *
45305
+ * Shared by all three sources so a crop can never be judged by two slightly
45306
+ * different rules depending on where it came from — the `cropSource` argument
45307
+ * is the ONLY thing that varies, and it varies in exactly one place below.
45308
+ */
45309
+ async function measure(candidate, crop, config, deps, cropSource) {
45160
45310
  const detections = await deps.redetect(crop);
45161
45311
  if (detections === null) return undecided(candidate, "redetect-error");
45162
45312
  let best;
@@ -45164,8 +45314,9 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45164
45314
  for (const d of detections) if (isConfirmationCompatible(candidate.className, d.macroClass)) {
45165
45315
  if (!best || d.score > best.score) best = d;
45166
45316
  } else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
45167
- const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45317
+ const bar = barFor(candidate, config, deps);
45168
45318
  const confirmed = best !== void 0 && best.score >= bar;
45319
+ if (!confirmed && cropSource !== "native") return undecided(candidate, cropSource === "fallback" ? "fallback-inconclusive" : "carried-inconclusive");
45169
45320
  const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
45170
45321
  return {
45171
45322
  trackId: candidate.trackId,
@@ -45173,6 +45324,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45173
45324
  reason: confirmed ? "confirmed" : "suppressed",
45174
45325
  className: candidate.className,
45175
45326
  appliedMinConfidence: bar,
45327
+ cropSource,
45176
45328
  ...best ? { bestScore: best.score } : {},
45177
45329
  ...bestIncompatible ? {
45178
45330
  bestIncompatibleClass: bestIncompatible.macroClass,
@@ -45203,7 +45355,27 @@ function promoteSecondary(candidate, detection, bar, deps) {
45203
45355
  }
45204
45356
  };
45205
45357
  }
45358
+ /**
45359
+ * The bar this candidate faces. Never below the configured one: the
45360
+ * `minConfidenceFor` hook is monotonic by construction, and clamping here says
45361
+ * so at the point of use rather than trusting the caller to have been careful.
45362
+ */
45363
+ function barFor(candidate, config, deps) {
45364
+ return Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45365
+ }
45206
45366
  async function confirmOne(candidate, config, deps, exhausted) {
45367
+ const native = candidate.nativeRecovery;
45368
+ if (native !== void 0) {
45369
+ const bar = barFor(candidate, config, deps);
45370
+ if (native.score >= bar) return {
45371
+ trackId: candidate.trackId,
45372
+ verdict: "confirmed",
45373
+ reason: "native-pass",
45374
+ className: candidate.className,
45375
+ bestScore: native.score,
45376
+ appliedMinConfidence: bar
45377
+ };
45378
+ }
45207
45379
  const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
45208
45380
  if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
45209
45381
  try {
@@ -51665,6 +51837,97 @@ async function normalizePhotoOrientation(photo) {
51665
51837
  if (orientation === void 0 || orientation === EXIF_UPRIGHT) return photo;
51666
51838
  return sharp(photo).rotate().jpeg({ quality: 95 }).toBuffer();
51667
51839
  }
51840
+ /**
51841
+ * How long one birth's evidence is kept. Twice `confirmationGateMaxDeferralMs`
51842
+ * (2 000 ms default) — the longest a deferred birth can still be re-offered.
51843
+ */
51844
+ var BIRTH_EVIDENCE_TTL_MS = 4e3;
51845
+ var BirthEvidenceStore = class {
51846
+ slotsPerCamera;
51847
+ ttlMs;
51848
+ byDevice = /* @__PURE__ */ new Map();
51849
+ constructor(slotsPerCamera = 8, ttlMs = BIRTH_EVIDENCE_TTL_MS) {
51850
+ this.slotsPerCamera = slotsPerCamera;
51851
+ this.ttlMs = ttlMs;
51852
+ }
51853
+ /**
51854
+ * Keep this birth's crop.
51855
+ *
51856
+ * The FIRST crop for an id wins. A later frame's crop of the same track is a
51857
+ * different instant, and this store exists precisely to preserve the instant
51858
+ * the birth was measured on — overwriting it would quietly reintroduce the
51859
+ * defect the fallback asymmetry exists to distrust.
51860
+ */
51861
+ remember(deviceKey, trackId, crop, nowMs) {
51862
+ let slots = this.byDevice.get(deviceKey);
51863
+ if (!slots) {
51864
+ slots = /* @__PURE__ */ new Map();
51865
+ this.byDevice.set(deviceKey, slots);
51866
+ }
51867
+ if (slots.has(trackId)) return;
51868
+ slots.set(trackId, {
51869
+ crop,
51870
+ atMs: nowMs
51871
+ });
51872
+ this.evict(deviceKey, slots, nowMs);
51873
+ }
51874
+ /** This birth's crop, or `null` when it was never kept or has aged out. */
51875
+ get(deviceKey, trackId, nowMs) {
51876
+ const entry = this.byDevice.get(deviceKey)?.get(trackId);
51877
+ if (!entry) return null;
51878
+ if (nowMs - entry.atMs > this.ttlMs) {
51879
+ this.resolve(deviceKey, trackId);
51880
+ return null;
51881
+ }
51882
+ return entry.crop;
51883
+ }
51884
+ /** A verdict finally arrived — the crop is dead weight from here. */
51885
+ resolve(deviceKey, trackId) {
51886
+ const slots = this.byDevice.get(deviceKey);
51887
+ if (!slots) return;
51888
+ slots.delete(trackId);
51889
+ if (slots.size === 0) this.byDevice.delete(deviceKey);
51890
+ }
51891
+ /**
51892
+ * Forget every id the tracker is no longer carrying — the same per-frame
51893
+ * contract as `DeferredBirthRegistry.retain`. An id the tracker dropped can
51894
+ * never be decided, so holding its pixels only leaks.
51895
+ */
51896
+ retain(deviceKey, currentTrackIds) {
51897
+ const slots = this.byDevice.get(deviceKey);
51898
+ if (!slots) return;
51899
+ for (const id of [...slots.keys()]) if (!currentTrackIds.has(id)) slots.delete(id);
51900
+ if (slots.size === 0) this.byDevice.delete(deviceKey);
51901
+ }
51902
+ /** Crops held for one camera (diagnostics and tests). */
51903
+ size(deviceKey) {
51904
+ return this.byDevice.get(deviceKey)?.size ?? 0;
51905
+ }
51906
+ /** Bytes held across every camera — the declared budget, checkable. */
51907
+ totalBytes() {
51908
+ let total = 0;
51909
+ for (const slots of this.byDevice.values()) for (const entry of slots.values()) total += entry.crop.byteLength;
51910
+ return total;
51911
+ }
51912
+ /** Drop a camera's reserve wholesale (device removed / pipeline reset). */
51913
+ clearDevice(deviceKey) {
51914
+ this.byDevice.delete(deviceKey);
51915
+ }
51916
+ /**
51917
+ * Age first, then the quota — and only ever inside THIS camera's slots.
51918
+ * `Map` keeps insertion order and a crop is inserted once, so the first key
51919
+ * is always the oldest.
51920
+ */
51921
+ evict(deviceKey, slots, nowMs) {
51922
+ for (const [id, entry] of [...slots]) if (nowMs - entry.atMs > this.ttlMs) slots.delete(id);
51923
+ while (slots.size > this.slotsPerCamera) {
51924
+ const oldest = slots.keys().next().value;
51925
+ if (oldest === void 0) break;
51926
+ slots.delete(oldest);
51927
+ }
51928
+ if (slots.size === 0) this.byDevice.delete(deviceKey);
51929
+ }
51930
+ };
51668
51931
  //#endregion
51669
51932
  //#region src/pipeline-analytics/pipeline/deferred-births.ts
51670
51933
  var DeferredBirthRegistry = class {
@@ -53821,6 +54084,77 @@ function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD
53821
54084
  return best >= threshold ? best : null;
53822
54085
  }
53823
54086
  //#endregion
54087
+ //#region src/pipeline-analytics/pipeline/started-births.ts
54088
+ /**
54089
+ * Memory of births whose ritual has already RUN, per device+source.
54090
+ *
54091
+ * A birth is a decision, and a decision is taken once. Until 2026-09-07 it
54092
+ * could be taken twice, on camera 615 it routinely was, and the second vote
54093
+ * won:
54094
+ *
54095
+ * 04:59:38.335Z confirmation gate: birth confirmed {trackId=f585b2ee…
54096
+ * className=animal bestScore=0.7068}
54097
+ * 04:59:38.335Z track started {trackId=f585b2ee…}
54098
+ * 04:59:38.821Z confirmation gate: birth suppressed (false positive)
54099
+ * {trackId=f585b2ee… className=animal} ← no bestScore:
54100
+ * the second crop held no compatible detection AT ALL
54101
+ * 04:59:38.821Z birth suppressed — track record retracted
54102
+ *
54103
+ * TWO conditions have to meet, and both are structural:
54104
+ *
54105
+ * 1. `processFrame` reads `lastActiveTrackIds` at the top and writes it at the
54106
+ * bottom, and AWAITS the confirmation gate in between (a crop round trip to
54107
+ * the frame's node plus a re-detection: 130–490 ms measured on this camera,
54108
+ * against 100 ms frame spacing). Several frames are therefore in flight at
54109
+ * once and each of them sees the same fresh id as unjudged.
54110
+ * 2. The other birth discriminator, `positionsCount > 1`, does not close the
54111
+ * gap either: a COASTED frame deliberately appends no position
54112
+ * (`TrackStore.upsert`, `matched: false`), so a track the full-frame pass
54113
+ * cannot see — which is the entire premise of the small-subject recovery —
54114
+ * keeps exactly one position for as long as it coasts, and
54115
+ * `classifyTrackAppearance` keeps answering `birth`.
54116
+ *
54117
+ * So the id that has already been born is remembered HERE, and both halves of
54118
+ * the birth path consult it: it is not offered to the gate again, and a verdict
54119
+ * that arrives for it anyway cannot retract it. Same lifetime discipline as
54120
+ * {@link SuppressedBirthRegistry} — held while the tracker still carries the
54121
+ * id, forgotten the frame it stops.
54122
+ */
54123
+ var StartedBirthRegistry = class {
54124
+ byDevice = /* @__PURE__ */ new Map();
54125
+ /** Record that this id's birth ritual ran — `start`, firstFrame, the lot. */
54126
+ markStarted(deviceKey, trackId) {
54127
+ let ids = this.byDevice.get(deviceKey);
54128
+ if (!ids) {
54129
+ ids = /* @__PURE__ */ new Set();
54130
+ this.byDevice.set(deviceKey, ids);
54131
+ }
54132
+ ids.add(trackId);
54133
+ }
54134
+ hasStarted(deviceKey, trackId) {
54135
+ return this.byDevice.get(deviceKey)?.has(trackId) === true;
54136
+ }
54137
+ /**
54138
+ * Forget every started id the tracker is no longer carrying. Called once per
54139
+ * frame with the ids present THIS frame — an id the tracker has dropped can
54140
+ * never be re-offered as a birth, so holding it serves nothing.
54141
+ */
54142
+ retain(deviceKey, currentTrackIds) {
54143
+ const ids = this.byDevice.get(deviceKey);
54144
+ if (!ids) return;
54145
+ for (const id of ids) if (!currentTrackIds.has(id)) ids.delete(id);
54146
+ if (ids.size === 0) this.byDevice.delete(deviceKey);
54147
+ }
54148
+ /** Started ids currently held for a device — diagnostics and tests. */
54149
+ size(deviceKey) {
54150
+ return this.byDevice.get(deviceKey)?.size ?? 0;
54151
+ }
54152
+ /** Drop a device's memory wholesale (device removed / pipeline reset). */
54153
+ clearDevice(deviceKey) {
54154
+ this.byDevice.delete(deviceKey);
54155
+ }
54156
+ };
54157
+ //#endregion
53824
54158
  //#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
53825
54159
  /**
53826
54160
  * stationary-evidence — how a parked object EARNS a registry entry.
@@ -67453,9 +67787,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67453
67787
  /** See `pipeline/suppressed-births.ts` — rejected births must not be
67454
67788
  * re-upserted, and must be forgotten when the tracker drops the id. */
67455
67789
  suppressedBirths = new SuppressedBirthRegistry();
67790
+ /** Ids whose birth ritual has already run — a birth is decided ONCE (D377). */
67791
+ startedBirths = new StartedBirthRegistry();
67456
67792
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
67457
67793
  deferredBirths = new DeferredBirthRegistry();
67458
67794
  /**
67795
+ * The pixels each deferred birth arrived with — a RESERVE per camera, evicted
67796
+ * by age inside that camera's own quota (D379).
67797
+ *
67798
+ * Not a shared cap. The path this replaces was one node-wide 4 096-entry LRU
67799
+ * measured pinned full in 6 814 of 6 849 samples over three hours, in which a
67800
+ * busy camera evicted a quiet one's proof and every camera added shortened
67801
+ * every other camera's window. See `pipeline/birth-evidence-store.ts` for the
67802
+ * two measurements the slot count and the TTL are taken from.
67803
+ */
67804
+ birthEvidence = new BirthEvidenceStore();
67805
+ /**
67459
67806
  * Subjects the confirmation gate found in a birth's crop BESIDES the birth
67460
67807
  * itself (a dog in a person's crop), waiting to ride the next frame of the
67461
67808
  * same (device, source) as detections — `pipeline/secondary-subjects.ts`.
@@ -70671,10 +71018,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70671
71018
  if (prevIds.has(id)) continue;
70672
71019
  const t = result.tracked.find((x) => x.trackId === id);
70673
71020
  if (!t) continue;
70674
- if (classifyTrackAppearance({
71021
+ const appearance = classifyTrackAppearance({
70675
71022
  inPrevActive: false,
70676
71023
  positionsCount: positionsCountById.get(id) ?? 1
70677
- }) === "resurrection") {
71024
+ });
71025
+ if (this.startedBirths.hasStarted(key, id)) continue;
71026
+ if (appearance === "resurrection") {
70678
71027
  log.info("track resumed", {
70679
71028
  tags: { deviceId },
70680
71029
  meta: {
@@ -70707,11 +71056,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70707
71056
  const exhaustionNowMs = Date.now();
70708
71057
  const exhaustedIds = /* @__PURE__ */ new Set();
70709
71058
  for (const { id } of bornCandidates) if (this.deferredBirths.exhausted(key, id, exhaustionNowMs, gateSettings.maxAttempts, gateSettings.maxDeferralMs)) exhaustedIds.add(id);
70710
- const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
70711
- trackId: id,
70712
- className: t.className,
70713
- bbox: t.bbox
70714
- })), {
71059
+ for (const { id, t } of bornCandidates) {
71060
+ if (t.birthEvidence === void 0) continue;
71061
+ this.birthEvidence.remember(key, id, Buffer.from(t.birthEvidence.jpegBase64, "base64"), exhaustionNowMs);
71062
+ }
71063
+ const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => {
71064
+ const evidenceCrop = this.birthEvidence.get(key, id, exhaustionNowMs);
71065
+ return {
71066
+ trackId: id,
71067
+ className: t.className,
71068
+ bbox: t.bbox,
71069
+ ...t.nativeRecovery !== void 0 ? { nativeRecovery: t.nativeRecovery } : {},
71070
+ ...evidenceCrop !== null ? { evidenceCrop } : {}
71071
+ };
71072
+ }), {
70715
71073
  deviceId,
70716
71074
  source,
70717
71075
  frameHandle,
@@ -70735,6 +71093,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70735
71093
  const deferredForMs = this.deferredBirths.elapsedMs(key, id, gateNowMs);
70736
71094
  const deferredAttempts = this.deferredBirths.attemptsFor(key, id);
70737
71095
  this.deferredBirths.resolve(key, id);
71096
+ this.birthEvidence.resolve(key, id);
71097
+ if (this.startedBirths.hasStarted(key, id)) {
71098
+ this.deferredBirths.resolve(key, id);
71099
+ log.info("late birth verdict ignored — the track was already born", { meta: {
71100
+ trackId: id,
71101
+ className: t.className,
71102
+ source,
71103
+ verdict: outcome.confirmed.has(id) ? "confirmed" : "suppressed"
71104
+ } });
71105
+ continue;
71106
+ }
70738
71107
  if (!outcome.confirmed.has(id)) {
70739
71108
  this.suppressedBirths.reject(key, id);
70740
71109
  this.trackStore?.dropActive(id);
@@ -70772,6 +71141,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70772
71141
  })));
70773
71142
  this.failureReport.notePersonBirth(deviceId, overlap === null ? void 0 : REASON_PERSON_OVER_VEHICLE);
70774
71143
  }
71144
+ this.startedBirths.markStarted(key, id);
70775
71145
  log.info("track started", { meta: {
70776
71146
  trackId: id,
70777
71147
  className: t.className,
@@ -70839,7 +71209,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70839
71209
  });
70840
71210
  }
70841
71211
  this.suppressedBirths.retain(key, currentTrackIds);
71212
+ this.startedBirths.retain(key, currentTrackIds);
70842
71213
  this.deferredBirths.retain(key, currentTrackIds);
71214
+ this.birthEvidence.retain(key, currentTrackIds);
70843
71215
  this.lastActiveTrackIds.set(key, currentTrackIds);
70844
71216
  if (currentTrackIds.size > 0) this.lastTrackActivityMs.set(deviceId, Date.now());
70845
71217
  const stationarySettings = this.stationarySettingsFromCache(deviceId);
@@ -71418,6 +71790,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71418
71790
  return allConfirmed();
71419
71791
  }
71420
71792
  const nodeId = frameHandle.nodeId;
71793
+ const candidateNativeRecovery = /* @__PURE__ */ new Map();
71794
+ for (const c of candidates) if (c.nativeRecovery !== void 0) candidateNativeRecovery.set(c.trackId, c.nativeRecovery);
71421
71795
  return confirmBirths(candidates, config, {
71422
71796
  fetchCrop: (candidate) => this.captureScheduler.request({
71423
71797
  deviceId,
@@ -71467,6 +71841,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71467
71841
  bestIncompatibleScore: decision.bestIncompatibleScore
71468
71842
  } : {},
71469
71843
  minConfidence: config.minConfidence,
71844
+ ...decision.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
71845
+ ...decision.reason === "native-pass" ? {
71846
+ nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
71847
+ nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
71848
+ } : {},
71470
71849
  ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
71471
71850
  appliedMinConfidence: decision.appliedMinConfidence,
71472
71851
  barRaisedByPhantomCell: true
@@ -73728,6 +74107,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73728
74107
  }
73729
74108
  });
73730
74109
  });
74110
+ p.setSpawnRefusalObserver((record) => {
74111
+ this.ctx.logger.info("tracker: detection dropped — no track was born from it", {
74112
+ tags: { deviceId },
74113
+ meta: {
74114
+ source,
74115
+ ...record
74116
+ }
74117
+ });
74118
+ });
73731
74119
  p.setTrackLabelState((trackId) => {
73732
74120
  const active = this.trackStore?.getActiveByTrack(trackId);
73733
74121
  return active !== null && active !== void 0 ? {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Gc-d7G7c.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DAc8SUDf.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }