@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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-ZhDqWNZF.js");
5
+ const require_dist = require("../dist-Cgy2NW9x.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -37075,6 +37075,7 @@ function growthAdjacentMatch(track, det, ctx) {
37075
37075
  const trackArea = track.w * track.h;
37076
37076
  const detArea = det.w * det.h;
37077
37077
  if (trackArea <= 0 || detArea <= 0) return false;
37078
+ if (ctx.frameArea > 0 && trackArea / ctx.frameArea > .01) return false;
37078
37079
  const larger = Math.max(trackArea, detArea);
37079
37080
  const smaller = Math.min(trackArea, detArea);
37080
37081
  if (larger / smaller < 1.5) return false;
@@ -37228,6 +37229,17 @@ var SortTracker = class SortTracker {
37228
37229
  * and a busy one span the same seconds (D438).
37229
37230
  */
37230
37231
  frameSeq = 0;
37232
+ /**
37233
+ * The area of the frame this pass is running over, or `0` when the caller
37234
+ * gave no frame context. Read by the growth clause's FRAGMENT test, which
37235
+ * three call sites reach and only one of them could have been handed it.
37236
+ */
37237
+ frameArea = 0;
37238
+ /** The frame DIMS of the pass, for the same reason `frameArea` is held: the
37239
+ * salvage-bind record carries both boxes, and a box without the frame it was
37240
+ * measured in cannot be put back on a picture. `0` = no frame context. */
37241
+ frameWidthPx = 0;
37242
+ frameHeightPx = 0;
37231
37243
  /** Sink for track deaths — see {@link setTrackLossObserver}. */
37232
37244
  trackLossObserver = null;
37233
37245
  trackLossSampler = new TrackLossSampler();
@@ -37345,7 +37357,7 @@ var SortTracker = class SortTracker {
37345
37357
  if (!this.config.reacquireTimeScaled) return "plausible-birth-veto";
37346
37358
  return this.isUnambiguousReach(track, det, detections) ? "already-claimed" : "crowd-guard";
37347
37359
  }
37348
- if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "already-claimed";
37360
+ if (growthAdjacentMatch(track.bbox, det.bbox, this.growthContext(track, timestamp))) return "already-claimed";
37349
37361
  return "geometry";
37350
37362
  }
37351
37363
  /** Emit one dropped-detection record, subject to the refusal sampler. */
@@ -37465,6 +37477,20 @@ var SortTracker = class SortTracker {
37465
37477
  centroidDistPx: Math.hypot(tc.x - dc.x, tc.y - dc.y),
37466
37478
  trackDiagPx: Math.hypot(track.bbox.w, track.bbox.h),
37467
37479
  areaRatio: smaller > 0 ? Math.max(trackArea, detArea) / smaller : 0,
37480
+ trackBbox: {
37481
+ x: track.bbox.x,
37482
+ y: track.bbox.y,
37483
+ w: track.bbox.w,
37484
+ h: track.bbox.h
37485
+ },
37486
+ detBbox: {
37487
+ x: det.bbox.x,
37488
+ y: det.bbox.y,
37489
+ w: det.bbox.w,
37490
+ h: det.bbox.h
37491
+ },
37492
+ frameWidth: this.frameWidthPx,
37493
+ frameHeight: this.frameHeightPx,
37468
37494
  dtMs: timestamp - track.lastSeen,
37469
37495
  trackAgeMs: timestamp - track.firstSeen,
37470
37496
  bindCount,
@@ -37472,10 +37498,11 @@ var SortTracker = class SortTracker {
37472
37498
  });
37473
37499
  }
37474
37500
  /** When the pair is being judged, relative to `track`. */
