@camstack/addon-post-analysis 1.2.265 → 1.2.267

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $ as TimelapseRuleSchema, $t as array, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pickClusterStepModels, E as NC_CONDITION_CATALOG, F as NcScheduleSchema, Ft as storageOccupancyCapability, Gt as createEvent, Ht as BaseAddon, I as NcSnoozeInputSchema, It as subKindsOf, J as SCENE_DIVERGED, Jt as hydrateSchema, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as customAction, L as NcSnoozeSchema, Lt as systemEventFilterApplies, M as NcRulePatchSchema, Mt as resolveLocationMode, N as NcRuleSchema, Ot as pipelineAnalyticsCapability, P as NcRuleTargetSchema, Pt as sceneMonitorCapability, Q as TimelapseRulePatchSchema, Qt as _enum, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as occupancyScope, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as CamProfileSchema, V as OpsLogEntrySchema, Vt as errMsg$1, W as RetrainStatusSchema, Wt as DeviceType, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as nodePin, Y as SceneMonitorSchema, Yt as isDeviceScopedCap, Z as TimelapseRuleInputSchema, Zt as sleep$1, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as partialRecord, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as unknown, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as boolean, et as TrackSourceSchema, f as DeclaredDevices, ft as evaluateSensorEdge, g as FIRST_LEVEL_MACRO_CLASSES, h as EVENT_PAD_MS, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as object, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, ln as EventCategory, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as literal, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as record, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as defineCustomActions, r as BaseDevice, rn as number, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as string, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as discriminatedUnion, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, v as FailureCounters, vt as isEventOwnerType, w as MediaFileKindEnum, wt as notificationRulesCapability, x as MAX_BIRTH_DECISION_RECORDS, xt as isSourceCap, y as LabelAttributionSchema, yt as isMediaOwnerType, z as NcSystemEventKindSchema, zt as videoclipsCapability } from "../dist-CxMzr0uA.mjs";
1
+ import { $ as TimelapseRuleSchema, $t as array, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pickClusterStepModels, E as NC_CONDITION_CATALOG, F as NcScheduleSchema, Ft as storageOccupancyCapability, Gt as createEvent, Ht as BaseAddon, I as NcSnoozeInputSchema, It as subKindsOf, J as SCENE_DIVERGED, Jt as hydrateSchema, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as customAction, L as NcSnoozeSchema, Lt as systemEventFilterApplies, M as NcRulePatchSchema, Mt as resolveLocationMode, N as NcRuleSchema, Ot as pipelineAnalyticsCapability, P as NcRuleTargetSchema, Pt as sceneMonitorCapability, Q as TimelapseRulePatchSchema, Qt as _enum, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as occupancyScope, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as CamProfileSchema, V as OpsLogEntrySchema, Vt as errMsg$1, W as RetrainStatusSchema, Wt as DeviceType, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as nodePin, Y as SceneMonitorSchema, Yt as isDeviceScopedCap, Z as TimelapseRuleInputSchema, Zt as sleep$1, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as partialRecord, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as unknown, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as boolean, et as TrackSourceSchema, f as DeclaredDevices, ft as evaluateSensorEdge, g as FIRST_LEVEL_MACRO_CLASSES, h as EVENT_PAD_MS, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as object, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, ln as EventCategory, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as literal, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as record, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as defineCustomActions, r as BaseDevice, rn as number, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as string, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as discriminatedUnion, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, v as FailureCounters, vt as isEventOwnerType, w as MediaFileKindEnum, wt as notificationRulesCapability, x as MAX_BIRTH_DECISION_RECORDS, xt as isSourceCap, y as LabelAttributionSchema, yt as isMediaOwnerType, z as NcSystemEventKindSchema, zt as videoclipsCapability } from "../dist-DO_99QlZ.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -37050,6 +37050,7 @@ function growthAdjacentMatch(track, det, ctx) {
37050
37050
  const trackArea = track.w * track.h;
37051
37051
  const detArea = det.w * det.h;
37052
37052
  if (trackArea <= 0 || detArea <= 0) return false;
37053
+ if (ctx.frameArea > 0 && trackArea / ctx.frameArea > .01) return false;
37053
37054
  const larger = Math.max(trackArea, detArea);
37054
37055
  const smaller = Math.min(trackArea, detArea);
37055
37056
  if (larger / smaller < 1.5) return false;
@@ -37203,6 +37204,17 @@ var SortTracker = class SortTracker {
37203
37204
  * and a busy one span the same seconds (D438).
37204
37205
  */
37205
37206
  frameSeq = 0;
37207
+ /**
37208
+ * The area of the frame this pass is running over, or `0` when the caller
37209
+ * gave no frame context. Read by the growth clause's FRAGMENT test, which
37210
+ * three call sites reach and only one of them could have been handed it.
37211
+ */
37212
+ frameArea = 0;
37213
+ /** The frame DIMS of the pass, for the same reason `frameArea` is held: the
37214
+ * salvage-bind record carries both boxes, and a box without the frame it was
37215
+ * measured in cannot be put back on a picture. `0` = no frame context. */
37216
+ frameWidthPx = 0;
37217
+ frameHeightPx = 0;
37206
37218
  /** Sink for track deaths — see {@link setTrackLossObserver}. */
37207
37219
  trackLossObserver = null;
37208
37220
  trackLossSampler = new TrackLossSampler();
@@ -37320,7 +37332,7 @@ var SortTracker = class SortTracker {
37320
37332
  if (!this.config.reacquireTimeScaled) return "plausible-birth-veto";
37321
37333
  return this.isUnambiguousReach(track, det, detections) ? "already-claimed" : "crowd-guard";
37322
37334
  }
37323
- if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "already-claimed";
37335
+ if (growthAdjacentMatch(track.bbox, det.bbox, this.growthContext(track, timestamp))) return "already-claimed";
37324
37336
  return "geometry";
37325
37337
  }
