@camstack/addon-post-analysis 1.2.190 → 1.2.191

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.
@@ -33278,6 +33278,52 @@ var ClosedRowMediaGate = class {
33278
33278
  }
33279
33279
  };
33280
33280
  //#endregion
33281
+ //#region src/pipeline-analytics/pipeline/co-moving-subjects.ts
33282
+ /**
33283
+ * Report at this much containment. A rider and a bicycle overlap heavily; two
33284
+ * people passing each other in the frame do not stay contained for long.
33285
+ * Deliberately a REPORTING threshold, not a decision one — nothing branches on
33286
+ * it, so it can be loose enough to see the near misses.
33287
+ */
33288
+ var CO_MOVING_MIN_CONTAINMENT = .6;
33289
+ function intersectionArea$4(a, b) {
33290
+ return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
33291
+ }
33292
+ /**
33293
+ * The overlaps a NEWLY born track has with live tracks of a DIFFERENT class.
33294
+ *
33295
+ * Same-class overlaps are excluded: those are the tracker's own duplicate
33296
+ * problem and are already measured elsewhere. A zero-area box reports nothing
33297
+ * rather than dividing by zero.
33298
+ */
33299
+ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMENT) {
33300
+ const out = [];
33301
+ for (const b of born) {
33302
+ const areaB = b.bbox.w * b.bbox.h;
33303
+ if (areaB <= 0) continue;
33304
+ for (const o of live) {
33305
+ if (o.trackId === b.trackId) continue;
33306
+ if (o.className === b.className) continue;
33307
+ const areaO = o.bbox.w * o.bbox.h;
33308
+ if (areaO <= 0) continue;
33309
+ const inter = intersectionArea$4(b.bbox, o.bbox);
33310
+ if (inter <= 0) continue;
33311
+ const union = areaB + areaO - inter;
33312
+ const containment = inter / Math.min(areaB, areaO);
33313
+ if (containment < minContainment) continue;
33314
+ out.push({
33315
+ trackId: b.trackId,
33316
+ className: b.className,
33317
+ otherTrackId: o.trackId,
33318
+ otherClassName: o.className,
33319
+ iou: union > 0 ? inter / union : 0,
33320
+ containment
33321
+ });
33322
+ }
33323
+ }
33324
+ return out;
33325
+ }
33326
+ //#endregion
33281
33327
  //#region src/pipeline-analytics/pipeline/edge-clear.ts
33282
33328
  /**
33283
33329
  * edge-clear — pure geometry for "is the subject fully in frame?" best-frame
@@ -33498,52 +33544,6 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
33498
33544
  return false;
33499
33545
  }
33500
33546
  //#endregion
33501
- //#region src/pipeline-analytics/pipeline/co-moving-subjects.ts
33502
- /**
33503
- * Report at this much containment. A rider and a bicycle overlap heavily; two
33504
- * people passing each other in the frame do not stay contained for long.
33505
- * Deliberately a REPORTING threshold, not a decision one — nothing branches on
33506
- * it, so it can be loose enough to see the near misses.
33507
- */
33508
- var CO_MOVING_MIN_CONTAINMENT = .6;
33509
- function intersectionArea$4(a, b) {
33510
- return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
33511
- }
33512
- /**
33513
- * The overlaps a NEWLY born track has with live tracks of a DIFFERENT class.
33514
- *
33515
- * Same-class overlaps are excluded: those are the tracker's own duplicate
33516
- * problem and are already measured elsewhere. A zero-area box reports nothing
33517
- * rather than dividing by zero.
33518
- */
33519
- function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMENT) {
33520
- const out = [];
33521
- for (const b of born) {
33522
- const areaB = b.bbox.w * b.bbox.h;
33523
- if (areaB <= 0) continue;
33524
- for (const o of live) {
33525
- if (o.trackId === b.trackId) continue;
33526
- if (o.className === b.className) continue;
33527
- const areaO = o.bbox.w * o.bbox.h;
33528
- if (areaO <= 0) continue;
33529
- const inter = intersectionArea$4(b.bbox, o.bbox);
33530
- if (inter <= 0) continue;
33531
- const union = areaB + areaO - inter;
33532
- const containment = inter / Math.min(areaB, areaO);
33533
- if (containment < minContainment) continue;
33534
- out.push({
33535
- trackId: b.trackId,
33536
- className: b.className,
33537
- otherTrackId: o.trackId,
33538
- otherClassName: o.className,
33539
- iou: union > 0 ? inter / union : 0,
33540
- containment
33541
- });
33542
- }
33543
- }
33544
- return out;
33545
- }
33546
- //#endregion
33547
33547
  //#region src/pipeline-analytics/pipeline/first-frame-retry-bbox.ts
33548
33548
  /** Crop box for a late firstFrame retry: the BIRTH geometry, not the
33549
33549
  * later frozen/coasted box (track 1b659d56). */
@@ -34067,7 +34067,7 @@ var MAX_PATH_LENGTH = 300;
34067
34067
  function clamp(value, min, max) {
34068
34068
  return Math.max(min, Math.min(max, value));
34069
34069
  }