37475
- static growthContext(track, timestamp) {
37501
+ growthContext(track, timestamp) {
37476
37502
  return {
37477
37503
  trackAgeMs: timestamp - track.firstSeen,
37478
- gapMs: timestamp - track.lastSeen
37504
+ gapMs: timestamp - track.lastSeen,
37505
+ frameArea: this.frameArea
37479
37506
  };
37480
37507
  }
37481
37508
  /** Whether two macro-classes may be collapsed as the SAME subject. Same class
@@ -37604,7 +37631,7 @@ var SortTracker = class SortTracker {
37604
37631
  });
37605
37632
  return true;
37606
37633
  }
37607
- if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
37634
+ if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, this.growthContext(t, timestamp))) {
37608
37635
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
37609
37636
  return true;
37610
37637
  }
@@ -37779,7 +37806,7 @@ var SortTracker = class SortTracker {
37779
37806
  const tc = bboxCentroid(track.bbox);
37780
37807
  const dc = bboxCentroid(det.bbox);
37781
37808
  if (Math.hypot(tc.x - dc.x, tc.y - dc.y) <= this.reacquireReach(track, timestamp, timeScaled)) return "centroid";
37782
- if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "growth";
37809
+ if (growthAdjacentMatch(track.bbox, det.bbox, this.growthContext(track, timestamp))) return "growth";
37783
37810
  return null;
37784
37811
  }
37785
37812
  /**
@@ -38007,6 +38034,9 @@ var SortTracker = class SortTracker {
38007
38034
  update(detections, timestamp, frameContext) {
38008
38035
  this.frameSeq++;
38009
38036
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
38037
+ this.frameArea = frameArea;
38038
+ this.frameWidthPx = frameContext?.frameWidth ?? 0;
38039
+ this.frameHeightPx = frameContext?.frameHeight ?? 0;
38010
38040
  const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
38011
38041
  const resurrectedScene = /* @__PURE__ */ new Map();
38012
38042
  if (this.config.maxTrackLifetimeMs > 0) {
@@ -41251,6 +41281,17 @@ var TrackResidentState = class {
41251
41281
  objectEmbeddingBestSelector = new TrackBestSelector(BEST_OBJECT_EMBED_POLICY);
41252
41282
  /** keyFrame keys of CLOSED tracks still readable until the recognizer-end
41253
41283
  * deferred drop settles (see module docstring). */
41284
+ /**
41285
+ * How many LIVE tracks carry the operator's `debug` flag, across every camera
41286
+ * this runner holds.
41287
+ *
41288
+ * A counter rather than a scan because the frame path asks it every frame:
41289
+ * it is what lets the host skip maintaining a per-frame handle mirror
41290
+ * entirely while nothing is marked, which is the state of the fleet
41291
+ * essentially always. Kept in step in the three places a resident's flag can
41292
+ * change: {@link setDebugMarked}, {@link close} and {@link clearAll}.
41293
+ */
41294
+ debugMarkedCount = 0;
41254
41295
  deferredKeyFrameKeys = /* @__PURE__ */ new Map();
41255
41296
  constructor(hooks = {}) {
41256
41297
  this.hooks = hooks;
@@ -41272,7 +41313,8 @@ var TrackResidentState = class {
41272
41313
  thumbnailLanded: false,
41273
41314
  keyFrameLanded: false,
41274
41315
  keyFrameRetries: 0,
41275
- confirmed: false
41316
+ confirmed: false,
41317
+ debugMarked: false
41276
41318
  };
41277
41319
  this.residents.set(trackId, created);
41278
41320
  return created;
@@ -41286,6 +41328,7 @@ var TrackResidentState = class {
41286
41328
  */
41287
41329
  close(trackId, mode) {
41288
41330
  const resident = this.residents.get(trackId);
41331
+ if (resident?.debugMarked === true) this.debugMarkedCount -= 1;
41289
41332
  this.residents.delete(trackId);
41290
41333
  this.bestFrameTracker.delete(trackId);
41291
41334
  this.objectEmbeddingBestSelector.delete(trackId);
@@ -41305,6 +41348,7 @@ var TrackResidentState = class {
41305
41348
  }
41306
41349
  /** Drop everything (addon dispose). */
41307
41350
  clearAll() {
41351
+ this.debugMarkedCount = 0;
41308
41352
  this.residents.clear();
41309
41353
  this.bestFrameTracker.clear();
41310
41354
  this.objectEmbeddingBestSelector.clear();
@@ -41496,6 +41540,42 @@ var TrackResidentState = class {
41496
41540
  markConfirmed(deviceId, trackId) {
41497
41541
  this.ensure(deviceId, trackId).confirmed = true;
41498
41542
  }
41543
+ /**
41544
+ * Is this track the one the operator is asking about?
41545
+ *
41546
+ * THE gate for every debug crop, and it must stay this cheap: one map lookup,
41547
+ * no store read, no allocation. An unknown track is not marked — a lookup
41548
+ * that finds nothing is the common case on every camera in the fleet.
41549
+ */
41550
+ isDebugMarked(trackId) {
41551
+ return this.residents.get(trackId)?.debugMarked ?? false;
41552
+ }
41553
+ /**
41554
+ * Record the operator's `debug` flag for a LIVE track.
41555
+ *
41556
+ * Called from the `setTrackFlags` cap mutation, which is the only thing that
41557
+ * can turn the flag on — so this runs once per press, not per frame.
41558
+ * `ensure` rather than a lookup: the button can be pressed on a track whose
41559
+ * first frame is still in flight, and a mark that silently missed would be
41560
+ * the operator clicking and getting nothing, which is the whole complaint.
41561
+ */
41562
+ setDebugMarked(deviceId, trackId, marked) {
41563
+ const resident = this.ensure(deviceId, trackId);
41564
+ if (resident.debugMarked === marked) return;
41565
+ resident.debugMarked = marked;
41566
+ this.debugMarkedCount += marked ? 1 : -1;
41567
+ }
41568
+ /**
41569
+ * Does ANY live track on this runner carry the flag?
41570
+ *
41571
+ * The frame path's own gate, one level above {@link isDebugMarked}: it lets
41572
+ * the host skip the per-frame bookkeeping a debug crop needs (the frame
41573
+ * handle mirror the salvage-bind observer reads) while nothing is marked —
41574
+ * which is the fleet's normal state. A counter read, no scan, no allocation.
41575
+ */
41576
+ anyDebugMarked() {
41577
+ return this.debugMarkedCount > 0;
41578
+ }
41499
41579
  /** Retain the FIRST detection-raster crop per track (zero-media fallback);
41500
41580
  * a later frame never overwrites an existing one. */
41501
41581
  retainRasterFallback(deviceId, trackId, fallback) {
@@ -48756,6 +48836,298 @@ async function extractCropRaw(frameData, frameWidth, frameHeight, bbox) {
48756
48836
  };
48757
48837
  }
48758
48838
  //#endregion
48839
+ //#region src/pipeline-analytics/pipeline/bbox-pad.ts
48840
+ /**
48841
+ * Pad a normalized bounding box by a fractional amount relative to its own
48842
+ * dimensions. All coordinates remain in the [0, 1] unit-square.
48843
+ *
48844
+ * Formula (before clamping):
48845
+ * newX = x - padding * w
48846
+ * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
48847
+ * newY = y - padding * h
48848
+ * newH = h + 2 * padding * h
48849
+ *
48850
+ * padding === 0 is the identity operation. Pure, immutable — always returns a
48851
+ * new object.
48852
+ *
48853
+ * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
48854
+ * bound slides inward and keeps the extent that was asked for, on either side.
48855
+ * It shrinks only when the padded window is larger than the frame itself
48856
+ * would (e.g. a left-hugging box gains a bit more right-side context). This is
48857
+ * acceptable for detection crops (more context, never out of [0,1]); the
48858
+ * downstream `extractCrop` clamps to pixel bounds again regardless.
48859
+ */
48860
+ function padBbox(bbox, padding) {
48861
+ const rawX = bbox.x - padding * bbox.w;
48862
+ const rawY = bbox.y - padding * bbox.h;
48863
+ const rawW = bbox.w * (1 + 2 * padding);
48864
+ const rawH = bbox.h * (1 + 2 * padding);
48865
+ const w = Math.min(rawW, 1);
48866
+ const h = Math.min(rawH, 1);
48867
+ return {
48868
+ x: Math.min(Math.max(0, rawX), 1 - w),
48869
+ y: Math.min(Math.max(0, rawY), 1 - h),
48870
+ w,
48871
+ h
48872
+ };
48873
+ }
48874
+ /**
48875
+ * Legibility floor: the window's SHORT side is grown until it is at least this
48876
+ * fraction of the frame's short side.
48877
+ *
48878
+ * The 2026-09-18 traces included a 24x18 px subject. At the context ratio alone
48879
+ * that is a 72x54 window — and 72x54 source pixels are 72x54 delivered pixels,
48880
+ * because nothing in this repo may upscale. Growing the RECTANGLE is the only
48881
+ * honest way to make the picture bigger: more real pixels arrive, the subject
48882
+ * occupies less of the frame, and its drawn box says which part of it is the
48883
+ * subject. 0.18 of 1080 = 194 px, which renders a 24 px subject recognisably in
48884
+ * its surroundings.
48885
+ */
48886
+ var DEBUG_CROP_MIN_SIDE_FRAC = .18;
48887
+ function unionBox(a, b) {
48888
+ const x = Math.min(a.x, b.x);
48889
+ const y = Math.min(a.y, b.y);
48890
+ const right = Math.max(a.x + a.w, b.x + b.w);
48891
+ const bottom = Math.max(a.y + a.h, b.y + b.h);
48892
+ return {
48893
+ x,
48894
+ y,
48895
+ w: right - x,
48896
+ h: bottom - y
48897
+ };
48898
+ }
48899
+ /**
48900
+ * Which borders a box touches, on `edge-clear.ts`'s tolerance band — the SAME
48901
+ * band `isEdgeClear` uses, so "clipped" here and "not edge-clear" there can
48902
+ * never disagree about one frame.
48903
+ */
48904
+ function debugCropClipSides(bbox, frameWidth, frameHeight) {
48905
+ if (frameWidth <= 0 || frameHeight <= 0) return [];
48906
+ const tolX = DEFAULT_EDGE_TOLERANCE * frameWidth;
48907
+ const tolY = DEFAULT_EDGE_TOLERANCE * frameHeight;
48908
+ const sides = [];
48909
+ if (bbox.x <= tolX) sides.push("left");
48910
+ if (bbox.y <= tolY) sides.push("top");
48911
+ if (bbox.x + bbox.w >= frameWidth - tolX) sides.push("right");
48912
+ if (bbox.y + bbox.h >= frameHeight - tolY) sides.push("bottom");
48913
+ return sides;
48914
+ }
48915
+ /**
48916
+ * The ratio that makes `base` reach the legibility floor, or
48917
+ * {@link DEBUG_CROP_CONTEXT_RATIO} when it already does.
48918
+ *
48919
+ * Expressed as a PADDING RATIO rather than as a second rectangle so the window
48920
+ * still comes out of one {@link padBbox} call: there is exactly one place in
48921
+ * this file where a rectangle is grown, and it is not here.
48922
+ */
48923
+ function contextRatioFor(base, frameWidth, frameHeight) {
48924
+ const minSide = DEBUG_CROP_MIN_SIDE_FRAC * Math.min(frameWidth, frameHeight);
48925
+ const ratioFor = (extent) => extent <= 0 ? 1 : (minSide / extent - 1) / 2;
48926
+ const needed = Math.max(ratioFor(base.w), ratioFor(base.h));
48927
+ return Math.max(1, needed);
48928
+ }
48929
+ /**
48930
+ * The one debug-crop rectangle. Pure.
48931
+ *
48932
+ * A refusal is a VALUE, not a `null`: the caller logs the reason, because a
48933
+ * branch that stores nothing and says nothing reads as "never happened".
48934
+ */
48935
+ function planDebugCrop(input) {
48936
+ const { frameWidth, frameHeight, subject } = input;
48937
+ if (frameWidth <= 0 || frameHeight <= 0) return {
48938
+ kind: "refused",
48939
+ reason: "degenerate-frame"
48940
+ };
48941
+ if (subject.w <= 0 || subject.h <= 0) return {
48942
+ kind: "refused",
48943
+ reason: "degenerate-subject"
48944
+ };
48945
+ const base = input.rival === void 0 ? subject : unionBox(subject, input.rival);
48946
+ const contextRatio = contextRatioFor(base, frameWidth, frameHeight);
48947
+ const norm = padBbox({
48948
+ x: base.x / frameWidth,
48949
+ y: base.y / frameHeight,
48950
+ w: base.w / frameWidth,
48951
+ h: base.h / frameHeight
48952
+ }, contextRatio);
48953
+ const rect = {
48954
+ x: norm.x * frameWidth,
48955
+ y: norm.y * frameHeight,
48956
+ w: norm.w * frameWidth,
48957
+ h: norm.h * frameHeight
48958
+ };
48959
+ const relative = (role, box) => ({
48960
+ role,
48961
+ x: box.x - rect.x,
48962
+ y: box.y - rect.y,
48963
+ w: box.w,
48964
+ h: box.h
48965
+ });
48966
+ const boxes = input.rival === void 0 ? [relative("subject", subject)] : [relative("subject", subject), relative("rival", input.rival)];
48967
+ return {
48968
+ kind: "plan",
48969
+ decision: input.decision,
48970
+ rect,
48971
+ norm,
48972
+ boxes,
48973
+ contextRatio,
48974
+ floorApplied: contextRatio > 1,
48975
+ clippedSides: debugCropClipSides(subject, frameWidth, frameHeight),
48976
+ subjectCenterNorm: {
48977
+ x: (subject.x + subject.w / 2) / frameWidth,
48978
+ y: (subject.y + subject.h / 2) / frameHeight
48979
+ }
48980
+ };
48981
+ }
48982
+ function formatRect(rect) {
48983
+ return `${Math.round(rect.x)},${Math.round(rect.y)} ${Math.round(rect.w)}x${Math.round(rect.h)}`;
48984
+ }
48985
+ /**
48986
+ * The attribution bag: the deciding site's evidence FIRST, then the structural
48987
+ * facts — so no evidence key can rename the decision, the track or the
48988
+ * rectangle the crop belongs to. That ordering is the whole point: a crop whose
48989
+ * own label can be overwritten by a caller is back to being unattributable.
48990
+ */
48991
+ function debugCropMeta(plan, attribution) {
48992
+ return {
48993
+ ...attribution.evidence,
48994
+ decision: attribution.decision,
48995
+ trackId: attribution.trackId,
48996
+ ...attribution.rivalTrackId === void 0 ? {} : { rivalTrackId: attribution.rivalTrackId },
48997
+ timestamp: attribution.timestamp,
48998
+ cropRect: formatRect(plan.rect),
48999
+ contextRatio: plan.contextRatio,
49000
+ floorApplied: plan.floorApplied,
49001
+ clippedSides: plan.clippedSides.join("|"),
49002
+ subjectCenter: `${plan.subjectCenterNorm.x.toFixed(3)},${plan.subjectCenterNorm.y.toFixed(3)}`
49003
+ };
49004
+ }
49005
+ /**
49006
+ * The caption burnt into the crop's own pixels.
49007
+ *
49008
+ * Burnt in, not stored beside it, because a debug crop travels: it is exported,
49009
+ * pasted into a note, and looked at months later next to five others. A blob
49010
+ * that cannot say what it is, on its own, is the thing that made the last
49011
+ * investigation slow — and this store has no column to say it in.
49012
+ */
49013
+ function debugCropLabel(plan, attribution) {
49014
+ 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);
49015
+ }
49016
+ //#endregion
49017
+ //#region src/pipeline-analytics/pipeline/debug-crop-capture.ts
49018
+ /** Stroke colour of the box the decision was ABOUT. */
49019
+ var SUBJECT_COLOR = "#00e5ff";
49020
+ /** Stroke colour of the box it was weighed AGAINST — a different colour,
49021
+ * because "which one is which" is the first question a two-box crop raises. */
49022
+ var RIVAL_COLOR = "#ff9500";
49023
+ /** Encode quality: a debug crop is read by a person looking for a defect, so it
49024
+ * is encoded above the timeline-tile default (80) — a JPEG artefact that reads
49025
+ * as a mirror edge would waste the very investigation this exists for. */
49026
+ var DEBUG_CROP_QUALITY = 92;
49027
+ /** Bound the per-track counters so a long-lived process never leaks them. */
49028
+ var MAX_TRACKED_TRACKS = 2e3;
49029
+ function createDebugCropCapture(deps) {
49030
+ const storedPerTrack = /* @__PURE__ */ new Map();
49031
+ const cappedAnnounced = /* @__PURE__ */ new Set();
49032
+ const skip = (input, reason, extra) => {
49033
+ deps.logger.info("debug crop not stored", {
49034
+ tags: { deviceId: input.deviceId },
49035
+ meta: {
49036
+ reason,
49037
+ decision: input.decision,
49038
+ trackId: input.trackId,
49039
+ timestamp: input.timestamp,
49040
+ ...extra
49041
+ }
49042
+ });
49043
+ return null;
49044
+ };
49045
+ return async (input) => {
49046
+ if (!deps.isDebugMarked(input.trackId)) return null;
49047
+ const already = storedPerTrack.get(input.trackId) ?? 0;
49048
+ if (already >= 6) {
49049
+ if (cappedAnnounced.has(input.trackId)) return null;
49050
+ if (cappedAnnounced.size >= MAX_TRACKED_TRACKS) cappedAnnounced.clear();
49051
+ cappedAnnounced.add(input.trackId);
49052
+ return skip(input, "per-track-cap", {
49053
+ stored: already,
49054
+ cap: 6
49055
+ });
49056
+ }
49057
+ const plan = planDebugCrop({
49058
+ decision: input.decision,
49059
+ subject: input.subject,
49060
+ ...input.rival === void 0 ? {} : { rival: input.rival },
49061
+ frameWidth: input.frameWidth,
49062
+ frameHeight: input.frameHeight
49063
+ });
49064
+ if (plan.kind === "refused") return skip(input, plan.reason, {});
49065
+ const attribution = {
49066
+ decision: input.decision,
49067
+ trackId: input.trackId,
49068
+ ...input.rivalTrackId === void 0 ? {} : { rivalTrackId: input.rivalTrackId },
49069
+ timestamp: input.timestamp,
49070
+ evidence: input.evidence
49071
+ };
49072
+ const meta = debugCropMeta(plan, attribution);
49073
+ const frame = await deps.fetchFrameRgb(input.frameHandle, input.deviceId, input.trackId);
49074
+ if (frame === null || frame.width <= 0 || frame.height <= 0) return skip(input, "frame-unavailable", { nodeId: input.frameHandle.nodeId });
49075
+ let jpeg;
49076
+ try {
49077
+ const cut = await deps.extractCropRaw(frame.bytes, frame.width, frame.height, plan.norm);
49078
+ const scale = plan.rect.w > 0 ? cut.width / plan.rect.w : 1;
49079
+ const boxes = plan.boxes.map((b) => {
49080
+ const geometry = {
49081
+ x: b.x * scale,
49082
+ y: b.y * scale,
49083
+ w: b.w * scale,
49084
+ h: b.h * scale
49085
+ };
49086
+ return b.role === "subject" ? {
49087
+ ...geometry,
49088
+ color: SUBJECT_COLOR,
49089
+ label: debugCropLabel(plan, attribution)
49090
+ } : {
49091
+ ...geometry,
49092
+ color: RIVAL_COLOR,
49093
+ label: "rival"
49094
+ };
49095
+ });
49096
+ jpeg = await deps.drawBoxedFrame(cut.data, cut.width, cut.height, boxes, { quality: DEBUG_CROP_QUALITY });
49097
+ } catch (err) {
49098
+ return skip(input, "render-failed", { error: err instanceof Error ? err.message : String(err) });
49099
+ }
49100
+ try {
49101
+ const mediaId = await deps.putDebugCrop({
49102
+ deviceId: input.deviceId,
49103
+ trackId: input.trackId,
49104
+ timestamp: input.timestamp,
49105
+ data: jpeg
49106
+ });
49107
+ if (storedPerTrack.size >= MAX_TRACKED_TRACKS) storedPerTrack.clear();
49108
+ storedPerTrack.set(input.trackId, already + 1);
49109
+ deps.logger.info("debug crop stored", {
49110
+ tags: { deviceId: input.deviceId },
49111
+ meta: {
49112
+ ...meta,
49113
+ mediaId,
49114
+ deviceId: input.deviceId
49115
+ }
49116
+ });
49117
+ return mediaId;
49118
+ } catch (err) {
49119
+ deps.logger.warn("debug crop store failed", {
49120
+ tags: { deviceId: input.deviceId },
49121
+ meta: {
49122
+ ...meta,
49123
+ error: err instanceof Error ? err.message : String(err)
49124
+ }
49125
+ });
49126
+ return null;
49127
+ }
49128
+ };
49129
+ }
49130
+ //#endregion
48759
49131
  //#region src/shared/frame/encode-crop.ts
48760
49132
  /**
48761
49133
  * JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
@@ -53547,42 +53919,6 @@ var AnalyticsFailureReport = class {
53547
53919
  }
53548
53920
  };
53549
53921
  //#endregion
53550
- //#region src/pipeline-analytics/pipeline/bbox-pad.ts
53551
- /**
53552
- * Pad a normalized bounding box by a fractional amount relative to its own
53553
- * dimensions. All coordinates remain in the [0, 1] unit-square.
53554
- *
53555
- * Formula (before clamping):
53556
- * newX = x - padding * w
53557
- * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
53558
- * newY = y - padding * h
53559
- * newH = h + 2 * padding * h
53560
- *
53561
- * padding === 0 is the identity operation. Pure, immutable — always returns a
53562
- * new object.
53563
- *
53564
- * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
53565
- * bound slides inward and keeps the extent that was asked for, on either side.
53566
- * It shrinks only when the padded window is larger than the frame itself
53567
- * would (e.g. a left-hugging box gains a bit more right-side context). This is
53568
- * acceptable for detection crops (more context, never out of [0,1]); the
53569
- * downstream `extractCrop` clamps to pixel bounds again regardless.
53570
- */
53571
- function padBbox(bbox, padding) {
53572
- const rawX = bbox.x - padding * bbox.w;
53573
- const rawY = bbox.y - padding * bbox.h;
53574
- const rawW = bbox.w * (1 + 2 * padding);
53575
- const rawH = bbox.h * (1 + 2 * padding);
53576
- const w = Math.min(rawW, 1);
53577
- const h = Math.min(rawH, 1);
53578
- return {
53579
- x: Math.min(Math.max(0, rawX), 1 - w),
53580
- y: Math.min(Math.max(0, rawY), 1 - h),
53581
- w,
53582
- h
53583
- };
53584
- }
53585
- //#endregion
53586
53922
  //#region src/pipeline-analytics/pipeline/capture-crop.ts
53587
53923
  /**
53588
53924
  * The normalised window a capture asks the runner for: the box padded by
@@ -75975,6 +76311,19 @@ var customActions = {
75975
76311
  ...faceRescoreActions
75976
76312
  };
75977
76313
  /**
76314
+ * Width cap on the ONE full frame a debug crop is cut from.
76315
+ *
76316
+ * A debug crop is a picture for a person, and the plan has already widened the
76317
+ * window to a neighbourhood — past this width the extra pixels buy nothing and
76318
+ * a 4K native surface would cross the transport for every marked track.
76319
+ */
76320
+ var DEBUG_CROP_FETCH_MAX_WIDTH = 1280;
76321
+ /** How long a queued debug-crop frame fetch is still worth executing. Past
76322
+ * this the handle's frame has aged out of the runner's retention and the next
76323
+ * decision will ask again with a young one, so the queued entry is dead
76324
+ * weight in a lane real media is waiting in. */
76325
+ var DEBUG_CROP_FETCH_STALE_MS = 2e3;
76326
+ /**
75978
76327
  * Assist threshold when the caller does not pin one. Packages have their own
75979
76328
  * (0.35, on the zone crop only — `retrain/retrain-assist.ts`); everything else
75980
76329
  * starts here and the operator drops the ones they do not want.
@@ -77008,7 +77357,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77008
77357
  * `docs/decisions/` — the gate is a model input, so this is a deliberate
77009
77358
  * exception to "model-input crops keep captureCrop".
77010
77359
  */
77360
+ /**
77361
+ * The operator-debug crop capture (`pipeline/debug-crop-capture.ts`).
77362
+ *
77363
+ * Null until the transport and the media store exist. Reached from three
77364
+ * decision sites and gated INSIDE the capture on the track's `debug` flag, so
77365
+ * a camera with no marked track pays one map lookup per decision — see the
77366
+ * capture's module doc for why the gate lives there and not at each site.
77367
+ */
77368
+ captureDebugCropFn = null;
77011
77369
  captureDisplayCropFn = null;
77370
+ /**
77371
+ * Offer one decision's pixels to the operator-debug crop capture.
77372
+ *
77373
+ * Fire-and-forget and NEVER throws: this is the addon's own bookkeeping, and
77374
+ * a frame path that can be slowed or broken by a diagnostic is worse than no
77375
+ * diagnostic. The gate, the per-track cap and all six refusal reasons live in
77376
+ * the capture (`pipeline/debug-crop-capture.ts`); this is only the seam.
77377
+ */
77378
+ /**
77379
+ * The CURRENT frame's handle and dims, per camera — the one thing the
77380
+ * salvage-bind observer lacks.
77381
+ *
77382
+ * The tracker knows no `deviceId`, holds no handle and writes no media (that
77383
+ * is the point of `pipeline/tracker/**`), so a growth bind reports its two
77384
+ * boxes and stops. This mirror is what lets the host put those boxes back on
77385
+ * a picture. Written ONLY while some track on this runner is marked
77386
+ * (`residents.anyDebugMarked()`), so an ordinary fleet does not allocate one
77387
+ * object per frame per camera for a diagnostic nobody asked for.
77388
+ */
77389
+ debugFrameContext = /* @__PURE__ */ new Map();
77390
+ offerDebugCrop(input) {
77391
+ const capture = this.captureDebugCropFn;
77392
+ if (capture === null) return;
77393
+ capture(input).catch((err) => {
77394
+ this.ctx.logger.debug("debug crop offer threw", {
77395
+ tags: { deviceId: input.deviceId },
77396
+ meta: {
77397
+ deviceId: input.deviceId,
77398
+ trackId: input.trackId,
77399
+ decision: input.decision,
77400
+ error: err instanceof Error ? err.message : String(err)
77401
+ }
77402
+ });
77403
+ });
77404
+ }
77012
77405
  captureCrop = null;
77013
77406
  /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
77014
77407
  * to the owning runner, PLUS the source `tier`. Captured in the async init
@@ -77373,6 +77766,36 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77373
77766
  isNodeOffline: (nodeId) => this.nodeLiveness.isKnownOffline(nodeId, Date.now())
77374
77767
  });
77375
77768
  this.captureCrop = transport.captureCrop;
77769
+ const fetchDebugFullFrame = transport.getNativeFullFrameRgb;
77770
+ this.captureDebugCropFn = createDebugCropCapture({
77771
+ isDebugMarked: (trackId) => this.residents.isDebugMarked(trackId),
77772
+ fetchFrameRgb: async (handle, deviceId, trackId) => {
77773
+ const frame = await this.captureScheduler.request({
77774
+ deviceId,
77775
+ trackId,
77776
+ kind: "other",
77777
+ coalesceKey: captureCoalesceKey(deviceId, "other", trackId),
77778
+ staleAfterMs: DEBUG_CROP_FETCH_STALE_MS,
77779
+ exec: () => fetchDebugFullFrame(handle, DEBUG_CROP_FETCH_MAX_WIDTH)
77780
+ });
77781
+ return frame === null || frame === void 0 ? null : {
77782
+ bytes: frame.data,
77783
+ width: frame.width,
77784
+ height: frame.height
77785
+ };
77786
+ },
77787
+ extractCropRaw: (data, w, h, box) => extractCropRaw(data, w, h, box),
77788
+ drawBoxedFrame: (data, w, h, boxes, options) => drawBoxedFrame(data, w, h, boxes, options ?? {}),
77789
+ putDebugCrop: (put) => stores.mediaStore.put({
77790
+ deviceId: put.deviceId,
77791
+ ownerKind: "track",
77792
+ ownerId: put.trackId,
77793
+ kind: "debugCrop",
77794
+ timestamp: put.timestamp,
77795
+ data: put.data
77796
+ }),
77797
+ logger: logger.child("DebugCrop")
77798
+ });
77376
77799
  this.eventMediaDispatcher = new EventMediaDispatcher({
77377
77800
  getRemoteFrame: transport.getRemoteFrame,
77378
77801
  getNativeCropJpeg: transport.getNativeCropJpeg,
@@ -80373,6 +80796,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
80373
80796
  frameHeight: result.frameHeight
80374
80797
  }, exhaustedIds);
80375
80798
  const outcome = run.outcome;
80799
+ if (frameHandle !== void 0 && this.residents.anyDebugMarked()) this.debugFrameContext.set(deviceId, {
80800
+ handle: frameHandle,
80801
+ width: result.frameWidth,
80802
+ height: result.frameHeight,
80803
+ timestamp: result.timestamp
80804
+ });
80376
80805
  const gateNowMs = Date.now();
80377
80806
  for (const { id, t } of bornCandidates) {
80378
80807
  if (outcome.undecided.has(id)) {
@@ -80431,6 +80860,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
80431
80860
  error: String(err)
80432
80861
  } });
80433
80862
  });
80863
+ if (frameHandle !== void 0) {
80864
+ const decision = run.decisions.get(id);
80865
+ this.offerDebugCrop({
80866
+ deviceId,
80867
+ trackId: id,
80868
+ decision: "birth-confirmation",
80869
+ subject: {
80870
+ x: t.bbox.x,
80871
+ y: t.bbox.y,
80872
+ w: t.bbox.w,
80873
+ h: t.bbox.h
80874
+ },
80875
+ frameHandle,
80876
+ frameWidth: result.frameWidth,
80877
+ frameHeight: result.frameHeight,
80878
+ timestamp: gateNowMs,
80879
+ evidence: {
80880
+ className: t.className,
80881
+ source,
80882
+ verdict: "suppression-held",
80883
+ ...decision?.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
80884
+ ...decision?.bestScore !== void 0 ? { bestScore: decision.bestScore } : {},
80885
+ judgedCropKept: judged !== void 0
80886
+ }
80887
+ });
80888
+ }
80434
80889
  log.info("birth suppression held — the gate is not this root detector", {
80435
80890
  tags: { deviceId },
80436
80891
  meta: {
@@ -82459,6 +82914,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
82459
82914
  meta: { ...bestShotPromotionMeta(promotionAttempt) }
82460
82915
  });
82461
82916
  }
82917
+ if (frameHandle !== void 0) {
82918
+ const heldBbox = this.residents.bestSeenBbox(t.trackId);
82919
+ const rival = heldBbox !== void 0 && heldBbox.frameWidth === frameWidth && heldBbox.frameHeight === frameHeight ? {
82920
+ x: heldBbox.x,
82921
+ y: heldBbox.y,
82922
+ w: heldBbox.w,
82923
+ h: heldBbox.h
82924
+ } : void 0;
82925
+ this.offerDebugCrop({
82926
+ deviceId,
82927
+ trackId: t.trackId,
82928
+ decision: "best-shot",
82929
+ subject: {
82930
+ x: t.bbox.x,
82931
+ y: t.bbox.y,
82932
+ w: t.bbox.w,
82933
+ h: t.bbox.h
82934
+ },
82935
+ ...rival !== void 0 ? { rival } : {},
82936
+ frameHandle,
82937
+ frameWidth,
82938
+ frameHeight,
82939
+ timestamp,
82940
+ evidence: { ...bestShotPromotionMeta(promotionAttempt) }
82941
+ });
82942
+ }
82462
82943
  if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && keyFrame === "skip") continue;
82463
82944
  targets.push({
82464
82945
  trackId: t.trackId,
@@ -83812,6 +84293,33 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
83812
84293
  ...record
83813
84294
  }
83814
84295
  });