37326
37338
  /** Emit one dropped-detection record, subject to the refusal sampler. */
@@ -37440,6 +37452,20 @@ var SortTracker = class SortTracker {
37440
37452
  centroidDistPx: Math.hypot(tc.x - dc.x, tc.y - dc.y),
37441
37453
  trackDiagPx: Math.hypot(track.bbox.w, track.bbox.h),
37442
37454
  areaRatio: smaller > 0 ? Math.max(trackArea, detArea) / smaller : 0,
37455
+ trackBbox: {
37456
+ x: track.bbox.x,
37457
+ y: track.bbox.y,
37458
+ w: track.bbox.w,
37459
+ h: track.bbox.h
37460
+ },
37461
+ detBbox: {
37462
+ x: det.bbox.x,
37463
+ y: det.bbox.y,
37464
+ w: det.bbox.w,
37465
+ h: det.bbox.h
37466
+ },
37467
+ frameWidth: this.frameWidthPx,
37468
+ frameHeight: this.frameHeightPx,
37443
37469
  dtMs: timestamp - track.lastSeen,
37444
37470
  trackAgeMs: timestamp - track.firstSeen,
37445
37471
  bindCount,
@@ -37447,10 +37473,11 @@ var SortTracker = class SortTracker {
37447
37473
  });
37448
37474
  }
37449
37475
  /** When the pair is being judged, relative to `track`. */
