@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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CJKdyCag.js");
5
+ const require_dist = require("../dist-DMnyp081.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -34017,6 +34017,18 @@ var LooseBindSampler = class {
34017
34017
  this.counts.clear();
34018
34018
  }
34019
34019
  };
34020
+ /** Per-reason counter + sampling decision. One instance per tracker, so the
34021
+ * counts are per camera and per detection source. */
34022
+ var SpawnRefusalSampler = class {
34023
+ counts = /* @__PURE__ */ new Map();
34024
+ /** The running count when this refusal should be reported, else `undefined`. */
34025
+ next(reason) {
34026
+ const count = (this.counts.get(reason) ?? 0) + 1;
34027
+ this.counts.set(reason, count);
34028
+ if (count <= 10) return count;
34029
+ return count % 10 === 0 ? count : void 0;
34030
+ }
34031
+ };
34020
34032
  //#endregion
34021
34033
  //#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
34022
34034
  var DEFAULT_TRACKER_CONFIG = {
@@ -34101,6 +34113,10 @@ var SortTracker = class SortTracker {
34101
34113
  /** Sink for salvage-bind diagnostics. Null until an owner that knows the
34102
34114
  * `deviceId` wires one — see {@link setBindObserver}. */
34103
34115
  bindObserver = null;
34116
+ /** Per-reason counters behind the sampled dropped-detection diagnostics. */
34117
+ refusalSampler = new SpawnRefusalSampler();
34118
+ /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34119
+ spawnRefusalObserver = null;
34104
34120
  constructor(config = {}) {
34105
34121
  this.config = {
34106
34122
  ...DEFAULT_TRACKER_CONFIG,
@@ -34116,6 +34132,37 @@ var SortTracker = class SortTracker {
34116
34132
  setBindObserver(observer) {
34117
34133
  this.bindObserver = observer;
34118
34134
  }
34135
+ /**
34136
+ * Wire the diagnostics sink for detections the tracker DROPS — a spawn a
34137
+ * gate refused, or a box a track of another class absorbed. Same contract as
34138
+ * {@link setBindObserver}: the tracker has no `deviceId`, the caller stamps
34139
+ * it. Nothing is computed when no observer is wired.
34140
+ */
34141
+ setSpawnRefusalObserver(observer) {
34142
+ this.spawnRefusalObserver = observer;
34143
+ }
34144
+ /** Emit one dropped-detection record, subject to the refusal sampler. */
34145
+ reportRefusal(reason, det, extra = {}) {
34146
+ const observer = this.spawnRefusalObserver;
34147
+ if (!observer) return;
34148
+ const refusalCount = this.refusalSampler.next(reason);
34149
+ if (refusalCount === void 0) return;
34150
+ observer({
34151
+ reason,
34152
+ detClass: det.class,
34153
+ detScore: det.score,
34154
+ bbox: det.bbox,
34155
+ ...det.nativeRecovery !== void 0 ? { nativeRecoveryScore: det.nativeRecovery.score } : {},
34156
+ ...extra.threshold !== void 0 ? { threshold: extra.threshold } : {},
34157
+ ...extra.measured !== void 0 ? { measured: extra.measured } : {},
34158
+ ...extra.against !== void 0 ? {
34159
+ againstTrackId: extra.against.id,
34160
+ againstTrackClass: extra.against.class
34161
+ } : {},
34162
+ refusalCount,
34163
+ sampledEvery: 10
34164
+ });
34165
+ }
34119
34166
  /** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
34120
34167
  * Nothing is computed when no observer is wired. */
34121
34168
  reportBind(stage, gate, track, det, timestamp) {
@@ -34175,9 +34222,49 @@ var SortTracker = class SortTracker {
34175
34222
  * exact class when `classGating` is off (no gate at all) — matching the legacy
34176
34223
  * contract — otherwise same association group (group == exact class unless
34177
34224
  * `classGroupAssoc` widened it to {person,animal}). */
34178
- associable(trackClass, detClass) {
34225
+ associable(trackClass, det) {
34226
+ if (!this.sameGroupAsMeasured(trackClass, det)) return false;
34179
34227
  if (!this.config.classGating) return true;
34180
- return this.assocGroup(trackClass) === this.assocGroup(detClass);
34228
+ return this.assocGroup(trackClass) === this.assocGroup(det.class);
34229
+ }
34230
+ /**
34231
+ * A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378).
34232
+ *
34233
+ * `classGroupAssoc` collapses {person, animal} into one association group for
34234
+ * one failure of the COARSE full-frame pass: a bending person re-labelled
34235
+ * `animal` for a frame, over the person's own box. Letting that box re-match
34236
+ * the person track is strictly better than minting a concurrent id.
34237
+ *
34238
+ * The runner's second pass makes the opposite measurement — the same detector
34239
+ * on a 640-800 px native square around a motion region the full-frame boxes
34240
+ * do NOT account for. Such a box is, by construction, not the subject the
34241
+ * full pass already has; absorbing it into a person track means the animal is
34242
+ * never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
34243
+ * an `animal` at `nativeRecoveryScore=0.833`.
34244
+ *
34245
+ * So the widening does not apply to a detection carrying `nativeRecovery`:
34246
+ * it associates to its OWN class or it spawns. Every other guard on that
34247
+ * spawn still runs — the per-class score floor, the area floor, the
34248
+ * `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
34249
+ * still beats a cross-class animal.
34250
+ */
34251
+ sameGroupAsMeasured(trackClass, det) {
34252
+ if (det.nativeRecovery === void 0) return true;
34253
+ return trackClass === det.class;
34254
+ }
34255
+ /**
34256
+ * Whether a matched track's box refuses `det` a birth. The gate exists for a
34257
+ * detector double-firing INSIDE a subject it already has, so it is a
34258
+ * same-subject claim: for a box the native pass measured (D378), a
34259
+ * cross-class occluder is not that claim — a dog at its owner's knee is 100%
34260
+ * contained in the person's box and is not the person. 2026-09-07 07:56:37,
34261
+ * an `animal` at `nativeRecoveryScore=0.5143` refused on `threshold=0.6`.
34262
+ * A SAME-class occluder still refuses it: that is the torso the crop cut out
34263
+ * of a person the full pass already has.
34264
+ */
34265
+ occludesSpawn(det, occluder) {
34266
+ if (!this.sameGroupAsMeasured(occluder.class, det)) return false;
34267
+ return containment(det.bbox, occluder.bbox) >= this.config.occlusionContainment;
34181
34268
  }
34182
34269
  /** Whether a spawn candidate clears its per-class minimum bbox-area floor
34183
34270
  * (fraction of frame area). Ungated when disabled, when the class has no floor,
@@ -34199,14 +34286,30 @@ var SortTracker = class SortTracker {
34199
34286
  isDuplicateSpawn(det, live, timestamp) {
34200
34287
  for (const t of live) {
34201
34288
  if (!this.dedupCompatible(t.class, det.class)) continue;
34202
- if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
34289
+ const overlap = iou$3(det.bbox, t.bbox);
34290
+ if (overlap >= this.config.dedupSpawnIou) {
34291
+ this.reportRefusal("dedup-iou", det, {
34292
+ threshold: this.config.dedupSpawnIou,
34293
+ measured: overlap,
34294
+ against: t
34295
+ });
34296
+ return true;
34297
+ }
34203
34298
  if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
34204
34299
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
34205
34300
  return true;
34206
34301
  }
34207
34302
  }
34208
- if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
34209
- for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
34303
+ if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.nativeRecovery === void 0 && det.score < this.config.animalOverPersonMaxScore) for (const t of live) {
34304
+ const overlap = iou$3(det.bbox, t.bbox);
34305
+ if (t.class === PERSON_CLASS && overlap >= this.config.animalOverPersonIou) {
34306
+ this.reportRefusal("animal-over-person", det, {
34307
+ threshold: this.config.animalOverPersonMaxScore,
34308
+ measured: overlap,
34309
+ against: t
34310
+ });
34311
+ return true;
34312
+ }
34210
34313
  }
34211
34314
  return false;
34212
34315
  }
@@ -34321,6 +34424,7 @@ var SortTracker = class SortTracker {
34321
34424
  * class.
34322
34425
  */
34323
34426
  looseMatch(track, det, timestamp) {
34427
+ if (!this.sameGroupAsMeasured(track.class, det)) return null;
34324
34428
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34325
34429
  if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34326
34430
  const tc = bboxCentroid(track.bbox);
@@ -34407,7 +34511,7 @@ var SortTracker = class SortTracker {
34407
34511
  for (const di of candidateIdxs) {
34408
34512
  if (used.has(di)) continue;
34409
34513
  const det = detections[di];
34410
- if (!this.associable(track.class, det.class)) continue;
34514
+ if (!this.associable(track.class, det)) continue;
34411
34515
  const score = iou$3(pbox, det.bbox);
34412
34516
  if (score >= iouThreshold) pairs.push({
34413
34517
  track,
@@ -34486,6 +34590,7 @@ var SortTracker = class SortTracker {
34486
34590
  }
34487
34591
  if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
34488
34592
  for (const [track, det] of matched) {
34593
+ if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
34489
34594
  const prevCenter = bboxCentroid({
34490
34595
  x: track.bbox.x,
34491
34596
  y: track.bbox.y,
@@ -34514,15 +34619,18 @@ var SortTracker = class SortTracker {
34514
34619
  if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
34515
34620
  this.addClassVote(track, det);
34516
34621
  }
34517
- const occluderBoxes = [];
34518
- for (const track of matchedTracks) occluderBoxes.push(track.bbox);
34622
+ const occluders = [];
34623
+ for (const track of matchedTracks) occluders.push({
34624
+ bbox: track.bbox,
34625
+ class: track.class
34626
+ });
34519
34627
  const surviving = [];
34520
34628
  for (const track of this.tracks) {
34521
34629
  if (matchedTracks.has(track)) {
34522
34630
  surviving.push(track);
34523
34631
  continue;
34524
34632
  }
34525
- const occluded = this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(this.predicted(track), ob) >= this.config.occlusionContainment);
34633
+ const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track), ob.bbox) >= this.config.occlusionContainment);
34526
34634
  track.age++;
34527
34635
  if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
34528
34636
  track.lost = true;
@@ -34601,14 +34709,25 @@ var SortTracker = class SortTracker {
34601
34709
  for (const di of unmatchedIdx) {
34602
34710
  const det = detections[di];
34603
34711
  if (!this.meetsClassScoreFloor(det)) {
34712
+ this.reportRefusal("score-floor", det, {
34713
+ threshold: this.config.classMinScores[det.class] ?? 0,
34714
+ measured: det.score
34715
+ });
34604
34716
  used.add(di);
34605
34717
  continue;
34606
34718
  }
34607
34719
  if (!this.meetsSpawnAreaFloor(det, frameArea)) {
34720
+ this.reportRefusal("area-floor", det, {
34721
+ threshold: this.config.classSpawnAreaFracs[det.class] ?? 0,
34722
+ measured: frameArea > 0 ? det.bbox.w * det.bbox.h / frameArea : 0
34723
+ });
34608
34724
  used.add(di);
34609
34725
  continue;
34610
34726
  }
34611
- if (this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(det.bbox, ob) >= this.config.occlusionContainment)) continue;
34727
+ if (this.config.occlusionEnabled && occluders.some((ob) => this.occludesSpawn(det, ob))) {
34728
+ this.reportRefusal("occlusion", det, { threshold: this.config.occlusionContainment });
34729
+ continue;
34730
+ }
34612
34731
  if (this.config.dedupEnabled && this.isDuplicateSpawn(det, surviving, timestamp)) {
34613
34732
  used.add(di);
34614
34733
  continue;
@@ -35096,6 +35215,9 @@ var FrameProcessor = class {
35096
35215
  * Only the rescue / resurrection / growth branches report, and they report
35097
35216
  * sampled: stage 1 runs per track per frame at frame-rate.
35098
35217
  */
35218
+ setSpawnRefusalObserver(observer) {
35219
+ this.tracker.setSpawnRefusalObserver(observer);
35220
+ }
35099
35221
  setBindObserver(observer) {
35100
35222
  this.tracker.setBindObserver(observer);
35101
35223
  }
@@ -35118,6 +35240,8 @@ var FrameProcessor = class {
35118
35240
  const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
35119
35241
  const plateByBbox = /* @__PURE__ */ new Map();
35120
35242
  const maskByBbox = /* @__PURE__ */ new Map();
35243
+ const nativeRecoveryByBbox = /* @__PURE__ */ new Map();
35244
+ const birthEvidenceByBbox = /* @__PURE__ */ new Map();
35121
35245
  const flatDetections = frame.detections.filter((d) => d.kind === "first-level").map((det) => {
35122
35246
  const bbox = {
35123
35247
  x: det.bbox.x,
@@ -35129,8 +35253,11 @@ var FrameProcessor = class {
35129
35253
  class: det.macroClass,
35130
35254
  originalClass: det.debug?.originalClass ?? det.macroClass,
35131
35255
  score: det.score,
35132
- bbox
35256
+ bbox,
35257
+ ...det.nativeRecovery !== void 0 ? { nativeRecovery: det.nativeRecovery } : {}
35133
35258
  };
35259
+ if (det.nativeRecovery !== void 0) nativeRecoveryByBbox.set(bbox, det.nativeRecovery);
35260
+ if (det.birthEvidence !== void 0) birthEvidenceByBbox.set(bbox, det.birthEvidence);
35134
35261
  if (det.labels.length > 0) labelsByBbox.set(bbox, det.labels);
35135
35262
  if (det.debug?.originalClassTier !== void 0) originalClassTierByBbox.set(bbox, det.debug.originalClassTier);
35136
35263
  if (det.embedding !== void 0) embeddingByBbox.set(bbox, {
@@ -35355,6 +35482,8 @@ var FrameProcessor = class {
35355
35482
  const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
35356
35483
  const plate = plateByBbox.get(td.bbox);
35357
35484
  const sourceDetectionId = sourceIdByBbox.get(td.bbox);
35485
+ const nativeRecovery = td.matchedThisFrame !== false ? nativeRecoveryByBbox.get(td.bbox) : void 0;
35486
+ const birthEvidence = td.matchedThisFrame !== false ? birthEvidenceByBbox.get(td.bbox) : void 0;
35358
35487
  return {
35359
35488
  trackId: td.trackId,
35360
35489
  ...sourceDetectionId !== void 0 ? { sourceDetectionId } : {},
@@ -35377,7 +35506,9 @@ var FrameProcessor = class {
35377
35506
  plateText: plate.text,
35378
35507
  plateScore: plate.score,
35379
35508
  plateBbox: plate.bbox
35380
- } : {}
35509
+ } : {},
35510
+ ...nativeRecovery !== void 0 ? { nativeRecovery } : {},
35511
+ ...birthEvidence !== void 0 ? { birthEvidence } : {}
35381
35512
  };
35382
35513
  });
35383
35514
  this.lastLabelWriteRefusals = refusals;
@@ -45228,9 +45359,28 @@ function withTimeout$2(promise, timeoutMs) {
45228
45359
  });
45229
45360
  }
45230
45361
  async function runConfirmation(candidate, config, deps, exhausted) {
45362
+ const carried = candidate.evidenceCrop ?? null;
45363
+ if (carried) {
45364
+ const verdict = await measure(candidate, carried, config, deps, "carried");
45365
+ if (verdict.verdict === "confirmed" || verdict.reason === "redetect-error") return verdict;
45366
+ }
45231
45367
  let crop = await deps.fetchCrop(candidate);
45232
- if (!crop && exhausted && deps.fetchFallbackCrop) crop = await deps.fetchFallbackCrop(candidate);
45368
+ let cropSource = "native";
45369
+ if (!crop && exhausted && deps.fetchFallbackCrop) {
45370
+ crop = await deps.fetchFallbackCrop(candidate);
45371
+ cropSource = "fallback";
45372
+ }
45233
45373
  if (!crop) return undecided(candidate, "no-crop");
45374
+ return measure(candidate, crop, config, deps, cropSource);
45375
+ }
45376
+ /**
45377
+ * Re-detect on ONE crop and turn the result into a verdict.
45378
+ *
45379
+ * Shared by all three sources so a crop can never be judged by two slightly
45380
+ * different rules depending on where it came from — the `cropSource` argument
45381
+ * is the ONLY thing that varies, and it varies in exactly one place below.
45382
+ */
45383
+ async function measure(candidate, crop, config, deps, cropSource) {
45234
45384
  const detections = await deps.redetect(crop);
45235
45385
  if (detections === null) return undecided(candidate, "redetect-error");
45236
45386
  let best;
@@ -45238,8 +45388,9 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45238
45388
  for (const d of detections) if (isConfirmationCompatible(candidate.className, d.macroClass)) {
45239
45389
  if (!best || d.score > best.score) best = d;
45240
45390
  } else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
45241
- const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45391
+ const bar = barFor(candidate, config, deps);
45242
45392
  const confirmed = best !== void 0 && best.score >= bar;
45393
+ if (!confirmed && cropSource !== "native") return undecided(candidate, cropSource === "fallback" ? "fallback-inconclusive" : "carried-inconclusive");
45243
45394
  const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
45244
45395
  return {
45245
45396
  trackId: candidate.trackId,
@@ -45247,6 +45398,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45247
45398
  reason: confirmed ? "confirmed" : "suppressed",
45248
45399
  className: candidate.className,
45249
45400
  appliedMinConfidence: bar,
45401
+ cropSource,
45250
45402
  ...best ? { bestScore: best.score } : {},
45251
45403
  ...bestIncompatible ? {
45252
45404
  bestIncompatibleClass: bestIncompatible.macroClass,
@@ -45277,7 +45429,27 @@ function promoteSecondary(candidate, detection, bar, deps) {
45277
45429
  }
45278
45430
  };
45279
45431
  }
45432
+ /**
45433
+ * The bar this candidate faces. Never below the configured one: the
45434
+ * `minConfidenceFor` hook is monotonic by construction, and clamping here says
45435
+ * so at the point of use rather than trusting the caller to have been careful.
45436
+ */
45437
+ function barFor(candidate, config, deps) {
45438
+ return Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45439
+ }
45280
45440
  async function confirmOne(candidate, config, deps, exhausted) {
45441
+ const native = candidate.nativeRecovery;
45442
+ if (native !== void 0) {
45443
+ const bar = barFor(candidate, config, deps);
45444
+ if (native.score >= bar) return {
45445
+ trackId: candidate.trackId,
45446
+ verdict: "confirmed",
45447
+ reason: "native-pass",
45448
+ className: candidate.className,
45449
+ bestScore: native.score,
45450
+ appliedMinConfidence: bar
45451
+ };
45452
+ }
45281
45453
  const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
45282
45454
  if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
45283
45455
  try {
@@ -51739,6 +51911,97 @@ async function normalizePhotoOrientation(photo) {
51739
51911
  if (orientation === void 0 || orientation === EXIF_UPRIGHT) return photo;
51740
51912
  return (0, sharp.default)(photo).rotate().jpeg({ quality: 95 }).toBuffer();
51741
51913
  }
51914
+ /**
51915
+ * How long one birth's evidence is kept. Twice `confirmationGateMaxDeferralMs`
51916
+ * (2 000 ms default) — the longest a deferred birth can still be re-offered.
51917
+ */
51918
+ var BIRTH_EVIDENCE_TTL_MS = 4e3;
51919
+ var BirthEvidenceStore = class {
51920
+ slotsPerCamera;
51921
+ ttlMs;
51922
+ byDevice = /* @__PURE__ */ new Map();
51923
+ constructor(slotsPerCamera = 8, ttlMs = BIRTH_EVIDENCE_TTL_MS) {
51924
+ this.slotsPerCamera = slotsPerCamera;
51925
+ this.ttlMs = ttlMs;
51926
+ }
51927
+ /**
51928
+ * Keep this birth's crop.
51929
+ *
51930
+ * The FIRST crop for an id wins. A later frame's crop of the same track is a
51931
+ * different instant, and this store exists precisely to preserve the instant
51932
+ * the birth was measured on — overwriting it would quietly reintroduce the
51933
+ * defect the fallback asymmetry exists to distrust.
51934
+ */
51935
+ remember(deviceKey, trackId, crop, nowMs) {
51936
+ let slots = this.byDevice.get(deviceKey);
51937
+ if (!slots) {
51938
+ slots = /* @__PURE__ */ new Map();
51939
+ this.byDevice.set(deviceKey, slots);
51940
+ }
51941
+ if (slots.has(trackId)) return;
51942
+ slots.set(trackId, {
51943
+ crop,
51944
+ atMs: nowMs
51945
+ });
51946
+ this.evict(deviceKey, slots, nowMs);
51947
+ }
51948
+ /** This birth's crop, or `null` when it was never kept or has aged out. */
51949
+ get(deviceKey, trackId, nowMs) {
51950
+ const entry = this.byDevice.get(deviceKey)?.get(trackId);
51951
+ if (!entry) return null;
51952
+ if (nowMs - entry.atMs > this.ttlMs) {
51953
+ this.resolve(deviceKey, trackId);
51954
+ return null;
51955
+ }
51956
+ return entry.crop;
51957
+ }
51958
+ /** A verdict finally arrived — the crop is dead weight from here. */
51959
+ resolve(deviceKey, trackId) {
51960
+ const slots = this.byDevice.get(deviceKey);
51961
+ if (!slots) return;
51962
+ slots.delete(trackId);
51963
+ if (slots.size === 0) this.byDevice.delete(deviceKey);
51964
+ }
51965
+ /**
51966
+ * Forget every id the tracker is no longer carrying — the same per-frame
51967
+ * contract as `DeferredBirthRegistry.retain`. An id the tracker dropped can
51968
+ * never be decided, so holding its pixels only leaks.
51969
+ */
51970
+ retain(deviceKey, currentTrackIds) {
51971
+ const slots = this.byDevice.get(deviceKey);
51972
+ if (!slots) return;
51973
+ for (const id of [...slots.keys()]) if (!currentTrackIds.has(id)) slots.delete(id);
51974
+ if (slots.size === 0) this.byDevice.delete(deviceKey);
51975
+ }
51976
+ /** Crops held for one camera (diagnostics and tests). */
51977
+ size(deviceKey) {
51978
+ return this.byDevice.get(deviceKey)?.size ?? 0;
51979
+ }
51980
+ /** Bytes held across every camera — the declared budget, checkable. */
51981
+ totalBytes() {
51982
+ let total = 0;
51983
+ for (const slots of this.byDevice.values()) for (const entry of slots.values()) total += entry.crop.byteLength;
51984
+ return total;
51985
+ }
51986
+ /** Drop a camera's reserve wholesale (device removed / pipeline reset). */
51987
+ clearDevice(deviceKey) {
51988
+ this.byDevice.delete(deviceKey);
51989
+ }
51990
+ /**
51991
+ * Age first, then the quota — and only ever inside THIS camera's slots.
51992
+ * `Map` keeps insertion order and a crop is inserted once, so the first key
51993
+ * is always the oldest.
51994
+ */
51995
+ evict(deviceKey, slots, nowMs) {
51996
+ for (const [id, entry] of [...slots]) if (nowMs - entry.atMs > this.ttlMs) slots.delete(id);
51997
+ while (slots.size > this.slotsPerCamera) {
51998
+ const oldest = slots.keys().next().value;
51999
+ if (oldest === void 0) break;
52000
+ slots.delete(oldest);
52001
+ }
52002
+ if (slots.size === 0) this.byDevice.delete(deviceKey);
52003
+ }
52004
+ };
51742
52005
  //#endregion
51743
52006
  //#region src/pipeline-analytics/pipeline/deferred-births.ts
51744
52007
  var DeferredBirthRegistry = class {
@@ -53895,6 +54158,77 @@ function personOverVehicle(person, neighbours, threshold = CONTAINMENT_THRESHOLD
53895
54158
  return best >= threshold ? best : null;
53896
54159
  }
53897
54160
  //#endregion
54161
+ //#region src/pipeline-analytics/pipeline/started-births.ts
54162
+ /**
54163
+ * Memory of births whose ritual has already RUN, per device+source.
54164
+ *
54165
+ * A birth is a decision, and a decision is taken once. Until 2026-09-07 it
54166
+ * could be taken twice, on camera 615 it routinely was, and the second vote
54167
+ * won:
54168
+ *
54169
+ * 04:59:38.335Z confirmation gate: birth confirmed {trackId=f585b2ee…
54170
+ * className=animal bestScore=0.7068}
54171
+ * 04:59:38.335Z track started {trackId=f585b2ee…}
54172
+ * 04:59:38.821Z confirmation gate: birth suppressed (false positive)
54173
+ * {trackId=f585b2ee… className=animal} ← no bestScore:
54174
+ * the second crop held no compatible detection AT ALL
54175
+ * 04:59:38.821Z birth suppressed — track record retracted
54176
+ *
54177
+ * TWO conditions have to meet, and both are structural:
54178
+ *
54179
+ * 1. `processFrame` reads `lastActiveTrackIds` at the top and writes it at the
54180
+ * bottom, and AWAITS the confirmation gate in between (a crop round trip to
54181
+ * the frame's node plus a re-detection: 130–490 ms measured on this camera,
54182
+ * against 100 ms frame spacing). Several frames are therefore in flight at
54183
+ * once and each of them sees the same fresh id as unjudged.
54184
+ * 2. The other birth discriminator, `positionsCount > 1`, does not close the
54185
+ * gap either: a COASTED frame deliberately appends no position
54186
+ * (`TrackStore.upsert`, `matched: false`), so a track the full-frame pass
54187
+ * cannot see — which is the entire premise of the small-subject recovery —
54188
+ * keeps exactly one position for as long as it coasts, and
54189
+ * `classifyTrackAppearance` keeps answering `birth`.
54190
+ *
54191
+ * So the id that has already been born is remembered HERE, and both halves of
54192
+ * the birth path consult it: it is not offered to the gate again, and a verdict
54193
+ * that arrives for it anyway cannot retract it. Same lifetime discipline as
54194
+ * {@link SuppressedBirthRegistry} — held while the tracker still carries the
54195
+ * id, forgotten the frame it stops.
54196
+ */
54197
+ var StartedBirthRegistry = class {
54198
+ byDevice = /* @__PURE__ */ new Map();
54199
+ /** Record that this id's birth ritual ran — `start`, firstFrame, the lot. */
54200
+ markStarted(deviceKey, trackId) {
54201
+ let ids = this.byDevice.get(deviceKey);
54202
+ if (!ids) {
54203
+ ids = /* @__PURE__ */ new Set();
54204
+ this.byDevice.set(deviceKey, ids);
54205
+ }
54206
+ ids.add(trackId);
54207
+ }
54208
+ hasStarted(deviceKey, trackId) {
54209
+ return this.byDevice.get(deviceKey)?.has(trackId) === true;
54210
+ }
54211
+ /**
54212
+ * Forget every started id the tracker is no longer carrying. Called once per
54213
+ * frame with the ids present THIS frame — an id the tracker has dropped can
54214
+ * never be re-offered as a birth, so holding it serves nothing.
54215
+ */
54216
+ retain(deviceKey, currentTrackIds) {
54217
+ const ids = this.byDevice.get(deviceKey);
54218
+ if (!ids) return;
54219
+ for (const id of ids) if (!currentTrackIds.has(id)) ids.delete(id);
54220
+ if (ids.size === 0) this.byDevice.delete(deviceKey);
54221
+ }
54222
+ /** Started ids currently held for a device — diagnostics and tests. */
54223
+ size(deviceKey) {
54224
+ return this.byDevice.get(deviceKey)?.size ?? 0;
54225
+ }
54226
+ /** Drop a device's memory wholesale (device removed / pipeline reset). */
54227
+ clearDevice(deviceKey) {
54228
+ this.byDevice.delete(deviceKey);
54229
+ }
54230
+ };
54231
+ //#endregion
53898
54232
  //#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
53899
54233
  /**
53900
54234
  * stationary-evidence — how a parked object EARNS a registry entry.
@@ -67527,9 +67861,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
67527
67861
  /** See `pipeline/suppressed-births.ts` — rejected births must not be
67528
67862
  * re-upserted, and must be forgotten when the tracker drops the id. */
67529
67863
  suppressedBirths = new SuppressedBirthRegistry();
67864
+ /** Ids whose birth ritual has already run — a birth is decided ONCE (D377). */
67865
+ startedBirths = new StartedBirthRegistry();
67530
67866
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
67531
67867
  deferredBirths = new DeferredBirthRegistry();
67532
67868
  /**
67869
+ * The pixels each deferred birth arrived with — a RESERVE per camera, evicted
67870
+ * by age inside that camera's own quota (D379).
67871
+ *
67872
+ * Not a shared cap. The path this replaces was one node-wide 4 096-entry LRU
67873
+ * measured pinned full in 6 814 of 6 849 samples over three hours, in which a
67874
+ * busy camera evicted a quiet one's proof and every camera added shortened
67875
+ * every other camera's window. See `pipeline/birth-evidence-store.ts` for the
67876
+ * two measurements the slot count and the TTL are taken from.
67877
+ */
67878
+ birthEvidence = new BirthEvidenceStore();
67879
+ /**
67533
67880
  * Subjects the confirmation gate found in a birth's crop BESIDES the birth
67534
67881
  * itself (a dog in a person's crop), waiting to ride the next frame of the
67535
67882
  * same (device, source) as detections — `pipeline/secondary-subjects.ts`.
@@ -70745,10 +71092,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70745
71092
  if (prevIds.has(id)) continue;
70746
71093
  const t = result.tracked.find((x) => x.trackId === id);
70747
71094
  if (!t) continue;
70748
- if (classifyTrackAppearance({
71095
+ const appearance = classifyTrackAppearance({
70749
71096
  inPrevActive: false,
70750
71097
  positionsCount: positionsCountById.get(id) ?? 1
70751
- }) === "resurrection") {
71098
+ });
71099
+ if (this.startedBirths.hasStarted(key, id)) continue;
71100
+ if (appearance === "resurrection") {
70752
71101
  log.info("track resumed", {
70753
71102
  tags: { deviceId },
70754
71103
  meta: {
@@ -70781,11 +71130,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70781
71130
  const exhaustionNowMs = Date.now();
70782
71131
  const exhaustedIds = /* @__PURE__ */ new Set();
70783
71132
  for (const { id } of bornCandidates) if (this.deferredBirths.exhausted(key, id, exhaustionNowMs, gateSettings.maxAttempts, gateSettings.maxDeferralMs)) exhaustedIds.add(id);
70784
- const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
70785
- trackId: id,
70786
- className: t.className,
70787
- bbox: t.bbox
70788
- })), {
71133
+ for (const { id, t } of bornCandidates) {
71134
+ if (t.birthEvidence === void 0) continue;
71135
+ this.birthEvidence.remember(key, id, Buffer.from(t.birthEvidence.jpegBase64, "base64"), exhaustionNowMs);
71136
+ }
71137
+ const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => {
71138
+ const evidenceCrop = this.birthEvidence.get(key, id, exhaustionNowMs);
71139
+ return {
71140
+ trackId: id,
71141
+ className: t.className,
71142
+ bbox: t.bbox,
71143
+ ...t.nativeRecovery !== void 0 ? { nativeRecovery: t.nativeRecovery } : {},
71144
+ ...evidenceCrop !== null ? { evidenceCrop } : {}
71145
+ };
71146
+ }), {
70789
71147
  deviceId,
70790
71148
  source,
70791
71149
  frameHandle,
@@ -70809,6 +71167,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70809
71167
  const deferredForMs = this.deferredBirths.elapsedMs(key, id, gateNowMs);
70810
71168
  const deferredAttempts = this.deferredBirths.attemptsFor(key, id);
70811
71169
  this.deferredBirths.resolve(key, id);
71170
+ this.birthEvidence.resolve(key, id);
71171
+ if (this.startedBirths.hasStarted(key, id)) {
71172
+ this.deferredBirths.resolve(key, id);
71173
+ log.info("late birth verdict ignored — the track was already born", { meta: {
71174
+ trackId: id,
71175
+ className: t.className,
71176
+ source,
71177
+ verdict: outcome.confirmed.has(id) ? "confirmed" : "suppressed"
71178
+ } });
71179
+ continue;
71180
+ }
70812
71181
  if (!outcome.confirmed.has(id)) {
70813
71182
  this.suppressedBirths.reject(key, id);
70814
71183
  this.trackStore?.dropActive(id);
@@ -70846,6 +71215,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70846
71215
  })));
70847
71216
  this.failureReport.notePersonBirth(deviceId, overlap === null ? void 0 : REASON_PERSON_OVER_VEHICLE);
70848
71217
  }
71218
+ this.startedBirths.markStarted(key, id);
70849
71219
  log.info("track started", { meta: {
70850
71220
  trackId: id,
70851
71221
  className: t.className,
@@ -70913,7 +71283,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70913
71283
  });
70914
71284
  }
70915
71285
  this.suppressedBirths.retain(key, currentTrackIds);
71286
+ this.startedBirths.retain(key, currentTrackIds);
70916
71287
  this.deferredBirths.retain(key, currentTrackIds);
71288
+ this.birthEvidence.retain(key, currentTrackIds);
70917
71289
  this.lastActiveTrackIds.set(key, currentTrackIds);
70918
71290
  if (currentTrackIds.size > 0) this.lastTrackActivityMs.set(deviceId, Date.now());
70919
71291
  const stationarySettings = this.stationarySettingsFromCache(deviceId);
@@ -71492,6 +71864,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71492
71864
  return allConfirmed();
71493
71865
  }
71494
71866
  const nodeId = frameHandle.nodeId;
71867
+ const candidateNativeRecovery = /* @__PURE__ */ new Map();
71868
+ for (const c of candidates) if (c.nativeRecovery !== void 0) candidateNativeRecovery.set(c.trackId, c.nativeRecovery);
71495
71869
  return confirmBirths(candidates, config, {
71496
71870
  fetchCrop: (candidate) => this.captureScheduler.request({
71497
71871
  deviceId,
@@ -71541,6 +71915,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71541
71915
  bestIncompatibleScore: decision.bestIncompatibleScore
71542
71916
  } : {},
71543
71917
  minConfidence: config.minConfidence,
71918
+ ...decision.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
71919
+ ...decision.reason === "native-pass" ? {
71920
+ nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
71921
+ nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
71922
+ } : {},
71544
71923
  ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
71545
71924
  appliedMinConfidence: decision.appliedMinConfidence,
71546
71925
  barRaisedByPhantomCell: true
@@ -73802,6 +74181,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73802
74181
  }
73803
74182
  });
73804
74183
  });
74184
+ p.setSpawnRefusalObserver((record) => {
74185
+ this.ctx.logger.info("tracker: detection dropped — no track was born from it", {
74186
+ tags: { deviceId },
74187
+ meta: {
74188
+ source,
74189
+ ...record
74190
+ }
74191
+ });
74192
+ });
73805
74193
  p.setTrackLabelState((trackId) => {
73806
74194
  const active = this.trackStore?.getActiveByTrack(trackId);
73807
74195
  return active !== null && active !== void 0 ? {