84296
+ if (record.gate !== "growth") return;
84297
+ const frame = this.debugFrameContext.get(deviceId);
84298
+ if (frame === void 0 || frame.width !== record.frameWidth) return;
84299
+ this.offerDebugCrop({
84300
+ deviceId,
84301
+ trackId: record.trackId,
84302
+ decision: "growth-rescue",
84303
+ subject: record.trackBbox,
84304
+ rival: record.detBbox,
84305
+ frameHandle: frame.handle,
84306
+ frameWidth: record.frameWidth,
84307
+ frameHeight: record.frameHeight,
84308
+ timestamp: frame.timestamp,
84309
+ evidence: {
84310
+ stage: record.stage,
84311
+ trackClass: record.trackClass,
84312
+ detClass: record.detClass,
84313
+ detScore: record.detScore,
84314
+ iou: record.iou,
84315
+ centroidDistPx: record.centroidDistPx,
84316
+ trackDiagPx: record.trackDiagPx,
84317
+ areaRatio: record.areaRatio,
84318
+ dtMs: record.dtMs,
84319
+ trackAgeMs: record.trackAgeMs,
84320
+ source
84321
+ }
84322
+ });
83815
84323
  });
83816
84324
  p.setSpawnRefusalObserver((record) => {
83817
84325
  this.ctx.logger.info("tracker: detection dropped — nothing downstream received it", {
@@ -85958,6 +86466,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
85958
86466
  logger: this.ctx.logger,
85959
86467
  ...this.debugNoteArchive !== null ? { noteArchive: this.debugNoteArchive } : {}
85960
86468
  }, input);
86469
+ this.residents.setDebugMarked(input.deviceId, input.trackId, out.debug);
85961
86470
  this.ctx.logger.info("track operator flags set", {
85962
86471
  tags: { deviceId: input.deviceId },
85963
86472
  meta: {