37450
- static growthContext(track, timestamp) {
37476
+ growthContext(track, timestamp) {
37451
37477
  return {
37452
37478
  trackAgeMs: timestamp - track.firstSeen,
37453
- gapMs: timestamp - track.lastSeen
37479
+ gapMs: timestamp - track.lastSeen,
37480
+ frameArea: this.frameArea
37454
37481
  };
37455
37482
  }
37456
37483
  /** Whether two macro-classes may be collapsed as the SAME subject. Same class
@@ -37579,7 +37606,7 @@ var SortTracker = class SortTracker {
37579
37606
  });
37580
37607
  return true;
37581
37608
  }
37582
- if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
37609
+ if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, this.growthContext(t, timestamp))) {
37583
37610
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
37584
37611
  return true;
37585
37612
  }
@@ -37754,7 +37781,7 @@ var SortTracker = class SortTracker {
37754
37781
  const tc = bboxCentroid(track.bbox);
37755
37782
  const dc = bboxCentroid(det.bbox);
37756
37783
  if (Math.hypot(tc.x - dc.x, tc.y - dc.y) <= this.reacquireReach(track, timestamp, timeScaled)) return "centroid";
37757
- if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "growth";
37784
+ if (growthAdjacentMatch(track.bbox, det.bbox, this.growthContext(track, timestamp))) return "growth";
37758
37785
  return null;
37759
37786
  }
37760
37787
  /**
@@ -37982,6 +38009,9 @@ var SortTracker = class SortTracker {
37982
38009
  update(detections, timestamp, frameContext) {
37983
38010
  this.frameSeq++;
37984
38011
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
38012
+ this.frameArea = frameArea;
38013
+ this.frameWidthPx = frameContext?.frameWidth ?? 0;
38014
+ this.frameHeightPx = frameContext?.frameHeight ?? 0;
37985
38015
  const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
37986
38016
  const resurrectedScene = /* @__PURE__ */ new Map();
37987
38017
  if (this.config.maxTrackLifetimeMs > 0) {
@@ -41226,6 +41256,17 @@ var TrackResidentState = class {
41226
41256
  objectEmbeddingBestSelector = new TrackBestSelector(BEST_OBJECT_EMBED_POLICY);
41227
41257
  /** keyFrame keys of CLOSED tracks still readable until the recognizer-end
41228
41258
  * deferred drop settles (see module docstring). */
41259
+ /**
41260
+ * How many LIVE tracks carry the operator's `debug` flag, across every camera
41261
+ * this runner holds.
41262
+ *
41263
+ * A counter rather than a scan because the frame path asks it every frame:
41264
+ * it is what lets the host skip maintaining a per-frame handle mirror
41265
+ * entirely while nothing is marked, which is the state of the fleet
41266
+ * essentially always. Kept in step in the three places a resident's flag can
41267
+ * change: {@link setDebugMarked}, {@link close} and {@link clearAll}.
41268
+ */
41269
+ debugMarkedCount = 0;
41229
41270
  deferredKeyFrameKeys = /* @__PURE__ */ new Map();
41230
41271
  constructor(hooks = {}) {
41231
41272
  this.hooks = hooks;
@@ -41247,7 +41288,8 @@ var TrackResidentState = class {
41247
41288
  thumbnailLanded: false,
41248
41289
  keyFrameLanded: false,
41249
41290
  keyFrameRetries: 0,
41250
- confirmed: false
41291
+ confirmed: false,
41292
+ debugMarked: false
41251
41293
  };
41252
41294
  this.residents.set(trackId, created);
41253
41295
  return created;
@@ -41261,6 +41303,7 @@ var TrackResidentState = class {
41261
41303
  */
41262
41304
  close(trackId, mode) {
41263
41305
  const resident = this.residents.get(trackId);
41306
+ if (resident?.debugMarked === true) this.debugMarkedCount -= 1;
41264
41307
  this.residents.delete(trackId);
41265
41308
  this.bestFrameTracker.delete(trackId);
41266
41309
  this.objectEmbeddingBestSelector.delete(trackId);
@@ -41280,6 +41323,7 @@ var TrackResidentState = class {
41280
41323
  }
41281
41324
  /** Drop everything (addon dispose). */
41282
41325
  clearAll() {
41326
+ this.debugMarkedCount = 0;
41283
41327
  this.residents.clear();
41284
41328
  this.bestFrameTracker.clear();
41285
41329
  this.objectEmbeddingBestSelector.clear();
@@ -41471,6 +41515,42 @@ var TrackResidentState = class {
41471
41515
  markConfirmed(deviceId, trackId) {
41472
41516
  this.ensure(deviceId, trackId).confirmed = true;
41473
41517
  }
41518
+ /**
41519
+ * Is this track the one the operator is asking about?
41520
+ *
41521
+ * THE gate for every debug crop, and it must stay this cheap: one map lookup,
41522
+ * no store read, no allocation. An unknown track is not marked — a lookup
41523
+ * that finds nothing is the common case on every camera in the fleet.
41524
+ */
41525
+ isDebugMarked(trackId) {
41526
+ return this.residents.get(trackId)?.debugMarked ?? false;
41527
+ }
41528
+ /**
41529
+ * Record the operator's `debug` flag for a LIVE track.
41530
+ *
41531
+ * Called from the `setTrackFlags` cap mutation, which is the only thing that
41532
+ * can turn the flag on — so this runs once per press, not per frame.
41533
+ * `ensure` rather than a lookup: the button can be pressed on a track whose
41534
+ * first frame is still in flight, and a mark that silently missed would be
41535
+ * the operator clicking and getting nothing, which is the whole complaint.
41536
+ */
41537
+ setDebugMarked(deviceId, trackId, marked) {
41538
+ const resident = this.ensure(deviceId, trackId);
41539
+ if (resident.debugMarked === marked) return;
41540
+ resident.debugMarked = marked;
41541
+ this.debugMarkedCount += marked ? 1 : -1;
41542
+ }
41543
+ /**
41544
+ * Does ANY live track on this runner carry the flag?
41545
+ *
41546
+ * The frame path's own gate, one level above {@link isDebugMarked}: it lets
41547
+ * the host skip the per-frame bookkeeping a debug crop needs (the frame
41548
+ * handle mirror the salvage-bind observer reads) while nothing is marked —
41549
+ * which is the fleet's normal state. A counter read, no scan, no allocation.
41550
+ */
41551
+ anyDebugMarked() {
41552
+ return this.debugMarkedCount > 0;
41553
+ }
41474
41554
  /** Retain the FIRST detection-raster crop per track (zero-media fallback);
41475
41555
  * a later frame never overwrites an existing one. */
41476
41556
  retainRasterFallback(deviceId, trackId, fallback) {
@@ -48682,6 +48762,298 @@ async function extractCropRaw(frameData, frameWidth, frameHeight, bbox) {
48682
48762
  };
48683
48763
  }
48684
48764
  //#endregion
48765
+ //#region src/pipeline-analytics/pipeline/bbox-pad.ts
48766
+ /**
48767
+ * Pad a normalized bounding box by a fractional amount relative to its own
48768
+ * dimensions. All coordinates remain in the [0, 1] unit-square.
48769
+ *
48770
+ * Formula (before clamping):
48771
+ * newX = x - padding * w
48772
+ * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
48773
+ * newY = y - padding * h
48774
+ * newH = h + 2 * padding * h
48775
+ *
48776
+ * padding === 0 is the identity operation. Pure, immutable — always returns a
48777
+ * new object.
48778
+ *
48779
+ * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
48780
+ * bound slides inward and keeps the extent that was asked for, on either side.
48781
+ * It shrinks only when the padded window is larger than the frame itself
48782
+ * would (e.g. a left-hugging box gains a bit more right-side context). This is
48783
+ * acceptable for detection crops (more context, never out of [0,1]); the
48784
+ * downstream `extractCrop` clamps to pixel bounds again regardless.
48785
+ */
48786
+ function padBbox(bbox, padding) {
48787
+ const rawX = bbox.x - padding * bbox.w;
48788
+ const rawY = bbox.y - padding * bbox.h;
48789
+ const rawW = bbox.w * (1 + 2 * padding);
48790
+ const rawH = bbox.h * (1 + 2 * padding);
48791
+ const w = Math.min(rawW, 1);
48792
+ const h = Math.min(rawH, 1);
48793
+ return {
48794
+ x: Math.min(Math.max(0, rawX), 1 - w),
48795
+ y: Math.min(Math.max(0, rawY), 1 - h),
48796
+ w,
48797
+ h
48798
+ };
48799
+ }
48800
+ /**
48801
+ * Legibility floor: the window's SHORT side is grown until it is at least this
48802
+ * fraction of the frame's short side.
48803
+ *
48804
+ * The 2026-09-18 traces included a 24x18 px subject. At the context ratio alone
48805
+ * that is a 72x54 window — and 72x54 source pixels are 72x54 delivered pixels,
48806
+ * because nothing in this repo may upscale. Growing the RECTANGLE is the only
48807
+ * honest way to make the picture bigger: more real pixels arrive, the subject
48808
+ * occupies less of the frame, and its drawn box says which part of it is the
48809
+ * subject. 0.18 of 1080 = 194 px, which renders a 24 px subject recognisably in
48810
+ * its surroundings.
48811
+ */
48812
+ var DEBUG_CROP_MIN_SIDE_FRAC = .18;
48813
+ function unionBox(a, b) {
48814
+ const x = Math.min(a.x, b.x);
48815
+ const y = Math.min(a.y, b.y);
48816
+ const right = Math.max(a.x + a.w, b.x + b.w);
48817
+ const bottom = Math.max(a.y + a.h, b.y + b.h);
48818
+ return {
48819
+ x,
48820
+ y,
48821
+ w: right - x,
48822
+ h: bottom - y
48823
+ };
48824
+ }
48825
+ /**
48826
+ * Which borders a box touches, on `edge-clear.ts`'s tolerance band — the SAME
48827
+ * band `isEdgeClear` uses, so "clipped" here and "not edge-clear" there can
48828
+ * never disagree about one frame.
48829
+ */
48830
+ function debugCropClipSides(bbox, frameWidth, frameHeight) {
48831
+ if (frameWidth <= 0 || frameHeight <= 0) return [];
48832
+ const tolX = DEFAULT_EDGE_TOLERANCE * frameWidth;
48833
+ const tolY = DEFAULT_EDGE_TOLERANCE * frameHeight;
48834
+ const sides = [];
48835
+ if (bbox.x <= tolX) sides.push("left");
48836
+ if (bbox.y <= tolY) sides.push("top");
48837
+ if (bbox.x + bbox.w >= frameWidth - tolX) sides.push("right");
48838
+ if (bbox.y + bbox.h >= frameHeight - tolY) sides.push("bottom");
48839
+ return sides;
48840
+ }
48841
+ /**
48842
+ * The ratio that makes `base` reach the legibility floor, or
48843
+ * {@link DEBUG_CROP_CONTEXT_RATIO} when it already does.
48844
+ *
48845
+ * Expressed as a PADDING RATIO rather than as a second rectangle so the window
48846
+ * still comes out of one {@link padBbox} call: there is exactly one place in
48847
+ * this file where a rectangle is grown, and it is not here.
48848
+ */
48849
+ function contextRatioFor(base, frameWidth, frameHeight) {
48850
+ const minSide = DEBUG_CROP_MIN_SIDE_FRAC * Math.min(frameWidth, frameHeight);
48851
+ const ratioFor = (extent) => extent <= 0 ? 1 : (minSide / extent - 1) / 2;
48852
+ const needed = Math.max(ratioFor(base.w), ratioFor(base.h));
48853
+ return Math.max(1, needed);
48854
+ }
48855
+ /**
48856
+ * The one debug-crop rectangle. Pure.
48857
+ *
48858
+ * A refusal is a VALUE, not a `null`: the caller logs the reason, because a
48859
+ * branch that stores nothing and says nothing reads as "never happened".
48860
+ */
48861
+ function planDebugCrop(input) {
48862
+ const { frameWidth, frameHeight, subject } = input;
48863
+ if (frameWidth <= 0 || frameHeight <= 0) return {
48864
+ kind: "refused",
48865
+ reason: "degenerate-frame"
48866
+ };
48867
+ if (subject.w <= 0 || subject.h <= 0) return {
48868
+ kind: "refused",
48869
+ reason: "degenerate-subject"
48870
+ };
48871
+ const base = input.rival === void 0 ? subject : unionBox(subject, input.rival);
48872
+ const contextRatio = contextRatioFor(base, frameWidth, frameHeight);
48873
+ const norm = padBbox({
48874
+ x: base.x / frameWidth,
48875
+ y: base.y / frameHeight,
48876
+ w: base.w / frameWidth,
48877
+ h: base.h / frameHeight
48878
+ }, contextRatio);
48879
+ const rect = {
48880
+ x: norm.x * frameWidth,
48881
+ y: norm.y * frameHeight,
48882
+ w: norm.w * frameWidth,
48883
+ h: norm.h * frameHeight
48884
+ };
48885
+ const relative = (role, box) => ({
48886
+ role,
48887
+ x: box.x - rect.x,
48888
+ y: box.y - rect.y,
48889
+ w: box.w,
48890
+ h: box.h
48891
+ });
48892
+ const boxes = input.rival === void 0 ? [relative("subject", subject)] : [relative("subject", subject), relative("rival", input.rival)];
48893
+ return {
48894
+ kind: "plan",
48895
+ decision: input.decision,
48896
+ rect,
48897
+ norm,
48898
+ boxes,
48899
+ contextRatio,
48900
+ floorApplied: contextRatio > 1,
48901
+ clippedSides: debugCropClipSides(subject, frameWidth, frameHeight),
48902
+ subjectCenterNorm: {
48903
+ x: (subject.x + subject.w / 2) / frameWidth,
48904
+ y: (subject.y + subject.h / 2) / frameHeight
48905
+ }
48906
+ };
48907
+ }
48908
+ function formatRect(rect) {
48909
+ return `${Math.round(rect.x)},${Math.round(rect.y)} ${Math.round(rect.w)}x${Math.round(rect.h)}`;
48910
+ }
48911
+ /**
48912
+ * The attribution bag: the deciding site's evidence FIRST, then the structural
48913
+ * facts — so no evidence key can rename the decision, the track or the
48914
+ * rectangle the crop belongs to. That ordering is the whole point: a crop whose
48915
+ * own label can be overwritten by a caller is back to being unattributable.
48916
+ */
48917
+ function debugCropMeta(plan, attribution) {
48918
+ return {
48919
+ ...attribution.evidence,
48920
+ decision: attribution.decision,
48921
+ trackId: attribution.trackId,
48922
+ ...attribution.rivalTrackId === void 0 ? {} : { rivalTrackId: attribution.rivalTrackId },
48923
+ timestamp: attribution.timestamp,
48924
+ cropRect: formatRect(plan.rect),
48925
+ contextRatio: plan.contextRatio,
48926
+ floorApplied: plan.floorApplied,
48927
+ clippedSides: plan.clippedSides.join("|"),
48928
+ subjectCenter: `${plan.subjectCenterNorm.x.toFixed(3)},${plan.subjectCenterNorm.y.toFixed(3)}`
48929
+ };
48930
+ }
48931
+ /**
48932
+ * The caption burnt into the crop's own pixels.
48933
+ *
48934
+ * Burnt in, not stored beside it, because a debug crop travels: it is exported,
48935
+ * pasted into a note, and looked at months later next to five others. A blob
48936
+ * that cannot say what it is, on its own, is the thing that made the last
48937
+ * investigation slow — and this store has no column to say it in.
48938
+ */
48939
+ function debugCropLabel(plan, attribution) {
48940
+ return `${`${attribution.decision} ${attribution.trackId}`}${attribution.rivalTrackId === void 0 ? "" : ` vs ${attribution.rivalTrackId}`}${plan.clippedSides.length === 0 ? "" : ` clipped:${plan.clippedSides.join("+")}`} ${Object.entries(attribution.evidence).map(([k, v]) => `${k}=${typeof v === "number" ? Math.round(v * 1e3) / 1e3 : String(v)}`).join(" ")}`.trim().slice(0, 160);
48941
+ }
48942
+ //#endregion
48943
+ //#region src/pipeline-analytics/pipeline/debug-crop-capture.ts
48944
+ /** Stroke colour of the box the decision was ABOUT. */
48945
+ var SUBJECT_COLOR = "#00e5ff";
48946
+ /** Stroke colour of the box it was weighed AGAINST — a different colour,
48947
+ * because "which one is which" is the first question a two-box crop raises. */
48948
+ var RIVAL_COLOR = "#ff9500";
48949
+ /** Encode quality: a debug crop is read by a person looking for a defect, so it
48950
+ * is encoded above the timeline-tile default (80) — a JPEG artefact that reads
48951
+ * as a mirror edge would waste the very investigation this exists for. */
48952
+ var DEBUG_CROP_QUALITY = 92;
48953
+ /** Bound the per-track counters so a long-lived process never leaks them. */
48954
+ var MAX_TRACKED_TRACKS = 2e3;
48955
+ function createDebugCropCapture(deps) {
48956
+ const storedPerTrack = /* @__PURE__ */ new Map();
48957
+ const cappedAnnounced = /* @__PURE__ */ new Set();
48958
+ const skip = (input, reason, extra) => {
48959
+ deps.logger.info("debug crop not stored", {
48960
+ tags: { deviceId: input.deviceId },
48961
+ meta: {
48962
+ reason,
48963
+ decision: input.decision,
48964
+ trackId: input.trackId,
48965
+ timestamp: input.timestamp,
48966
+ ...extra
48967
+ }
48968
+ });
48969
+ return null;
48970
+ };
48971
+ return async (input) => {
48972
+ if (!deps.isDebugMarked(input.trackId)) return null;
48973
+ const already = storedPerTrack.get(input.trackId) ?? 0;
48974
+ if (already >= 6) {
48975
+ if (cappedAnnounced.has(input.trackId)) return null;
48976
+ if (cappedAnnounced.size >= MAX_TRACKED_TRACKS) cappedAnnounced.clear();
48977
+ cappedAnnounced.add(input.trackId);
48978
+ return skip(input, "per-track-cap", {
48979
+ stored: already,
48980
+ cap: 6
48981
+ });
48982
+ }
48983
+ const plan = planDebugCrop({
48984
+ decision: input.decision,
48985
+ subject: input.subject,
48986
+ ...input.rival === void 0 ? {} : { rival: input.rival },
48987
+ frameWidth: input.frameWidth,
48988
+ frameHeight: input.frameHeight
48989
+ });
48990
+ if (plan.kind === "refused") return skip(input, plan.reason, {});
48991
+ const attribution = {
48992
+ decision: input.decision,
48993
+ trackId: input.trackId,
48994
+ ...input.rivalTrackId === void 0 ? {} : { rivalTrackId: input.rivalTrackId },
48995
+ timestamp: input.timestamp,
48996
+ evidence: input.evidence
48997
+ };
48998
+ const meta = debugCropMeta(plan, attribution);
48999
+ const frame = await deps.fetchFrameRgb(input.frameHandle, input.deviceId, input.trackId);
49000
+ if (frame === null || frame.width <= 0 || frame.height <= 0) return skip(input, "frame-unavailable", { nodeId: input.frameHandle.nodeId });
49001
+ let jpeg;
49002
+ try {
49003
+ const cut = await deps.extractCropRaw(frame.bytes, frame.width, frame.height, plan.norm);
49004
+ const scale = plan.rect.w > 0 ? cut.width / plan.rect.w : 1;
49005
+ const boxes = plan.boxes.map((b) => {
49006
+ const geometry = {
49007
+ x: b.x * scale,
49008
+ y: b.y * scale,
49009
+ w: b.w * scale,
49010
+ h: b.h * scale
49011
+ };
49012
+ return b.role === "subject" ? {
49013
+ ...geometry,
49014
+ color: SUBJECT_COLOR,
49015
+ label: debugCropLabel(plan, attribution)
49016
+ } : {
49017
+ ...geometry,
49018
+ color: RIVAL_COLOR,
49019
+ label: "rival"
49020
+ };
49021
+ });
49022
+ jpeg = await deps.drawBoxedFrame(cut.data, cut.width, cut.height, boxes, { quality: DEBUG_CROP_QUALITY });
49023
+ } catch (err) {
49024
+ return skip(input, "render-failed", { error: err instanceof Error ? err.message : String(err) });
49025
+ }
49026
+ try {
49027
+ const mediaId = await deps.putDebugCrop({
49028
+ deviceId: input.deviceId,
49029
+ trackId: input.trackId,
49030
+ timestamp: input.timestamp,
49031
+ data: jpeg
49032
+ });
49033
+ if (storedPerTrack.size >= MAX_TRACKED_TRACKS) storedPerTrack.clear();
49034
+ storedPerTrack.set(input.trackId, already + 1);
49035
+ deps.logger.info("debug crop stored", {
49036
+ tags: { deviceId: input.deviceId },
49037
+ meta: {
49038
+ ...meta,
49039
+ mediaId,
49040
+ deviceId: input.deviceId
49041
+ }
49042
+ });
49043
+ return mediaId;
49044
+ } catch (err) {
49045
+ deps.logger.warn("debug crop store failed", {
49046
+ tags: { deviceId: input.deviceId },
49047
+ meta: {
49048
+ ...meta,
49049
+ error: err instanceof Error ? err.message : String(err)
49050
+ }
49051
+ });
49052
+ return null;
49053
+ }
49054
+ };
49055
+ }
49056
+ //#endregion
48685
49057
  //#region src/shared/frame/encode-crop.ts
48686
49058
  /**
48687
49059
  * JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
@@ -53473,42 +53845,6 @@ var AnalyticsFailureReport = class {
53473
53845
  }
53474
53846
  };
53475
53847
  //#endregion
53476
- //#region src/pipeline-analytics/pipeline/bbox-pad.ts
53477
- /**
53478
- * Pad a normalized bounding box by a fractional amount relative to its own
53479
- * dimensions. All coordinates remain in the [0, 1] unit-square.
53480
- *
53481
- * Formula (before clamping):
53482
- * newX = x - padding * w
53483
- * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
53484
- * newY = y - padding * h
53485
- * newH = h + 2 * padding * h
53486
- *
53487
- * padding === 0 is the identity operation. Pure, immutable — always returns a
53488
- * new object.
53489
- *
53490
- * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
53491
- * bound slides inward and keeps the extent that was asked for, on either side.
53492
- * It shrinks only when the padded window is larger than the frame itself
53493
- * would (e.g. a left-hugging box gains a bit more right-side context). This is
53494
- * acceptable for detection crops (more context, never out of [0,1]); the
53495
- * downstream `extractCrop` clamps to pixel bounds again regardless.
53496
- */
53497
- function padBbox(bbox, padding) {
53498
- const rawX = bbox.x - padding * bbox.w;
53499
- const rawY = bbox.y - padding * bbox.h;
53500
- const rawW = bbox.w * (1 + 2 * padding);
53501
- const rawH = bbox.h * (1 + 2 * padding);
53502
- const w = Math.min(rawW, 1);
53503
- const h = Math.min(rawH, 1);
53504
- return {
53505
- x: Math.min(Math.max(0, rawX), 1 - w),
53506
- y: Math.min(Math.max(0, rawY), 1 - h),
53507
- w,
53508
- h
53509
- };
53510
- }
53511
- //#endregion
53512
53848
  //#region src/pipeline-analytics/pipeline/capture-crop.ts
53513
53849
  /**
53514
53850
  * The normalised window a capture asks the runner for: the box padded by
@@ -75901,6 +76237,19 @@ var customActions = {
75901
76237
  ...faceRescoreActions
75902
76238
  };
75903
76239
  /**
76240
+ * Width cap on the ONE full frame a debug crop is cut from.
76241
+ *
76242
+ * A debug crop is a picture for a person, and the plan has already widened the
76243
+ * window to a neighbourhood — past this width the extra pixels buy nothing and
76244
+ * a 4K native surface would cross the transport for every marked track.
76245
+ */
76246
+ var DEBUG_CROP_FETCH_MAX_WIDTH = 1280;
76247
+ /** How long a queued debug-crop frame fetch is still worth executing. Past
76248
+ * this the handle's frame has aged out of the runner's retention and the next
76249
+ * decision will ask again with a young one, so the queued entry is dead
76250
+ * weight in a lane real media is waiting in. */
76251
+ var DEBUG_CROP_FETCH_STALE_MS = 2e3;
76252
+ /**
75904
76253
  * Assist threshold when the caller does not pin one. Packages have their own
75905
76254
  * (0.35, on the zone crop only — `retrain/retrain-assist.ts`); everything else
75906
76255
  * starts here and the operator drops the ones they do not want.
@@ -76934,7 +77283,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
76934
77283
  * `docs/decisions/` — the gate is a model input, so this is a deliberate
76935
77284
  * exception to "model-input crops keep captureCrop".
76936
77285
  */
77286
+ /**
77287
+ * The operator-debug crop capture (`pipeline/debug-crop-capture.ts`).
77288
+ *
77289
+ * Null until the transport and the media store exist. Reached from three
77290
+ * decision sites and gated INSIDE the capture on the track's `debug` flag, so
77291
+ * a camera with no marked track pays one map lookup per decision — see the
77292
+ * capture's module doc for why the gate lives there and not at each site.
77293
+ */
77294
+ captureDebugCropFn = null;
76937
77295
  captureDisplayCropFn = null;
77296
+ /**
77297
+ * Offer one decision's pixels to the operator-debug crop capture.
77298
+ *
77299
+ * Fire-and-forget and NEVER throws: this is the addon's own bookkeeping, and
77300
+ * a frame path that can be slowed or broken by a diagnostic is worse than no
77301
+ * diagnostic. The gate, the per-track cap and all six refusal reasons live in
77302
+ * the capture (`pipeline/debug-crop-capture.ts`); this is only the seam.
77303
+ */
77304
+ /**
77305
+ * The CURRENT frame's handle and dims, per camera — the one thing the
77306
+ * salvage-bind observer lacks.
77307
+ *
77308
+ * The tracker knows no `deviceId`, holds no handle and writes no media (that
77309
+ * is the point of `pipeline/tracker/**`), so a growth bind reports its two
77310
+ * boxes and stops. This mirror is what lets the host put those boxes back on
77311
+ * a picture. Written ONLY while some track on this runner is marked
77312
+ * (`residents.anyDebugMarked()`), so an ordinary fleet does not allocate one
77313
+ * object per frame per camera for a diagnostic nobody asked for.
77314
+ */
77315
+ debugFrameContext = /* @__PURE__ */ new Map();
77316
+ offerDebugCrop(input) {
77317
+ const capture = this.captureDebugCropFn;
77318
+ if (capture === null) return;
77319
+ capture(input).catch((err) => {
77320
+ this.ctx.logger.debug("debug crop offer threw", {
77321
+ tags: { deviceId: input.deviceId },
77322
+ meta: {
77323
+ deviceId: input.deviceId,
77324
+ trackId: input.trackId,
77325
+ decision: input.decision,
77326
+ error: err instanceof Error ? err.message : String(err)
77327
+ }
77328
+ });
77329
+ });
77330
+ }
76938
77331
  captureCrop = null;
76939
77332
  /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
76940
77333
  * to the owning runner, PLUS the source `tier`. Captured in the async init
@@ -77299,6 +77692,36 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
77299
77692
  isNodeOffline: (nodeId) => this.nodeLiveness.isKnownOffline(nodeId, Date.now())
77300
77693
  });
77301
77694
  this.captureCrop = transport.captureCrop;
77695
+ const fetchDebugFullFrame = transport.getNativeFullFrameRgb;
77696
+ this.captureDebugCropFn = createDebugCropCapture({
77697
+ isDebugMarked: (trackId) => this.residents.isDebugMarked(trackId),
77698
+ fetchFrameRgb: async (handle, deviceId, trackId) => {
77699
+ const frame = await this.captureScheduler.request({
77700
+ deviceId,
77701
+ trackId,
77702
+ kind: "other",
77703
+ coalesceKey: captureCoalesceKey(deviceId, "other", trackId),
77704
+ staleAfterMs: DEBUG_CROP_FETCH_STALE_MS,
77705
+ exec: () => fetchDebugFullFrame(handle, DEBUG_CROP_FETCH_MAX_WIDTH)
77706
+ });
77707
+ return frame === null || frame === void 0 ? null : {
77708
+ bytes: frame.data,
77709
+ width: frame.width,
77710
+ height: frame.height
77711
+ };
77712
+ },
77713
+ extractCropRaw: (data, w, h, box) => extractCropRaw(data, w, h, box),
77714
+ drawBoxedFrame: (data, w, h, boxes, options) => drawBoxedFrame(data, w, h, boxes, options ?? {}),
77715
+ putDebugCrop: (put) => stores.mediaStore.put({
77716
+ deviceId: put.deviceId,
77717
+ ownerKind: "track",
77718
+ ownerId: put.trackId,
77719
+ kind: "debugCrop",
77720
+ timestamp: put.timestamp,
77721
+ data: put.data
77722
+ }),
77723
+ logger: logger.child("DebugCrop")
77724
+ });
77302
77725
  this.eventMediaDispatcher = new EventMediaDispatcher({
77303
77726
  getRemoteFrame: transport.getRemoteFrame,
77304
77727
  getNativeCropJpeg: transport.getNativeCropJpeg,
@@ -80299,6 +80722,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
80299
80722
  frameHeight: result.frameHeight
80300
80723
  }, exhaustedIds);
80301
80724
  const outcome = run.outcome;
80725
+ if (frameHandle !== void 0 && this.residents.anyDebugMarked()) this.debugFrameContext.set(deviceId, {
80726
+ handle: frameHandle,
80727
+ width: result.frameWidth,
80728
+ height: result.frameHeight,
80729
+ timestamp: result.timestamp
80730
+ });
80302
80731
  const gateNowMs = Date.now();
80303
80732
  for (const { id, t } of bornCandidates) {
80304
80733
  if (outcome.undecided.has(id)) {
@@ -80357,6 +80786,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
80357
80786
  error: String(err)
80358
80787
  } });
80359
80788
  });
80789
+ if (frameHandle !== void 0) {
80790
+ const decision = run.decisions.get(id);
80791
+ this.offerDebugCrop({
80792
+ deviceId,
80793
+ trackId: id,
80794
+ decision: "birth-confirmation",
80795
+ subject: {
80796
+ x: t.bbox.x,
80797
+ y: t.bbox.y,
80798
+ w: t.bbox.w,
80799
+ h: t.bbox.h
80800
+ },
80801
+ frameHandle,
80802
+ frameWidth: result.frameWidth,
80803
+ frameHeight: result.frameHeight,
80804
+ timestamp: gateNowMs,
80805
+ evidence: {
80806
+ className: t.className,
80807
+ source,
80808
+ verdict: "suppression-held",
80809
+ ...decision?.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
80810
+ ...decision?.bestScore !== void 0 ? { bestScore: decision.bestScore } : {},
80811
+ judgedCropKept: judged !== void 0
80812
+ }
80813
+ });
80814
+ }
80360
80815
  log.info("birth suppression held — the gate is not this root detector", {
80361
80816
  tags: { deviceId },
80362
80817
  meta: {
@@ -82385,6 +82840,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
82385
82840
  meta: { ...bestShotPromotionMeta(promotionAttempt) }
82386
82841
  });
82387
82842
  }
82843
+ if (frameHandle !== void 0) {
82844
+ const heldBbox = this.residents.bestSeenBbox(t.trackId);
82845
+ const rival = heldBbox !== void 0 && heldBbox.frameWidth === frameWidth && heldBbox.frameHeight === frameHeight ? {
82846
+ x: heldBbox.x,
82847
+ y: heldBbox.y,
82848
+ w: heldBbox.w,
82849
+ h: heldBbox.h
82850
+ } : void 0;
82851
+ this.offerDebugCrop({
82852
+ deviceId,
82853
+ trackId: t.trackId,
82854
+ decision: "best-shot",
82855
+ subject: {
82856
+ x: t.bbox.x,
82857
+ y: t.bbox.y,
82858
+ w: t.bbox.w,
82859
+ h: t.bbox.h
82860
+ },
82861
+ ...rival !== void 0 ? { rival } : {},
82862
+ frameHandle,
82863
+ frameWidth,
82864
+ frameHeight,
82865
+ timestamp,
82866
+ evidence: { ...bestShotPromotionMeta(promotionAttempt) }
82867
+ });
82868
+ }
82388
82869
  if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && keyFrame === "skip") continue;
82389
82870
  targets.push({
82390
82871
  trackId: t.trackId,
@@ -83738,6 +84219,33 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
83738
84219
  ...record
83739
84220
  }
83740
84221
  });
84222
+ if (record.gate !== "growth") return;
84223
+ const frame = this.debugFrameContext.get(deviceId);
84224
+ if (frame === void 0 || frame.width !== record.frameWidth) return;
84225
+ this.offerDebugCrop({
84226
+ deviceId,
84227
+ trackId: record.trackId,
84228
+ decision: "growth-rescue",
84229
+ subject: record.trackBbox,
84230
+ rival: record.detBbox,
84231
+ frameHandle: frame.handle,
84232
+ frameWidth: record.frameWidth,
84233
+ frameHeight: record.frameHeight,
84234
+ timestamp: frame.timestamp,
84235
+ evidence: {
84236
+ stage: record.stage,
84237
+ trackClass: record.trackClass,
84238
+ detClass: record.detClass,
84239
+ detScore: record.detScore,
84240
+ iou: record.iou,
84241
+ centroidDistPx: record.centroidDistPx,
84242
+ trackDiagPx: record.trackDiagPx,
84243
+ areaRatio: record.areaRatio,
84244
+ dtMs: record.dtMs,
84245
+ trackAgeMs: record.trackAgeMs,
84246
+ source
84247
+ }
84248
+ });
83741
84249
  });
83742
84250
  p.setSpawnRefusalObserver((record) => {
83743
84251
  this.ctx.logger.info("tracker: detection dropped — nothing downstream received it", {
@@ -85884,6 +86392,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
85884
86392
  logger: this.ctx.logger,
85885
86393
  ...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
85886
86394
  }, input);
86395
+ this.residents.setDebugMarked(input.deviceId, input.trackId, out.debug);
85887
86396
  this.ctx.logger.info("track operator flags set", {
85888
86397
  tags: { deviceId: input.deviceId },
85889
86398
  meta: {