34070
- function iou$2(a, b) {
34070
+ function iou$3(a, b) {
34071
34071
  const ax1 = a.x, ay1 = a.y, ax2 = a.x + a.w, ay2 = a.y + a.h;
34072
34072
  const bx1 = b.x, by1 = b.y, bx2 = b.x + b.w, by2 = b.y + b.h;
34073
34073
  const ix1 = Math.max(ax1, bx1), iy1 = Math.max(ay1, by1);
@@ -34135,7 +34135,7 @@ var SortTracker = class SortTracker {
34135
34135
  trackClass: track.class,
34136
34136
  detClass: det.class,
34137
34137
  detScore: det.score,
34138
- iou: iou$2(track.bbox, det.bbox),
34138
+ iou: iou$3(track.bbox, det.bbox),
34139
34139
  centroidDistPx: Math.hypot(tc.x - dc.x, tc.y - dc.y),
34140
34140
  trackDiagPx: Math.hypot(track.bbox.w, track.bbox.h),
34141
34141
  areaRatio: smaller > 0 ? Math.max(trackArea, detArea) / smaller : 0,
@@ -34199,14 +34199,14 @@ var SortTracker = class SortTracker {
34199
34199
  isDuplicateSpawn(det, live, timestamp) {
34200
34200
  for (const t of live) {
34201
34201
  if (!this.dedupCompatible(t.class, det.class)) continue;
34202
- if (iou$2(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
34203
- if (growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
34202
+ if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
34203
+ if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
34204
34204
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
34205
34205
  return true;
34206
34206
  }
34207
34207
  }
34208
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$2(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
34209
+ for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
34210
34210
  }
34211
34211
  return false;
34212
34212
  }
@@ -34246,7 +34246,7 @@ var SortTracker = class SortTracker {
34246
34246
  if (dropIds.has(a.id) || dropIds.has(b.id)) continue;
34247
34247
  if (!this.dedupCompatible(a.class, b.class)) continue;
34248
34248
  const key = SortTracker.pairKey(a.id, b.id);
34249
- if (iou$2(a.bbox, b.bbox) < this.config.dedupMergeIou) {
34249
+ if (iou$3(a.bbox, b.bbox) < this.config.dedupMergeIou) {
34250
34250
  this.dupStreaks.delete(key);
34251
34251
  continue;
34252
34252
  }
@@ -34322,7 +34322,7 @@ var SortTracker = class SortTracker {
34322
34322
  */
34323
34323
  looseMatch(track, det, timestamp) {
34324
34324
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34325
- if (iou$2(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34325
+ if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34326
34326
  const tc = bboxCentroid(track.bbox);
34327
34327
  const dc = bboxCentroid(det.bbox);
34328
34328
  const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
@@ -34408,7 +34408,7 @@ var SortTracker = class SortTracker {
34408
34408
  if (used.has(di)) continue;
34409
34409
  const det = detections[di];
34410
34410
  if (!this.associable(track.class, det.class)) continue;
34411
- const score = iou$2(pbox, det.bbox);
34411
+ const score = iou$3(pbox, det.bbox);
34412
34412
  if (score >= iouThreshold) pairs.push({
34413
34413
  track,
34414
34414
  detIdx: di,
@@ -34470,7 +34470,7 @@ var SortTracker = class SortTracker {
34470
34470
  rescuePairs.push({
34471
34471
  track,
34472
34472
  detIdx: di,
34473
- score: iou$2(track.bbox, det.bbox),
34473
+ score: iou$3(track.bbox, det.bbox),
34474
34474
  gate
34475
34475
  });
34476
34476
  }
@@ -34547,7 +34547,7 @@ var SortTracker = class SortTracker {
34547
34547
  const gate = this.looseMatch(lost, det, timestamp);
34548
34548
  if (gate === null) continue;
34549
34549
  if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
34550
- const score = iou$2(lost.bbox, det.bbox);
34550
+ const score = iou$3(lost.bbox, det.bbox);
34551
34551
  const peers = this.sameGroupPeerCount(lost, surviving);
34552
34552
  if (this.config.resurrectionCrowdGuard && peers > 0 && score < this.config.resurrectionCrowdedIouThreshold) {
34553
34553
  if (score > refusedScore) {
@@ -44374,6 +44374,32 @@ var AnalyticsQueryFacade = class {
44374
44374
  }
44375
44375
  };
44376
44376
  //#endregion
44377
+ //#region src/pipeline-analytics/audio-confirm-window.ts
44378
+ function emptyAudioConfirmState() {
44379
+ return { hitsByClass: /* @__PURE__ */ new Map() };
44380
+ }
44381
+ function observeAudioConfirm(state, input, cfg) {
44382
+ if (cfg.confirmHits <= 1) return {
44383
+ confirmed: true,
44384
+ nextState: state
44385
+ };
44386
+ const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
44387
+ const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
44388
+ const next = new Map(state.hitsByClass);
44389
+ if (kept.length < cfg.confirmHits) {
44390
+ next.set(input.className, kept);
44391
+ return {
44392
+ confirmed: false,
44393
+ nextState: { hitsByClass: next }
44394
+ };
44395
+ }
44396
+ next.delete(input.className);
44397
+ return {
44398
+ confirmed: true,
44399
+ nextState: { hitsByClass: next }
44400
+ };
44401
+ }
44402
+ //#endregion
44377
44403
  //#region src/pipeline-analytics/audio-detection-settings.ts
44378
44404
  var AudioDetectionSettingsSchema = require_dist.object({
44379
44405
  classificationMinScore: require_dist.number().min(0).max(1).default(.6),
@@ -44634,32 +44660,6 @@ function observeAudioEpisode(state, sample, knobs) {
44634
44660
  };
44635
44661
  }
44636
44662
  //#endregion
44637
- //#region src/pipeline-analytics/audio-confirm-window.ts
44638
- function emptyAudioConfirmState() {
44639
- return { hitsByClass: /* @__PURE__ */ new Map() };
44640
- }
44641
- function observeAudioConfirm(state, input, cfg) {
44642
- if (cfg.confirmHits <= 1) return {
44643
- confirmed: true,
44644
- nextState: state
44645
- };
44646
- const cutoff = input.timestampMs - cfg.confirmWindowSec * 1e3;
44647
- const kept = [...(state.hitsByClass.get(input.className) ?? []).filter((at) => at > cutoff), input.timestampMs];
44648
- const next = new Map(state.hitsByClass);
44649
- if (kept.length < cfg.confirmHits) {
44650
- next.set(input.className, kept);
44651
- return {
44652
- confirmed: false,
44653
- nextState: { hitsByClass: next }
44654
- };
44655
- }
44656
- next.delete(input.className);
44657
- return {
44658
- confirmed: true,
44659
- nextState: { hitsByClass: next }
44660
- };
44661
- }
44662
- //#endregion
44663
44663
  //#region src/pipeline-analytics/audio-event-router.ts
44664
44664
  /**
44665
44665
  * Decide whether an audio frame is a (meaningful) classification event or a
@@ -45240,6 +45240,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45240
45240
  } else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
45241
45241
  const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45242
45242
  const confirmed = best !== void 0 && best.score >= bar;
45243
+ const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
45243
45244
  return {
45244
45245
  trackId: candidate.trackId,
45245
45246
  verdict: confirmed ? "confirmed" : "suppressed",
@@ -45250,7 +45251,30 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45250
45251
  ...bestIncompatible ? {
45251
45252
  bestIncompatibleClass: bestIncompatible.macroClass,
45252
45253
  bestIncompatibleScore: bestIncompatible.score
45253
- } : {}
45254
+ } : {},
45255
+ ...secondary ? { secondary } : {}
45256
+ };
45257
+ }
45258
+ /**
45259
+ * The incompatible best becomes a secondary subject when it clears the SAME
45260
+ * bar the candidate faced and can be placed in the frame. The crop-normalised
45261
+ * box is scaled into the crop's frame rectangle — the one derivation the
45262
+ * capture used, supplied by the caller, never re-derived here.
45263
+ */
45264
+ function promoteSecondary(candidate, detection, bar, deps) {
45265
+ if (detection.score < bar || detection.bbox === void 0) return null;
45266
+ const rect = deps.cropRectFor?.(candidate) ?? null;
45267
+ if (rect === null || rect.w <= 0 || rect.h <= 0) return null;
45268
+ const box = detection.bbox;
45269
+ return {
45270
+ className: detection.macroClass,
45271
+ score: detection.score,
45272
+ bbox: {
45273
+ x: rect.x + box.x * rect.w,
45274
+ y: rect.y + box.y * rect.h,
45275
+ w: box.w * rect.w,
45276
+ h: box.h * rect.h
45277
+ }
45254
45278
  };
45255
45279
  }
45256
45280
  async function confirmOne(candidate, config, deps, exhausted) {
@@ -48010,6 +48034,336 @@ var MediaWriteGate = class {
48010
48034
  this.paused = false;
48011
48035
  }
48012
48036
  };
48037
+ //#endregion
48038
+ //#region src/pipeline-analytics/runtime/failure-report.ts
48039
+ /**
48040
+ * What `pipeline-analytics` reports through `failure-contribution` — the four
48041
+ * per-camera counters, and the ONE place their vocabulary is written down.
48042
+ *
48043
+ * ## Why the vocabulary is a module and not four string literals
48044
+ *
48045
+ * D276 gave a track row a `previewMissReason` composed from strings like
48046
+ * `keyframe:native-miss` and `thumbnail:overflow`. Those strings are now
48047
+ * joinable evidence: a row says which track lost its tile, and these counters
48048
+ * say how often that camera loses one and out of how many tries. A second
48049
+ * vocabulary for the same loss would break the join silently — the row would
48050
+ * say `native-miss` and the counter `nativeMiss`, and nobody would notice until
48051
+ * somebody tried to correlate them. So the reasons live here, named once, and
48052
+ * they deliberately reuse D276's spelling.
48053
+ *
48054
+ * ## Every family carries its denominator
48055
+ *
48056
+ * Each `note*` call is ONE attempt. The success calls are not decoration: a
48057
+ * failure count without them is the number that read as "35x worse than
48058
+ * yesterday" on 2026-08-28 and was flat all day once divided
48059
+ * (`packages/types/src/metrics/failure-counters.ts`).
48060
+ *
48061
+ * ## Nothing here logs
48062
+ *
48063
+ * These paths run at frame rate — 507 enrichment-crop misses in five minutes at
48064
+ * peak, ~100/min. A line each is its own defect, and the log lines that already
48065
+ * exist on these paths (`enrichment crop native miss`, `capture queue full`,
48066
+ * `plate: read discarded`) are unchanged and still throttled. This is the
48067
+ * counter that makes them countable without reading them.
48068
+ */
48069
+ /**
48070
+ * The enrichment crop capture — face / plate / clip / detail-classifier.
48071
+ * Denominator: every crop this addon asked the owning runner for.
48072
+ */
48073
+ var FAMILY_ENRICHMENT_CROP = "enrichment-crop";
48074
+ /**
48075
+ * The per-camera capture scheduler lane. Denominator: every queued capture that
48076
+ * reached a TERMINAL scheduling outcome — started, or dropped. A coalesced or
48077
+ * cancelled request is neither and is deliberately not counted: a track that
48078
+ * closed before its capture ran did not lose work, it stopped wanting it.
48079
+ */
48080
+ var FAMILY_CAPTURE_SCHEDULE = "capture-schedule";
48081
+ /** Plate OCR reads routed from the detail plane. Denominator: every read. */
48082
+ var FAMILY_PLATE_OCR = "plate-ocr";
48083
+ /**
48084
+ * Person tracks born on this camera. Denominator: every confirmed person birth.
48085
+ *
48086
+ * The "failure" here is NOT suppressed and must not be — a person standing over
48087
+ * a car is a real person, and this repo will not trade that away for a cleaner
48088
+ * count (D275 made the same choice for stationary suppression). It is counted so
48089
+ * the phantom rate is a number instead of an impression: 4 of 23 on camera 617
48090
+ * in two hours, measured by hand on 2026-08-28.
48091
+ */
48092
+ var FAMILY_PERSON_BIRTH = "person-birth";
48093
+ /**
48094
+ * The owning runner had no native pixels for the ROI.
48095
+ *
48096
+ * This collapses the runner-side causes the operator can already see in logs
48097
+ * (`worker-lease-gone`, `no-registry-entry`, `jpeg-encode-failed`) because
48098
+ * `pipelineRunner.getNativeCrop` answers an ROI miss with `null` and carries no
48099
+ * reason on the wire — and `FrameHandle` has no `deviceId`, so the runner could
48100
+ * not attribute the split per camera even if it reported it. Splitting this is a
48101
+ * framework change (a reason on the reply); until then the honest name is the
48102
+ * one the caller can actually stand behind.
48103
+ */
48104
+ var REASON_NATIVE_MISS = "native-miss";
48105
+ /** Queued past `staleAfterMs`; the frame it wanted is gone. */
48106
+ var REASON_STALE_DROP = "stale-drop";
48107
+ /** Shed because the camera's lane was at its queue cap. */
48108
+ var REASON_QUEUE_OVERFLOW = "queue-overflow";
48109
+ /**
48110
+ * The OCR step returned, with nothing in it.
48111
+ *
48112
+ * The defect this counter was built for: ~35% of plate reads came back with an
48113
+ * empty label and fell through the detail router's `d.label.length > 0` gate
48114
+ * into no branch at all — no store, no log, no row. Silence read as "this camera
48115
+ * never saw a plate", which is a completely different fix.
48116
+ */
48117
+ var REASON_EMPTY_READ = "empty-read";
48118
+ /** A plate detail with no bbox — nothing to crop, nothing to place. */
48119
+ var REASON_NO_BBOX = "no-bbox";
48120
+ /** `isPlausiblePlateRead` refused it (junk OCR off a distant/oblique plate). */
48121
+ var REASON_IMPLAUSIBLE = "implausible";
48122
+ /** Below the camera's configured minimum OCR confidence. */
48123
+ var REASON_BELOW_MIN_CONFIDENCE = "below-min-confidence";
48124
+ /** A person born with its box inside a vehicle's. See {@link FAMILY_PERSON_BIRTH}. */
48125
+ var REASON_PERSON_OVER_VEHICLE = "person-over-vehicle";
48126
+ /**
48127
+ * This addon's `failure-contribution` counters.
48128
+ *
48129
+ * One instance per addon process, handed to the paths that fail. Every method
48130
+ * takes the numeric `deviceId` first, for the same reason every log line does:
48131
+ * the question is always per camera.
48132
+ */
48133
+ var AnalyticsFailureReport = class {
48134
+ now;
48135
+ counters;
48136
+ constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
48137
+ this.now = now;
48138
+ this.counters = counters;
48139
+ }
48140
+ /** One enrichment crop capture. `reason` absent = the native ROI landed. */
48141
+ noteEnrichmentCrop(deviceId, reason) {
48142
+ this.counters.note({
48143
+ deviceId,
48144
+ family: FAMILY_ENRICHMENT_CROP,
48145
+ ...reason !== void 0 ? { reason } : {}
48146
+ }, this.now());
48147
+ }
48148
+ /** One capture that reached a terminal scheduling outcome. */
48149
+ noteCaptureSchedule(deviceId, reason) {
48150
+ this.counters.note({
48151
+ deviceId,
48152
+ family: FAMILY_CAPTURE_SCHEDULE,
48153
+ ...reason !== void 0 ? { reason } : {}
48154
+ }, this.now());
48155
+ }
48156
+ /** One plate OCR read. `reason` absent = the read was accepted and stored. */
48157
+ notePlateRead(deviceId, reason) {
48158
+ this.counters.note({
48159
+ deviceId,
48160
+ family: FAMILY_PLATE_OCR,
48161
+ ...reason !== void 0 ? { reason } : {}
48162
+ }, this.now());
48163
+ }
48164
+ /** One confirmed person birth. `reason` present = it was born over a vehicle. */
48165
+ notePersonBirth(deviceId, reason) {
48166
+ this.counters.note({
48167
+ deviceId,
48168
+ family: FAMILY_PERSON_BIRTH,
48169
+ ...reason !== void 0 ? { reason } : {}
48170
+ }, this.now());
48171
+ }
48172
+ /** The `failure-contribution` provider's payload. Reads, never resets. */
48173
+ list() {
48174
+ return this.counters.snapshot(this.now());
48175
+ }
48176
+ /** Addon disposal. */
48177
+ clear() {
48178
+ this.counters.clear();
48179
+ }
48180
+ };
48181
+ //#endregion
48182
+ //#region src/pipeline-analytics/pipeline/bbox-pad.ts
48183
+ /**
48184
+ * Pad a normalized bounding box by a fractional amount relative to its own
48185
+ * dimensions. All coordinates remain in the [0, 1] unit-square.
48186
+ *
48187
+ * Formula (before clamping):
48188
+ * newX = x - padding * w
48189
+ * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
48190
+ * newY = y - padding * h
48191
+ * newH = h + 2 * padding * h
48192
+ *
48193
+ * padding === 0 is the identity operation. Pure, immutable — always returns a
48194
+ * new object.
48195
+ *
48196
+ * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
48197
+ * bound slides inward and keeps the extent that was asked for, on either side.
48198
+ * It shrinks only when the padded window is larger than the frame itself
48199
+ * would (e.g. a left-hugging box gains a bit more right-side context). This is
48200
+ * acceptable for detection crops (more context, never out of [0,1]); the
48201
+ * downstream `extractCrop` clamps to pixel bounds again regardless.
48202
+ */
48203
+ function padBbox(bbox, padding) {
48204
+ const rawX = bbox.x - padding * bbox.w;
48205
+ const rawY = bbox.y - padding * bbox.h;
48206
+ const rawW = bbox.w * (1 + 2 * padding);
48207
+ const rawH = bbox.h * (1 + 2 * padding);
48208
+ const w = Math.min(rawW, 1);
48209
+ const h = Math.min(rawH, 1);
48210
+ return {
48211
+ x: Math.min(Math.max(0, rawX), 1 - w),
48212
+ y: Math.min(Math.max(0, rawY), 1 - h),
48213
+ w,
48214
+ h
48215
+ };
48216
+ }
48217
+ //#endregion
48218
+ //#region src/pipeline-analytics/pipeline/capture-crop.ts
48219
+ /**
48220
+ * The normalised window a capture asks the runner for: the box padded by
48221
+ * `padding` and slid inside the unit square. ONE derivation, shared by the
48222
+ * capture below and by any caller that must put a crop-relative box back
48223
+ * into the frame (the confirmation gate's secondary subject) — two copies of
48224
+ * this arithmetic would drift, and a drifted one places the subject somewhere
48225
+ * it is not.
48226
+ */
48227
+ function captureCropWindow(bbox, frameWidth, frameHeight, padding) {
48228
+ return padBbox({
48229
+ x: bbox.x / frameWidth,
48230
+ y: bbox.y / frameHeight,
48231
+ w: bbox.w / frameWidth,
48232
+ h: bbox.h / frameHeight
48233
+ }, padding);
48234
+ }
48235
+ /** {@link captureCropWindow} in FRAME pixels. */
48236
+ function captureCropFrameRect(bbox, frameWidth, frameHeight, padding) {
48237
+ const window = captureCropWindow(bbox, frameWidth, frameHeight, padding);
48238
+ return {
48239
+ x: window.x * frameWidth,
48240
+ y: window.y * frameHeight,
48241
+ w: window.w * frameWidth,
48242
+ h: window.h * frameHeight
48243
+ };
48244
+ }
48245
+ function createCaptureCrop(deps) {
48246
+ return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
48247
+ const paddedNorm = captureCropWindow(bbox, frameWidth, frameHeight, padding);
48248
+ const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
48249
+ if (nativeCrop) {
48250
+ deps.bumpCropMetric(true);
48251
+ if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId);
48252
+ return nativeCrop;
48253
+ }
48254
+ deps.bumpCropMetric(false);
48255
+ if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId, REASON_NATIVE_MISS);
48256
+ deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
48257
+ ...deviceId !== void 0 ? { tags: { deviceId } } : {},
48258
+ meta: { nodeId: frameHandle.nodeId }
48259
+ });
48260
+ return null;
48261
+ };
48262
+ }
48263
+ //#endregion
48264
+ //#region src/pipeline-analytics/pipeline/secondary-subjects.ts
48265
+ /**
48266
+ * How long a promoted subject may keep riding. Rides are consumed within
48267
+ * ~300 ms at 10 fps; this bound is what stops a promotion made just before a
48268
+ * stall from riding a frame that belongs to a scene that has moved on.
48269
+ */
48270
+ var SECONDARY_SUBJECT_TTL_MS = 3e3;
48271
+ var SecondarySubjectQueue = class {
48272
+ byKey = /* @__PURE__ */ new Map();
48273
+ nextId = 0;
48274
+ add(key, subject, foundBesideTrackId, now) {
48275
+ const pending = this.byKey.get(key) ?? [];
48276
+ this.nextId += 1;
48277
+ this.byKey.set(key, [...pending, {
48278
+ id: `s${this.nextId}`,
48279
+ subject,
48280
+ foundBesideTrackId,
48281
+ expiresAt: now + SECONDARY_SUBJECT_TTL_MS,
48282
+ ridesLeft: 3
48283
+ }]);
48284
+ }
48285
+ /**
48286
+ * Every subject still owed a ride for `key`, one ride spent. An entry out of
48287
+ * rides or past its TTL is forgotten here rather than served.
48288
+ */
48289
+ take(key, now) {
48290
+ const pending = this.byKey.get(key);
48291
+ if (!pending) return [];
48292
+ const live = pending.filter((entry) => entry.expiresAt > now && entry.ridesLeft > 0);
48293
+ const remaining = live.map((entry) => ({
48294
+ ...entry,
48295
+ ridesLeft: entry.ridesLeft - 1
48296
+ })).filter((entry) => entry.ridesLeft > 0);
48297
+ if (remaining.length === 0) this.byKey.delete(key);
48298
+ else this.byKey.set(key, remaining);
48299
+ return live.map((entry) => ({
48300
+ id: entry.id,
48301
+ subject: entry.subject,
48302
+ foundBesideTrackId: entry.foundBesideTrackId
48303
+ }));
48304
+ }
48305
+ /** A subject a real detection has taken over rides no further. */
48306
+ drop(key, id) {
48307
+ const pending = this.byKey.get(key);
48308
+ if (!pending) return;
48309
+ const remaining = pending.filter((entry) => entry.id !== id);
48310
+ if (remaining.length === 0) this.byKey.delete(key);
48311
+ else this.byKey.set(key, remaining);
48312
+ }
48313
+ forget(key) {
48314
+ this.byKey.delete(key);
48315
+ }
48316
+ };
48317
+ function iou$2(a, b) {
48318
+ const inter = Math.max(0, Math.min(a.x + a.w, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.height) - Math.max(a.y, b.y));
48319
+ const union = a.w * a.h + b.width * b.height - inter;
48320
+ return union > 0 ? inter / union : 0;
48321
+ }
48322
+ /**
48323
+ * The runner's detections plus one synthetic first-level detection per pending
48324
+ * subject that no same-class detection already covers. The synthetic box
48325
+ * carries the gate's score — a measurement on a native crop, not an invention
48326
+ * — and an id that says where it came from. The original result is not
48327
+ * mutated.
48328
+ */
48329
+ function injectSecondarySubjects(frame, pending) {
48330
+ if (pending.length === 0) return {
48331
+ frame,
48332
+ covered: []
48333
+ };
48334
+ const added = [];
48335
+ const covered = [];
48336
+ for (const { id, subject, foundBesideTrackId } of pending) {
48337
+ if ([...frame.detections, ...added].some((d) => d.kind === "first-level" && d.macroClass === subject.className && iou$2(subject.bbox, d.bbox) >= .5)) {
48338
+ covered.push(id);
48339
+ continue;
48340
+ }
48341
+ added.push({
48342
+ id: `secondary-${id}-${foundBesideTrackId}`,
48343
+ kind: "first-level",
48344
+ macroClass: subject.className,
48345
+ score: subject.score,
48346
+ labels: [],
48347
+ bbox: {
48348
+ x: subject.bbox.x,
48349
+ y: subject.bbox.y,
48350
+ width: subject.bbox.w,
48351
+ height: subject.bbox.h
48352
+ }
48353
+ });
48354
+ }
48355
+ if (added.length === 0) return {
48356
+ frame,
48357
+ covered
48358
+ };
48359
+ return {
48360
+ frame: {
48361
+ ...frame,
48362
+ detections: [...frame.detections, ...added]
48363
+ },
48364
+ covered
48365
+ };
48366
+ }
48013
48367
  var MACRO_SET = new Set([
48014
48368
  "person",
48015
48369
  "vehicle",
@@ -51859,42 +52213,6 @@ function decideKeyFrameCapture(input) {
51859
52213
  return { kind: "retry" };
51860
52214
  }
51861
52215
  //#endregion
51862
- //#region src/pipeline-analytics/pipeline/bbox-pad.ts
51863
- /**
51864
- * Pad a normalized bounding box by a fractional amount relative to its own
51865
- * dimensions. All coordinates remain in the [0, 1] unit-square.
51866
- *
51867
- * Formula (before clamping):
51868
- * newX = x - padding * w
51869
- * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
51870
- * newY = y - padding * h
51871
- * newH = h + 2 * padding * h
51872
- *
51873
- * padding === 0 is the identity operation. Pure, immutable — always returns a
51874
- * new object.
51875
- *
51876
- * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
51877
- * bound slides inward and keeps the extent that was asked for, on either side.
51878
- * It shrinks only when the padded window is larger than the frame itself
51879
- * would (e.g. a left-hugging box gains a bit more right-side context). This is
51880
- * acceptable for detection crops (more context, never out of [0,1]); the
51881
- * downstream `extractCrop` clamps to pixel bounds again regardless.
51882
- */
51883
- function padBbox(bbox, padding) {
51884
- const rawX = bbox.x - padding * bbox.w;
51885
- const rawY = bbox.y - padding * bbox.h;
51886
- const rawW = bbox.w * (1 + 2 * padding);
51887
- const rawH = bbox.h * (1 + 2 * padding);
51888
- const w = Math.min(rawW, 1);
51889
- const h = Math.min(rawH, 1);
51890
- return {
51891
- x: Math.min(Math.max(0, rawX), 1 - w),
51892
- y: Math.min(Math.max(0, rawY), 1 - h),
51893
- w,
51894
- h
51895
- };
51896
- }
51897
- //#endregion
51898
52216
  //#region src/pipeline-analytics/pipeline/subject-agreement.ts
51899
52217
  /**
51900
52218
  * `subjectAgreement` — the ONE predicate for "a rectangle measured on frame A
@@ -55249,150 +55567,6 @@ function votePlateText(reads) {
55249
55567
  return out;
55250
55568
  }
55251
55569
  //#endregion
55252
- //#region src/pipeline-analytics/runtime/failure-report.ts
55253
- /**
55254
- * What `pipeline-analytics` reports through `failure-contribution` — the four
55255
- * per-camera counters, and the ONE place their vocabulary is written down.
55256
- *
55257
- * ## Why the vocabulary is a module and not four string literals
55258
- *
55259
- * D276 gave a track row a `previewMissReason` composed from strings like
55260
- * `keyframe:native-miss` and `thumbnail:overflow`. Those strings are now
55261
- * joinable evidence: a row says which track lost its tile, and these counters
55262
- * say how often that camera loses one and out of how many tries. A second
55263
- * vocabulary for the same loss would break the join silently — the row would
55264
- * say `native-miss` and the counter `nativeMiss`, and nobody would notice until
55265
- * somebody tried to correlate them. So the reasons live here, named once, and
55266
- * they deliberately reuse D276's spelling.
55267
- *
55268
- * ## Every family carries its denominator
55269
- *
55270
- * Each `note*` call is ONE attempt. The success calls are not decoration: a
55271
- * failure count without them is the number that read as "35x worse than
55272
- * yesterday" on 2026-08-28 and was flat all day once divided
55273
- * (`packages/types/src/metrics/failure-counters.ts`).
55274
- *
55275
- * ## Nothing here logs
55276
- *
55277
- * These paths run at frame rate — 507 enrichment-crop misses in five minutes at
55278
- * peak, ~100/min. A line each is its own defect, and the log lines that already
55279
- * exist on these paths (`enrichment crop native miss`, `capture queue full`,
55280
- * `plate: read discarded`) are unchanged and still throttled. This is the
55281
- * counter that makes them countable without reading them.
55282
- */
55283
- /**
55284
- * The enrichment crop capture — face / plate / clip / detail-classifier.
55285
- * Denominator: every crop this addon asked the owning runner for.
55286
- */
55287
- var FAMILY_ENRICHMENT_CROP = "enrichment-crop";
55288
- /**
55289
- * The per-camera capture scheduler lane. Denominator: every queued capture that
55290
- * reached a TERMINAL scheduling outcome — started, or dropped. A coalesced or
55291
- * cancelled request is neither and is deliberately not counted: a track that
55292
- * closed before its capture ran did not lose work, it stopped wanting it.
55293
- */
55294
- var FAMILY_CAPTURE_SCHEDULE = "capture-schedule";
55295
- /** Plate OCR reads routed from the detail plane. Denominator: every read. */
55296
- var FAMILY_PLATE_OCR = "plate-ocr";
55297
- /**
55298
- * Person tracks born on this camera. Denominator: every confirmed person birth.
55299
- *
55300
- * The "failure" here is NOT suppressed and must not be — a person standing over
55301
- * a car is a real person, and this repo will not trade that away for a cleaner
55302
- * count (D275 made the same choice for stationary suppression). It is counted so
55303
- * the phantom rate is a number instead of an impression: 4 of 23 on camera 617
55304
- * in two hours, measured by hand on 2026-08-28.
55305
- */
55306
- var FAMILY_PERSON_BIRTH = "person-birth";
55307
- /**
55308
- * The owning runner had no native pixels for the ROI.
55309
- *
55310
- * This collapses the runner-side causes the operator can already see in logs
55311
- * (`worker-lease-gone`, `no-registry-entry`, `jpeg-encode-failed`) because
55312
- * `pipelineRunner.getNativeCrop` answers an ROI miss with `null` and carries no
55313
- * reason on the wire — and `FrameHandle` has no `deviceId`, so the runner could
55314
- * not attribute the split per camera even if it reported it. Splitting this is a
55315
- * framework change (a reason on the reply); until then the honest name is the
55316
- * one the caller can actually stand behind.
55317
- */
55318
- var REASON_NATIVE_MISS = "native-miss";
55319
- /** Queued past `staleAfterMs`; the frame it wanted is gone. */
55320
- var REASON_STALE_DROP = "stale-drop";
55321
- /** Shed because the camera's lane was at its queue cap. */
55322
- var REASON_QUEUE_OVERFLOW = "queue-overflow";
55323
- /**
55324
- * The OCR step returned, with nothing in it.
55325
- *
55326
- * The defect this counter was built for: ~35% of plate reads came back with an
55327
- * empty label and fell through the detail router's `d.label.length > 0` gate
55328
- * into no branch at all — no store, no log, no row. Silence read as "this camera
55329
- * never saw a plate", which is a completely different fix.
55330
- */
55331
- var REASON_EMPTY_READ = "empty-read";
55332
- /** A plate detail with no bbox — nothing to crop, nothing to place. */
55333
- var REASON_NO_BBOX = "no-bbox";
55334
- /** `isPlausiblePlateRead` refused it (junk OCR off a distant/oblique plate). */
55335
- var REASON_IMPLAUSIBLE = "implausible";
55336
- /** Below the camera's configured minimum OCR confidence. */
55337
- var REASON_BELOW_MIN_CONFIDENCE = "below-min-confidence";
55338
- /** A person born with its box inside a vehicle's. See {@link FAMILY_PERSON_BIRTH}. */
55339
- var REASON_PERSON_OVER_VEHICLE = "person-over-vehicle";
55340
- /**
55341
- * This addon's `failure-contribution` counters.
55342
- *
55343
- * One instance per addon process, handed to the paths that fail. Every method
55344
- * takes the numeric `deviceId` first, for the same reason every log line does:
55345
- * the question is always per camera.
55346
- */
55347
- var AnalyticsFailureReport = class {
55348
- now;
55349
- counters;
55350
- constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
55351
- this.now = now;
55352
- this.counters = counters;
55353
- }
55354
- /** One enrichment crop capture. `reason` absent = the native ROI landed. */
55355
- noteEnrichmentCrop(deviceId, reason) {
55356
- this.counters.note({
55357
- deviceId,
55358
- family: FAMILY_ENRICHMENT_CROP,
55359
- ...reason !== void 0 ? { reason } : {}
55360
- }, this.now());
55361
- }
55362
- /** One capture that reached a terminal scheduling outcome. */
55363
- noteCaptureSchedule(deviceId, reason) {
55364
- this.counters.note({
55365
- deviceId,
55366
- family: FAMILY_CAPTURE_SCHEDULE,
55367
- ...reason !== void 0 ? { reason } : {}
55368
- }, this.now());
55369
- }
55370
- /** One plate OCR read. `reason` absent = the read was accepted and stored. */
55371
- notePlateRead(deviceId, reason) {
55372
- this.counters.note({
55373
- deviceId,
55374
- family: FAMILY_PLATE_OCR,
55375
- ...reason !== void 0 ? { reason } : {}
55376
- }, this.now());
55377
- }
55378
- /** One confirmed person birth. `reason` present = it was born over a vehicle. */
55379
- notePersonBirth(deviceId, reason) {
55380
- this.counters.note({
55381
- deviceId,
55382
- family: FAMILY_PERSON_BIRTH,
55383
- ...reason !== void 0 ? { reason } : {}
55384
- }, this.now());
55385
- }
55386
- /** The `failure-contribution` provider's payload. Reads, never resets. */
55387
- list() {
55388
- return this.counters.snapshot(this.now());
55389
- }
55390
- /** Addon disposal. */
55391
- clear() {
55392
- this.counters.clear();
55393
- }
55394
- };
55395
- //#endregion
55396
55570
  //#region src/pipeline-analytics/plate-recognizer.ts
55397
55571
  /**
55398
55572
  * PlateRecognizer — collects EVERY license-plate OCR read a vehicle track
@@ -60451,31 +60625,6 @@ var NativeCropDedupe = class {
60451
60625
  }
60452
60626
  };
60453
60627
  //#endregion
60454
- //#region src/pipeline-analytics/pipeline/capture-crop.ts
60455
- function createCaptureCrop(deps) {
60456
- return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth, deviceId) => {
60457
- const paddedNorm = padBbox({
60458
- x: bbox.x / frameWidth,
60459
- y: bbox.y / frameHeight,
60460
- w: bbox.w / frameWidth,
60461
- h: bbox.h / frameHeight
60462
- }, padding);
60463
- const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
60464
- if (nativeCrop) {
60465
- deps.bumpCropMetric(true);
60466
- if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId);
60467
- return nativeCrop;
60468
- }
60469
- deps.bumpCropMetric(false);
60470
- if (deviceId !== void 0) deps.noteCropOutcome?.(deviceId, REASON_NATIVE_MISS);
60471
- deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", {
60472
- ...deviceId !== void 0 ? { tags: { deviceId } } : {},
60473
- meta: { nodeId: frameHandle.nodeId }
60474
- });
60475
- return null;
60476
- };
60477
- }
60478
- //#endregion
60479
60628
  //#region src/pipeline-analytics/pipeline/display-crop.ts
60480
60629
  /** Bound the once-per-owner memo so a long-lived process never leaks it. */
60481
60630
  var MAX_LOGGED_OWNERS = 5e3;
@@ -65351,6 +65500,144 @@ function rowFromMetadata(id, metadata) {
65351
65500
  };
65352
65501
  }
65353
65502
  //#endregion
65503
+ //#region src/pipeline-analytics/store/settings-raw-mirror.ts
65504
+ /** How long a mirrored entry is served before a refresh is started behind the
65505
+ * next reader. See the module doc for why this matches the per-kind caches'
65506
+ * own window rather than introducing a second staleness bound. */
65507
+ var RAW_SETTINGS_STALE_MS = 5e3;
65508
+ /**
65509
+ * How long a FAILED refresh is not retried.
65510
+ *
65511
+ * A refresh that throws leaves `fetchedAt` where it was, so without a
65512
+ * cooldown every subsequent reader would start another attempt — a broken
65513
+ * settings store would be hammered at frame rate by the very mechanism built
65514
+ * to stop that.
65515
+ */
65516
+ var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
65517
+ /**
65518
+ * The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
65519
+ * fresh OFF every per-kind settings cache's hot path.
65520
+ *
65521
+ * `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
65522
+ * (or frames) missing on the same device in the same tick costs ONE read,
65523
+ * not one per caller — the exact failure this class exists to remove.
65524
+ */
65525
+ var RawDeviceBlobMirror = class {
65526
+ deps;
65527
+ staleMs;
65528
+ missCooldownMs;
65529
+ entries = /* @__PURE__ */ new Map();
65530
+ inFlight = /* @__PURE__ */ new Map();
65531
+ now;
65532
+ constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
65533
+ this.deps = deps;
65534
+ this.staleMs = staleMs;
65535
+ this.missCooldownMs = missCooldownMs;
65536
+ this.now = deps.now ?? (() => Date.now());
65537
+ }
65538
+ /**
65539
+ * The device's raw stored blob.
65540
+ *
65541
+ * COLD awaits — the first settings kind to touch a device has to get the
65542
+ * real answer. WARM returns immediately, stale or not, and starts the
65543
+ * refresh behind the caller.
65544
+ */
65545
+ async resolve(deviceId) {
65546
+ const entry = this.entries.get(deviceId);
65547
+ if (entry === void 0) return this.load(deviceId);
65548
+ if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
65549
+ return entry.value;
65550
+ }
65551
+ /** Drop a device's entry. Called by every in-process writer of this
65552
+ * device's row, so an operator's own change is never served stale. */
65553
+ invalidate(deviceId) {
65554
+ this.entries.delete(deviceId);
65555
+ }
65556
+ invalidateAll() {
65557
+ this.entries.clear();
65558
+ }
65559
+ async load(deviceId) {
65560
+ const pending = this.inFlight.get(deviceId);
65561
+ if (pending !== void 0) return pending;
65562
+ const run = this.deps.read(deviceId).then((value) => {
65563
+ this.entries.set(deviceId, {
65564
+ value,
65565
+ fetchedAt: this.now()
65566
+ });
65567
+ return value;
65568
+ }).catch((err) => {
65569
+ this.deps.onRefreshFailed?.(deviceId, err);
65570
+ const existing = this.entries.get(deviceId);
65571
+ if (existing !== void 0) {
65572
+ existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
65573
+ return existing.value;
65574
+ }
65575
+ return {};
65576
+ }).finally(() => {
65577
+ this.inFlight.delete(deviceId);
65578
+ });
65579
+ this.inFlight.set(deviceId, run);
65580
+ return run;
65581
+ }
65582
+ };
65583
+ /**
65584
+ * The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
65585
+ * kept fresh OFF every per-kind settings cache's hot path. Same shape as
65586
+ * {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
65587
+ * one addon store per process.
65588
+ */
65589
+ var RawGlobalStoreMirror = class {
65590
+ deps;
65591
+ staleMs;
65592
+ missCooldownMs;
65593
+ entry = null;
65594
+ inFlight = null;
65595
+ now;
65596
+ constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
65597
+ this.deps = deps;
65598
+ this.staleMs = staleMs;
65599
+ this.missCooldownMs = missCooldownMs;
65600
+ this.now = deps.now ?? (() => Date.now());
65601
+ }
65602
+ async resolve() {
65603
+ const entry = this.entry;
65604
+ if (entry === null) return this.load();
65605
+ if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
65606
+ return entry.value;
65607
+ }
65608
+ /** Drop the mirrored store. Called by every in-process global-settings
65609
+ * writer (`onConfigChanged`), so an operator's own change is never served
65610
+ * stale. */
65611
+ invalidate() {
65612
+ this.entry = null;
65613
+ }
65614
+ async load() {
65615
+ if (this.inFlight !== null) return this.inFlight;
65616
+ const run = this.deps.read().then((value) => {
65617
+ this.entry = {
65618
+ value,
65619
+ fetchedAt: this.now()
65620
+ };
65621
+ return value;
65622
+ }).catch((err) => {
65623
+ this.deps.onRefreshFailed?.(err);
65624
+ const existing = this.entry;
65625
+ if (existing !== null) {
65626
+ this.entry = {
65627
+ value: existing.value,
65628
+ fetchedAt: this.now() - this.staleMs + this.missCooldownMs
65629
+ };
65630
+ return existing.value;
65631
+ }
65632
+ return {};
65633
+ }).finally(() => {
65634
+ this.inFlight = null;
65635
+ });
65636
+ this.inFlight = run;
65637
+ return run;
65638
+ }
65639
+ };
65640
+ //#endregion
65354
65641
  //#region src/pipeline-analytics/summary/summary-score.ts
65355
65642
  /** A member whose own box is covered this much by others is hidden. */
65356
65643
  var OCCLUDED_COVER_FRACTION = .5;
@@ -65655,144 +65942,6 @@ var SummarySealer = class {
65655
65942
  }
65656
65943
  };
65657
65944
  //#endregion
65658
- //#region src/pipeline-analytics/store/settings-raw-mirror.ts
65659
- /** How long a mirrored entry is served before a refresh is started behind the
65660
- * next reader. See the module doc for why this matches the per-kind caches'
65661
- * own window rather than introducing a second staleness bound. */
65662
- var RAW_SETTINGS_STALE_MS = 5e3;
65663
- /**
65664
- * How long a FAILED refresh is not retried.
65665
- *
65666
- * A refresh that throws leaves `fetchedAt` where it was, so without a
65667
- * cooldown every subsequent reader would start another attempt — a broken
65668
- * settings store would be hammered at frame rate by the very mechanism built
65669
- * to stop that.
65670
- */
65671
- var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
65672
- /**
65673
- * The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
65674
- * fresh OFF every per-kind settings cache's hot path.
65675
- *
65676
- * `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
65677
- * (or frames) missing on the same device in the same tick costs ONE read,
65678
- * not one per caller — the exact failure this class exists to remove.
65679
- */
65680
- var RawDeviceBlobMirror = class {
65681
- deps;
65682
- staleMs;
65683
- missCooldownMs;
65684
- entries = /* @__PURE__ */ new Map();
65685
- inFlight = /* @__PURE__ */ new Map();
65686
- now;
65687
- constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
65688
- this.deps = deps;
65689
- this.staleMs = staleMs;
65690
- this.missCooldownMs = missCooldownMs;
65691
- this.now = deps.now ?? (() => Date.now());
65692
- }
65693
- /**
65694
- * The device's raw stored blob.
65695
- *
65696
- * COLD awaits — the first settings kind to touch a device has to get the
65697
- * real answer. WARM returns immediately, stale or not, and starts the
65698
- * refresh behind the caller.
65699
- */
65700
- async resolve(deviceId) {
65701
- const entry = this.entries.get(deviceId);
65702
- if (entry === void 0) return this.load(deviceId);
65703
- if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
65704
- return entry.value;
65705
- }
65706
- /** Drop a device's entry. Called by every in-process writer of this
65707
- * device's row, so an operator's own change is never served stale. */
65708
- invalidate(deviceId) {
65709
- this.entries.delete(deviceId);
65710
- }
65711
- invalidateAll() {
65712
- this.entries.clear();
65713
- }
65714
- async load(deviceId) {
65715
- const pending = this.inFlight.get(deviceId);
65716
- if (pending !== void 0) return pending;
65717
- const run = this.deps.read(deviceId).then((value) => {
65718
- this.entries.set(deviceId, {
65719
- value,
65720
- fetchedAt: this.now()
65721
- });
65722
- return value;
65723
- }).catch((err) => {
65724
- this.deps.onRefreshFailed?.(deviceId, err);
65725
- const existing = this.entries.get(deviceId);
65726
- if (existing !== void 0) {
65727
- existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
65728
- return existing.value;
65729
- }
65730
- return {};
65731
- }).finally(() => {
65732
- this.inFlight.delete(deviceId);
65733
- });
65734
- this.inFlight.set(deviceId, run);
65735
- return run;
65736
- }
65737
- };
65738
- /**
65739
- * The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
65740
- * kept fresh OFF every per-kind settings cache's hot path. Same shape as
65741
- * {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
65742
- * one addon store per process.
65743
- */
65744
- var RawGlobalStoreMirror = class {
65745
- deps;
65746
- staleMs;
65747
- missCooldownMs;
65748
- entry = null;
65749
- inFlight = null;
65750
- now;
65751
- constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
65752
- this.deps = deps;
65753
- this.staleMs = staleMs;
65754
- this.missCooldownMs = missCooldownMs;
65755
- this.now = deps.now ?? (() => Date.now());
65756
- }
65757
- async resolve() {
65758
- const entry = this.entry;
65759
- if (entry === null) return this.load();
65760
- if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
65761
- return entry.value;
65762
- }
65763
- /** Drop the mirrored store. Called by every in-process global-settings
65764
- * writer (`onConfigChanged`), so an operator's own change is never served
65765
- * stale. */
65766
- invalidate() {
65767
- this.entry = null;
65768
- }
65769
- async load() {
65770
- if (this.inFlight !== null) return this.inFlight;
65771
- const run = this.deps.read().then((value) => {
65772
- this.entry = {
65773
- value,
65774
- fetchedAt: this.now()
65775
- };
65776
- return value;
65777
- }).catch((err) => {
65778
- this.deps.onRefreshFailed?.(err);
65779
- const existing = this.entry;
65780
- if (existing !== null) {
65781
- this.entry = {
65782
- value: existing.value,
65783
- fetchedAt: this.now() - this.staleMs + this.missCooldownMs
65784
- };
65785
- return existing.value;
65786
- }
65787
- return {};
65788
- }).finally(() => {
65789
- this.inFlight = null;
65790
- });
65791
- this.inFlight = run;
65792
- return run;
65793
- }
65794
- };
65795
- //#endregion
65796
65945
  //#region src/pipeline-analytics/track-retention-sweep.ts
65797
65946
  /**
65798
65947
  * Sweep every device with persisted tracks: skip retention-disabled devices,
@@ -67380,6 +67529,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
67380
67529
  suppressedBirths = new SuppressedBirthRegistry();
67381
67530
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
67382
67531
  deferredBirths = new DeferredBirthRegistry();
67532
+ /**
67533
+ * Subjects the confirmation gate found in a birth's crop BESIDES the birth
67534
+ * itself (a dog in a person's crop), waiting to ride the next frame of the
67535
+ * same (device, source) as detections — `pipeline/secondary-subjects.ts`.
67536
+ */
67537
+ secondarySubjects = new SecondarySubjectQueue();
67383
67538
  lastFrameDimsByDevice = /* @__PURE__ */ new Map();
67384
67539
  lastAudioInsertByDevice = /* @__PURE__ */ new Map();
67385
67540
  lastMotionInsertByDevice = /* @__PURE__ */ new Map();
@@ -70476,9 +70631,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70476
70631
  processor.setZones(liveZones);
70477
70632
  processor.setDetectionRules(liveRules);
70478
70633
  processor.setZoneMembershipMinOverlap(trk.zoneMembershipMinOverlap);
70634
+ const injection = injectSecondarySubjects(frame, this.secondarySubjects.take(key, Date.now()));
70635
+ for (const id of injection.covered) this.secondarySubjects.drop(key, id);
70636
+ const trackerFrame = injection.frame;
70637
+ if (trackerFrame !== frame) this.ctx.logger.info("secondary subjects ride this frame as detections", {
70638
+ tags: { deviceId },
70639
+ meta: {
70640
+ source,
70641
+ added: trackerFrame.detections.length - frame.detections.length,
70642
+ classes: trackerFrame.detections.slice(frame.detections.length).map((d) => d.macroClass)
70643
+ }
70644
+ });
70479
70645
  const result = processor.process({
70480
70646
  timestamp: frame.timestamp,
70481
- frame
70647
+ frame: trackerFrame
70482
70648
  });
70483
70649
  for (const r of processor.getLastLabelWriteRefusals()) {
70484
70650
  const message = r.reason.kind === "macro-class" ? "label tier collapse refused" : "label write refused";
@@ -70621,6 +70787,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70621
70787
  bbox: t.bbox
70622
70788
  })), {
70623
70789
  deviceId,
70790
+ source,
70624
70791
  frameHandle,
70625
70792
  frameWidth: result.frameWidth,
70626
70793
  frameHeight: result.frameHeight
@@ -71265,16 +71432,25 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71265
71432
  const executor = this.pipelineExecutorApi();
71266
71433
  if (!executor?.runPipeline) return null;
71267
71434
  try {
71268
- const detections = (await executor.runPipeline.mutate({
71435
+ const result = await executor.runPipeline.mutate({
71269
71436
  steps: [step],
71270
71437
  image: new Uint8Array(cropJpeg),
71271
71438
  deviceId,
71272
71439
  plane: "frame"
71273
- }, require_dist.nodePin(nodeId)))?.detections;
71440
+ }, require_dist.nodePin(nodeId));
71441
+ const detections = result?.detections;
71274
71442
  if (!detections) return null;
71443
+ const cropWidth = result.width ?? 0;
71444
+ const cropHeight = result.height ?? 0;
71275
71445
  return detections.map((d) => ({
71276
71446
  macroClass: d.macroClass,
71277
- score: d.score
71447
+ score: d.score,
71448
+ ...d.bbox !== void 0 && cropWidth > 0 && cropHeight > 0 ? { bbox: {
71449
+ x: d.bbox.x / cropWidth,
71450
+ y: d.bbox.y / cropHeight,
71451
+ w: d.bbox.width / cropWidth,
71452
+ h: d.bbox.height / cropHeight
71453
+ } } : {}
71278
71454
  }));
71279
71455
  } catch (err) {
71280
71456
  this.ctx.logger.debug("confirmation gate: re-detection failed — fail-open", {
@@ -71300,7 +71476,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71300
71476
  if (candidates.length === 0) return allConfirmed();
71301
71477
  const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
71302
71478
  if (!config.enabled) return allConfirmed();
71303
- const { frameHandle, frameWidth, frameHeight, deviceId } = params;
71479
+ const { frameHandle, frameWidth, frameHeight, deviceId, source } = params;
71304
71480
  const captureCrop = this.captureCrop;
71305
71481
  if (!frameHandle || !captureCrop || frameWidth <= 0 || frameHeight <= 0) {
71306
71482
  this.ctx.logger.info("confirmation gate: no crop path — births allowed unmeasured", {
@@ -71340,6 +71516,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71340
71516
  }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
71341
71517
  },
71342
71518
  redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
71519
+ cropRectFor: (candidate) => captureCropFrameRect({
71520
+ x: candidate.bbox.x,
71521
+ y: candidate.bbox.y,
71522
+ w: candidate.bbox.w,
71523
+ h: candidate.bbox.h
71524
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
71343
71525
  minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
71344
71526
  deviceId,
71345
71527
  className: candidate.className,
@@ -71384,6 +71566,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71384
71566
  });
71385
71567
  break;
71386
71568
  }
71569
+ if (decision.secondary !== void 0) {
71570
+ this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
71571
+ this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
71572
+ tags: { deviceId },
71573
+ meta: {
71574
+ trackId: decision.trackId,
71575
+ className: decision.className,
71576
+ secondaryClass: decision.secondary.className,
71577
+ secondaryScore: decision.secondary.score,
71578
+ secondaryBbox: decision.secondary.bbox,
71579
+ verdict: decision.verdict
71580
+ }
71581
+ });
71582
+ }
71387
71583
  }
71388
71584
  }, exhaustedIds);
71389
71585
  }
@@ -73260,6 +73456,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73260
73456
  const prefix = `${deviceId}:`;
73261
73457
  for (const k of this.processors.keys()) if (k.startsWith(prefix)) this.processors.delete(k);
73262
73458
  for (const k of this.lastActiveTrackIds.keys()) if (k.startsWith(prefix)) this.lastActiveTrackIds.delete(k);
73459
+ for (const source of ["pipeline", "onboard"]) this.secondarySubjects.forget(this.procKey(deviceId, source));
73263
73460
  for (const k of this.dropoutSkipsByKey.keys()) if (k.startsWith(prefix)) this.dropoutSkipsByKey.delete(k);
73264
73461
  this.lastFrameDimsByDevice.delete(deviceId);
73265
73462
  }