@camstack/addon-pipeline 1.2.195 → 1.2.197

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.
Files changed (38) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-CJdc8j83.js → default-detection-model-CM-8U0Bb.js} +1 -1
  4. package/dist/{default-detection-model-BGBAmtEp.mjs → default-detection-model-CplzzTmN.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-HG5geL9t.js → dist-CBa1j3On.js} +141 -35
  8. package/dist/{dist-mYDK58Jp.mjs → dist-Y4BsF_EC.mjs} +130 -36
  9. package/dist/{lazy-sharp-DmAwZ0s9.js → lazy-sharp-CS5WE9mI.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-xRsaPwBa.mjs → node-B5ouEQYN.mjs} +1 -1
  13. package/dist/{node--my8JTOW.js → node-C-B8VuGi.js} +1 -1
  14. package/dist/pipeline-runner/index.js +469 -30
  15. package/dist/pipeline-runner/index.mjs +468 -29
  16. package/dist/{process-memory-CoCSce63.js → process-memory-CmeKr_MZ.js} +1 -1
  17. package/dist/{process-memory-BDHR3lxe.mjs → process-memory-DeO2OZ3-.mjs} +1 -1
  18. package/dist/recorder/index.js +131 -18
  19. package/dist/recorder/index.mjs +131 -18
  20. package/dist/{retire-root-keys-KE6D6Xh_.js → retire-root-keys-Csb8z2f3.js} +19 -0
  21. package/dist/{retire-root-keys-DMolfhsP.mjs → retire-root-keys-DZvlso2l.mjs} +19 -0
  22. package/dist/{segment-demux-js-CXvgKfsn.mjs → segment-demux-js-DXPgq2cf.mjs} +1 -1
  23. package/dist/{segment-demux-js-CBynn9Gh.js → segment-demux-js-DiIsywGz.js} +1 -1
  24. package/dist/session-decode/decode-worker-child.js +2 -2
  25. package/dist/session-decode/decode-worker-child.mjs +1 -1
  26. package/dist/stream-broker/_stub.js +2 -2
  27. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B17jLw_E.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Q-6HzWJ0.mjs} +3 -3
  28. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_NVJXAy.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cv5McbTI.mjs} +1 -1
  29. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CBxobiZW.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs} +1 -1
  30. package/dist/stream-broker/demux-worker-child.js +1 -1
  31. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  32. package/dist/stream-broker/{hostInit-C6ku1nL0.mjs → hostInit-C-2Eo_nr.mjs} +3 -3
  33. package/dist/stream-broker/index.js +4 -4
  34. package/dist/stream-broker/index.mjs +4 -4
  35. package/dist/stream-broker/remoteEntry.js +1 -1
  36. package/dist/{worker-protocol-DCsi4YS1.js → worker-protocol-CXsdriRX.js} +1 -1
  37. package/dist/{worker-protocol-C0R1DDC0.mjs → worker-protocol-_7zrToTo.mjs} +1 -1
  38. package/package.json +1 -1
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-HG5geL9t.js");
6
- const require_node = require("../node--my8JTOW.js");
7
- const require_default_detection_model = require("../default-detection-model-CJdc8j83.js");
8
- const require_worker_protocol = require("../worker-protocol-DCsi4YS1.js");
5
+ const require_dist = require("../dist-CBa1j3On.js");
6
+ const require_node = require("../node-C-B8VuGi.js");
7
+ const require_default_detection_model = require("../default-detection-model-CM-8U0Bb.js");
8
+ const require_worker_protocol = require("../worker-protocol-CXsdriRX.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-DmAwZ0s9.js");
10
+ const require_lazy_sharp = require("../lazy-sharp-CS5WE9mI.js");
11
11
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
12
12
  const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
13
13
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
@@ -1977,6 +1977,169 @@ function buildBenchRunPipelineInput(req) {
1977
1977
  ...req.plane ? { plane: req.plane } : {}
1978
1978
  };
1979
1979
  }
1980
+ /**
1981
+ * How long a box stays "seen".
1982
+ *
1983
+ * Sized on the gate's own clock, not on a guess: `maxDeferralMs` defaults to
1984
+ * 2 000 ms, so a birth's whole life as a candidate is 2 s. A window shorter
1985
+ * than that would re-cut the same subject inside one deferral; a much longer
1986
+ * one would stop a subject that genuinely left and returned from being cut
1987
+ * again. 3 000 ms is the deferral budget plus one 1 s margin.
1988
+ */
1989
+ var BIRTH_EVIDENCE_NOVELTY_WINDOW_MS = 3e3;
1990
+ /**
1991
+ * Padding around the box, as a fraction of its size.
1992
+ *
1993
+ * 0.15 deliberately equals `CONFIRMATION_CROP_PADDING` in post-analysis: the
1994
+ * gate promotes a secondary subject by mapping a crop-space box back into the
1995
+ * frame through the rectangle the crop was cut from, and the two derivations
1996
+ * disagreeing is how a promoted box lands somewhere the pixels never were.
1997
+ */
1998
+ var BIRTH_EVIDENCE_PADDING = .15;
1999
+ /**
2000
+ * Boxes remembered per camera. A frame with more subjects than this keeps the
2001
+ * most recent — the cap is about bounding a crowd, and a box that fell out is
2002
+ * merely eligible for a cut it will not get twice (the encode budget above is
2003
+ * the real spend). 64 is 16x the tile tier's per-frame subject cap.
2004
+ */
2005
+ var REMEMBERED_BOXES_PER_DEVICE = 64;
2006
+ /**
2007
+ * Per-camera memory of the boxes recently on screen.
2008
+ *
2009
+ * Per CAMERA and never global: a box on 617 says nothing about 615, and a
2010
+ * shared structure would make one camera's traffic decide another's cuts —
2011
+ * which is the exact shape of the node-wide sharing this whole change moves
2012
+ * away from.
2013
+ *
2014
+ * Judged per CLASS. A dog at its owner's knee is 100% contained in the person's
2015
+ * box; a class-blind test would call it "already seen" and it would reach the
2016
+ * gate with no pixels — the failure D372, D377 and D378 each fought at a
2017
+ * different layer.
2018
+ */
2019
+ var BirthNoveltyMemory = class {
2020
+ byDevice = /* @__PURE__ */ new Map();
2021
+ /**
2022
+ * Is this box a subject the camera has not been showing?
2023
+ *
2024
+ * Pure: it does not record anything. The caller records the whole frame with
2025
+ * {@link note}, once, so a box that lost the encode budget is still counted
2026
+ * as seen — otherwise the cap turns into a permanent re-cut of the same
2027
+ * subjects.
2028
+ */
2029
+ isNovel(deviceId, box, macroClass, nowMs) {
2030
+ const boxes = this.byDevice.get(deviceId);
2031
+ if (!boxes) return true;
2032
+ for (const remembered of boxes) {
2033
+ if (nowMs - remembered.atMs > 3e3) continue;
2034
+ if (remembered.macroClass !== macroClass) continue;
2035
+ if (iou(remembered.rect, box) >= .3) return false;
2036
+ }
2037
+ return true;
2038
+ }
2039
+ /** Record a frame's detections and drop everything past the window / the cap. */
2040
+ note(deviceId, detections, nowMs) {
2041
+ const kept = (this.byDevice.get(deviceId) ?? []).filter((b) => nowMs - b.atMs <= BIRTH_EVIDENCE_NOVELTY_WINDOW_MS);
2042
+ for (const d of detections) {
2043
+ if (!isUsable$1(d.bbox)) continue;
2044
+ kept.push({
2045
+ rect: d.bbox,
2046
+ macroClass: d.macroClass,
2047
+ atMs: nowMs
2048
+ });
2049
+ }
2050
+ this.byDevice.set(deviceId, kept.length > REMEMBERED_BOXES_PER_DEVICE ? kept.slice(kept.length - REMEMBERED_BOXES_PER_DEVICE) : kept);
2051
+ }
2052
+ /** How many boxes this camera currently remembers (tests / diagnostics). */
2053
+ rememberedCount(deviceId) {
2054
+ return this.byDevice.get(deviceId)?.length ?? 0;
2055
+ }
2056
+ /** Forget a camera wholesale (detach, pipeline reset). */
2057
+ clearDevice(deviceId) {
2058
+ this.byDevice.delete(deviceId);
2059
+ }
2060
+ };
2061
+ /**
2062
+ * The cuts worth taking on this frame, highest confidence first, capped.
2063
+ *
2064
+ * Records EVERY usable detection in `memory` — including the ones the cap
2065
+ * refused — so the budget bounds the spend without turning into a leak.
2066
+ */
2067
+ function planBirthEvidence(deviceId, detections, frame, memory, nowMs, maxPerFrame = 2) {
2068
+ if (frame.width <= 0 || frame.height <= 0) return [];
2069
+ const usable = detections.map((d, index) => ({
2070
+ d,
2071
+ index
2072
+ })).filter(({ d }) => isUsable$1(d.bbox));
2073
+ const novel = usable.filter(({ d }) => memory.isNovel(deviceId, d.bbox, d.macroClass, nowMs));
2074
+ memory.note(deviceId, usable.map(({ d }) => ({
2075
+ bbox: d.bbox,
2076
+ macroClass: d.macroClass
2077
+ })), nowMs);
2078
+ const plan = [];
2079
+ for (const { d, index } of [...novel].sort((a, b) => b.d.score - a.d.score)) {
2080
+ if (plan.length >= maxPerFrame) break;
2081
+ const rect = padAndClamp(d.bbox, frame);
2082
+ if (rect === null) continue;
2083
+ plan.push({
2084
+ index,
2085
+ rect,
2086
+ target: targetFor(rect)
2087
+ });
2088
+ }
2089
+ return plan.sort((a, b) => a.index - b.index);
2090
+ }
2091
+ /**
2092
+ * The padded, clamped source rectangle — or `null` for a box that is not worth
2093
+ * an encode. A degenerate box is DROPPED rather than clamped: a zero-area crop
2094
+ * serves nothing and a NaN one would poison the encoder's geometry.
2095
+ */
2096
+ function padAndClamp(box, frame) {
2097
+ if (Math.min(box.width, box.height) < 128) return null;
2098
+ const padX = box.width * BIRTH_EVIDENCE_PADDING;
2099
+ const padY = box.height * BIRTH_EVIDENCE_PADDING;
2100
+ const left = Math.max(0, Math.floor(box.x - padX));
2101
+ const top = Math.max(0, Math.floor(box.y - padY));
2102
+ const right = Math.min(frame.width, Math.ceil(box.x + box.width + padX));
2103
+ const bottom = Math.min(frame.height, Math.ceil(box.y + box.height + padY));
2104
+ const width = right - left;
2105
+ const height = bottom - top;
2106
+ if (width <= 0 || height <= 0) return null;
2107
+ return {
2108
+ x: left,
2109
+ y: top,
2110
+ width,
2111
+ height
2112
+ };
2113
+ }
2114
+ /** Encoded size: capped, aspect-preserving, never upscaled. */
2115
+ function targetFor(rect) {
2116
+ if (rect.width <= 320) return {
2117
+ width: rect.width,
2118
+ height: rect.height
2119
+ };
2120
+ const scale = 320 / rect.width;
2121
+ return {
2122
+ width: 320,
2123
+ height: Math.max(1, Math.round(rect.height * scale))
2124
+ };
2125
+ }
2126
+ /** A box with finite, positive extent. */
2127
+ function isUsable$1(box) {
2128
+ return Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height) && box.width > 0 && box.height > 0;
2129
+ }
2130
+ /** Intersection over union of two absolute-pixel rectangles. */
2131
+ function iou(a, b) {
2132
+ const left = Math.max(a.x, b.x);
2133
+ const top = Math.max(a.y, b.y);
2134
+ const right = Math.min(a.x + a.width, b.x + b.width);
2135
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
2136
+ const w = right - left;
2137
+ const h = bottom - top;
2138
+ if (w <= 0 || h <= 0) return 0;
2139
+ const intersection = w * h;
2140
+ const union = a.width * a.height + b.width * b.height - intersection;
2141
+ return union <= 0 ? 0 : intersection / union;
2142
+ }
1980
2143
  //#endregion
1981
2144
  //#region src/pipeline-runner/capacity/capacity-runner.ts
1982
2145
  /**
@@ -5156,12 +5319,6 @@ var PipelineRunner = class {
5156
5319
  return null;
5157
5320
  }
5158
5321
  };
5159
- /**
5160
- * How much of a region must lie OUTSIDE every full-frame detection for the
5161
- * pass to run on it. A person's own blob (dilated, with shadow) is covered
5162
- * 70–90% by the person box; a person-and-dog blob is covered ~55% by it.
5163
- */
5164
- var RECOVERY_MIN_UNCOVERED_FRACTION = .35;
5165
5322
  function area(r) {
5166
5323
  return Math.max(0, r.w) * Math.max(0, r.h);
5167
5324
  }
@@ -5209,25 +5366,53 @@ function cropsForRegion(region, native) {
5209
5366
  return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
5210
5367
  }
5211
5368
  /**
5212
- * The crops the second pass runs on this frame — possibly none.
5369
+ * The ONE place the two region gates are decided. `planRecoveryCrops` filters
5370
+ * on it and {@link countRecoverySkips} counts it, so the report can never
5371
+ * describe a gate the planner does not actually apply.
5372
+ */
5373
+ function classifyRegion(region, uncovered) {
5374
+ if (area(region) > .25) return "too-large";
5375
+ if (uncovered < .35) return "covered";
5376
+ return "eligible";
5377
+ }
5378
+ /**
5379
+ * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
5213
5380
  *
5214
- * A region qualifies when it is small (`RECOVERY_MAX_REGION_FRAME_FRACTION`)
5215
- * and mostly uncovered by the full-frame boxes
5216
- * (`RECOVERY_MIN_UNCOVERED_FRACTION`). Qualifying regions are served largest
5217
- * uncovered area first under `RECOVERY_MAX_CROPS_PER_FRAME`; a crop whose
5218
- * centre already lies inside a planned crop is folded into it. A frame that
5219
- * cannot hold the smallest crop yields nothing: it is already seen at scale.
5381
+ * The pass report counts the passes that RAN, so "the second pass stops running
5382
+ * when a person is beside the dog" — a plausible reading of
5383
+ * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
5384
+ * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
5385
+ * — could not be confirmed or killed from the logs at all. This classifies the
5386
+ * regions a frame offered, against the same predicate the planner filters on.
5220
5387
  */
5388
+ function countRecoverySkips(input) {
5389
+ let tooLarge = 0;
5390
+ let covered = 0;
5391
+ let eligible = 0;
5392
+ for (const region of input.regions) switch (classifyRegion(region, uncoveredFraction(region, input.detections))) {
5393
+ case "too-large":
5394
+ tooLarge += 1;
5395
+ break;
5396
+ case "covered":
5397
+ covered += 1;
5398
+ break;
5399
+ case "eligible":
5400
+ eligible += 1;
5401
+ break;
5402
+ }
5403
+ return {
5404
+ tooLarge,
5405
+ covered,
5406
+ eligible
5407
+ };
5408
+ }
5221
5409
  function planRecoveryCrops(input) {
5222
5410
  const { native } = input;
5223
5411
  if (Math.min(native.width, native.height) < 640) return [];
5224
5412
  const ranked = input.regions.map((region) => ({
5225
5413
  region,
5226
5414
  uncovered: uncoveredFraction(region, input.detections)
5227
- })).filter(({ region, uncovered }) => {
5228
- if (area(region) > .25) return false;
5229
- return uncovered >= RECOVERY_MIN_UNCOVERED_FRACTION;
5230
- }).toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
5415
+ })).filter(({ region, uncovered }) => classifyRegion(region, uncovered) === "eligible").toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
5231
5416
  const planned = [];
5232
5417
  for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
5233
5418
  if (planned.length >= 2) return planned;
@@ -5258,6 +5443,11 @@ function remapCropDetections(detections, crop, view, result, native) {
5258
5443
  if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
5259
5444
  out.push({
5260
5445
  ...detection,
5446
+ nativeRecovery: {
5447
+ score: detection.score,
5448
+ cropSidePx: crop.side,
5449
+ viewPx: view.width
5450
+ },
5261
5451
  bbox: {
5262
5452
  x: (crop.left + x * viewToNative) * nativeToResultX,
5263
5453
  y: (crop.top + y * viewToNative) * nativeToResultY,
@@ -5512,6 +5702,15 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
5512
5702
  var MAX_BOUND_FRAME_CUTTERS = 256;
5513
5703
  /** How often a camera reports its undispatched cuts. */
5514
5704
  var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
5705
+ /**
5706
+ * How often a camera reports what carrying its birth evidence COST (D379).
5707
+ *
5708
+ * The cost of this change is the one thing that must be measured rather than
5709
+ * estimated, so the report is per camera, states encodes / bytes / encode-ms
5710
+ * over the window, and drains its counters — a camera that cut nothing says
5711
+ * nothing until it does.
5712
+ */
5713
+ var BIRTH_EVIDENCE_REPORT_INTERVAL_MS = 6e5;
5515
5714
  var DEFAULT_CONFIG = {
5516
5715
  maxQueueDepth: 30,
5517
5716
  maxConcurrentInferences: 16,
@@ -5873,6 +6072,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5873
6072
  tileCutDrops = /* @__PURE__ */ new Map();
5874
6073
  tileCutDropLoggedAt = /* @__PURE__ */ new Map();
5875
6074
  /**
6075
+ * Per-camera memory of the boxes recently on screen — what decides whether a
6076
+ * detection is worth cutting its own evidence for (D379). Per CAMERA, never
6077
+ * global: one camera's traffic must never decide another's cuts.
6078
+ */
6079
+ birthNovelty = new BirthNoveltyMemory();
6080
+ /** Per-camera cost of the carried evidence, drained by its report line. */
6081
+ birthEvidenceCost = /* @__PURE__ */ new Map();
6082
+ birthEvidenceReportedAt = /* @__PURE__ */ new Map();
6083
+ /**
5876
6084
  * Node-level RAM store of retained rgb detection frames, keyed by the
5877
6085
  * `FrameHandle` that rides the inference-result event (shm-ring removal
5878
6086
  * 2026-07-16 — replaces the `csr.` shm retention ring). Backs the
@@ -5922,6 +6130,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5922
6130
  /** Per-camera counters since the last recovery report line. */
5923
6131
  recoveryStats = /* @__PURE__ */ new Map();
5924
6132
  /**
6133
+ * Per-camera counters for the frames on which the pass did NOT run, since the
6134
+ * last report line (D378). "The second pass stops running when a person is
6135
+ * beside the dog" was a live hypothesis on camera 615 and the logs could not
6136
+ * answer it: a report that counts only the passes that ran cannot say why the
6137
+ * others did not. `rateLimited` is the cadence cap, `noMotion` an empty or
6138
+ * stale motion mirror, and `tooLarge` / `covered` the two region gates.
6139
+ */
6140
+ recoverySkips = /* @__PURE__ */ new Map();
6141
+ /**
5925
6142
  * Gates the root-plane `package-detection` step to motion-settle edges +
5926
6143
  * a low-cadence safety poll instead of every frame (packages are
5927
6144
  * stationary — per-frame parcel inference is wasted cost). Consulted in
@@ -6141,7 +6358,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
6141
6358
  this.runner.onResult(async (deviceId, frame, result, _streamType, handle) => {
6142
6359
  this.retention?.noteInferenceResult(deviceId, handle, result);
6143
6360
  this.cutSubjectTiles(deviceId, result, handle);
6144
- this.emitInferenceResult(deviceId, frame, result, handle);
6361
+ const carried = await this.attachBirthEvidence(deviceId, frame, result);
6362
+ this.emitInferenceResult(deviceId, frame, carried, handle);
6145
6363
  const detectionCount = result.detections?.length ?? 0;
6146
6364
  this.runner?.noteDetectionResult(deviceId, detectionCount);
6147
6365
  if (detectionCount > 0 && this.occupancyBurstInFlight.has(deviceId)) this.occupancyBurstFound.add(deviceId);
@@ -7129,6 +7347,178 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7129
7347
  * stays at debug; a miss the caller is handed as `null` is lost work and is a
7130
7348
  * warn, carrying the numbers that decide the fix.
7131
7349
  */
7350
+ /**
7351
+ * Cut each candidate birth's own pixels from the frame that produced it, and
7352
+ * return a result carrying them (D379).
7353
+ *
7354
+ * Returns the SAME result object when there is nothing to cut, which is the
7355
+ * overwhelming majority of frames: a subject already on screen is not a
7356
+ * birth, so it costs a novelty lookup and no encode. A new result object is
7357
+ * built when there is — never a mutation of the one the executor produced.
7358
+ *
7359
+ * Every drop is counted and reported per camera. A branch that silently
7360
+ * dropped the evidence would be indistinguishable from a camera with no
7361
+ * births at all, which is the shape of failure this whole change exists to
7362
+ * remove.
7363
+ */
7364
+ async attachBirthEvidence(deviceId, frame, result) {
7365
+ const detections = result.detections;
7366
+ if (detections.length === 0) return result;
7367
+ const plan = planBirthEvidence(deviceId, detections.map((d) => ({
7368
+ bbox: d.bbox,
7369
+ score: d.score,
7370
+ macroClass: d.macroClass
7371
+ })), {
7372
+ width: result.width,
7373
+ height: result.height
7374
+ }, this.birthNovelty, Date.now());
7375
+ if (plan.length === 0) return result;
7376
+ const cost = this.birthEvidenceCost.get(deviceId) ?? {
7377
+ cut: 0,
7378
+ failed: 0,
7379
+ skippedNoPixels: 0,
7380
+ bytes: 0,
7381
+ encodeMs: 0
7382
+ };
7383
+ this.birthEvidenceCost.set(deviceId, cost);
7384
+ const byIndex = /* @__PURE__ */ new Map();
7385
+ if (!(frame.frameRef !== void 0 || frame.format === "rgb" && frame.data.byteLength >= result.width * result.height * 3)) {
7386
+ cost.skippedNoPixels += plan.length;
7387
+ this.reportBirthEvidenceCost(deviceId);
7388
+ return result;
7389
+ }
7390
+ for (const entry of plan) {
7391
+ const startedAt = Date.now();
7392
+ const jpeg = await this.encodeEvidenceCrop(frame, result, entry);
7393
+ if (jpeg === null) {
7394
+ cost.failed += 1;
7395
+ continue;
7396
+ }
7397
+ cost.cut += 1;
7398
+ cost.bytes += jpeg.byteLength;
7399
+ cost.encodeMs += Date.now() - startedAt;
7400
+ byIndex.set(entry.index, {
7401
+ jpegBase64: jpeg.toString("base64"),
7402
+ rect: entry.rect,
7403
+ tier: "analysis"
7404
+ });
7405
+ }
7406
+ this.reportBirthEvidenceCost(deviceId);
7407
+ if (byIndex.size === 0) return result;
7408
+ return {
7409
+ ...result,
7410
+ detections: detections.map((d, i) => {
7411
+ const evidence = byIndex.get(i);
7412
+ return evidence === void 0 ? d : {
7413
+ ...d,
7414
+ birthEvidence: evidence
7415
+ };
7416
+ })
7417
+ };
7418
+ }
7419
+ /**
7420
+ * One evidence crop, or `null` when neither raster can serve it.
7421
+ *
7422
+ * The process-local frame LEASE is tried first. On the co-located path
7423
+ * `DecodedFrame.data` is deliberately empty and the pixels live only behind
7424
+ * `frameRef` — reading `data` there produced `skippedNoPixels` on roughly half
7425
+ * the cluster's cameras in the first minutes after this shipped, which is
7426
+ * exactly the silent gap this whole change exists to close. It is also the
7427
+ * same lease the recovery pass resolves its views from, so it costs no new
7428
+ * mechanism and no new copy of the frame.
7429
+ *
7430
+ * The raw buffer is the fallback for a frame delivered inline. `format` is
7431
+ * checked rather than assumed: a planar frame read as RGB would produce a crop
7432
+ * of noise that re-detects as nothing — a false SUPPRESSION, the one error
7433
+ * this path must never make.
7434
+ */
7435
+ async encodeEvidenceCrop(frame, result, entry) {
7436
+ const fromLease = frame.frameRef ? await this.encodeEvidenceFromLease(frame.frameRef, entry) : null;
7437
+ if (fromLease !== null) return fromLease;
7438
+ const expectedBytes = result.width * result.height * 3;
7439
+ if (frame.format !== "rgb" || frame.data.byteLength < expectedBytes) return null;
7440
+ try {
7441
+ let pipeline = (await require_lazy_sharp.getSharp())(frame.data, { raw: {
7442
+ width: result.width,
7443
+ height: result.height,
7444
+ channels: 3
7445
+ } }).extract({
7446
+ left: entry.rect.x,
7447
+ top: entry.rect.y,
7448
+ width: entry.rect.width,
7449
+ height: entry.rect.height
7450
+ });
7451
+ if (entry.target.width !== entry.rect.width) pipeline = pipeline.resize(entry.target.width, entry.target.height, { fit: "fill" });
7452
+ return await pipeline.jpeg({ quality: 85 }).toBuffer();
7453
+ } catch {
7454
+ return null;
7455
+ }
7456
+ }
7457
+ /** The evidence crop from the process-local lease, JPEG-encoded by the view. */
7458
+ async encodeEvidenceFromLease(ref, entry) {
7459
+ const lease = require_default_detection_model.localFrameRegistry.acquire(ref);
7460
+ if (!lease) return null;
7461
+ let reason = "error";
7462
+ try {
7463
+ const view = await lease.resolve({
7464
+ crop: {
7465
+ left: entry.rect.x,
7466
+ top: entry.rect.y,
7467
+ width: entry.rect.width,
7468
+ height: entry.rect.height
7469
+ },
7470
+ content: {
7471
+ width: entry.target.width,
7472
+ height: entry.target.height
7473
+ },
7474
+ fit: "stretch",
7475
+ format: "jpeg"
7476
+ });
7477
+ if (view.kind !== "view") {
7478
+ reason = "drop";
7479
+ return null;
7480
+ }
7481
+ reason = "success";
7482
+ return Buffer.from(view.data);
7483
+ } catch {
7484
+ return null;
7485
+ } finally {
7486
+ lease.release(reason);
7487
+ }
7488
+ }
7489
+ /**
7490
+ * State, per camera, what carrying the evidence cost over the window — and
7491
+ * drain it. The operator's acceptance condition for D379 is that the runner's
7492
+ * memory and the frame path do not grow beyond a declared budget, and a
7493
+ * budget nobody measures is an estimate.
7494
+ */
7495
+ reportBirthEvidenceCost(deviceId) {
7496
+ const cost = this.birthEvidenceCost.get(deviceId);
7497
+ if (!cost) return;
7498
+ const now = Date.now();
7499
+ const lastAt = this.birthEvidenceReportedAt.get(deviceId) ?? 0;
7500
+ if (lastAt !== 0 && now - lastAt < BIRTH_EVIDENCE_REPORT_INTERVAL_MS) return;
7501
+ const windowMs = lastAt === 0 ? BIRTH_EVIDENCE_REPORT_INTERVAL_MS : now - lastAt;
7502
+ this.birthEvidenceReportedAt.set(deviceId, now);
7503
+ this.ctx.logger.info("birth evidence carried", {
7504
+ tags: { deviceId },
7505
+ meta: {
7506
+ cut: cost.cut,
7507
+ failed: cost.failed,
7508
+ skippedNoPixels: cost.skippedNoPixels,
7509
+ kb: Math.round(cost.bytes / 1024),
7510
+ encodeMs: cost.encodeMs,
7511
+ meanKb: cost.cut > 0 ? Math.round(cost.bytes / cost.cut / 1024) : 0,
7512
+ meanEncodeMs: cost.cut > 0 ? Math.round(cost.encodeMs / cost.cut) : 0,
7513
+ windowMs
7514
+ }
7515
+ });
7516
+ cost.cut = 0;
7517
+ cost.failed = 0;
7518
+ cost.skippedNoPixels = 0;
7519
+ cost.bytes = 0;
7520
+ cost.encodeMs = 0;
7521
+ }
7132
7522
  async resolveNativeCropPixels(input) {
7133
7523
  const b = input.bbox;
7134
7524
  const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
@@ -7645,12 +8035,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7645
8035
  this.motionRegions.forget(deviceId);
7646
8036
  this.lastRecoveryAt.delete(deviceId);
7647
8037
  this.recoveryStats.delete(deviceId);
8038
+ this.recoverySkips.delete(deviceId);
7648
8039
  const warnPrefix = `${deviceId}:`;
7649
8040
  for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
7650
8041
  this.attached.delete(deviceId);
7651
8042
  this.lastMotionAt.delete(deviceId);
7652
8043
  this.lastEmittedCameraMetrics.delete(deviceId);
7653
8044
  this.retention?.forgetDevice(deviceId);
8045
+ this.birthNovelty.clearDevice(deviceId);
8046
+ this.birthEvidenceCost.delete(deviceId);
8047
+ this.birthEvidenceReportedAt.delete(deviceId);
7654
8048
  this.runner?.unregisterCamera(deviceId);
7655
8049
  this.ctx?.logger.info("detachCamera", {
7656
8050
  tags: { deviceId },
@@ -8413,9 +8807,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8413
8807
  const api = this.ctx?.api;
8414
8808
  if (!api || !frame.frameRef) return result;
8415
8809
  const now = Date.now();
8416
- if (now - (this.lastRecoveryAt.get(deviceId) ?? 0) < 500) return result;
8810
+ if (now - (this.lastRecoveryAt.get(deviceId) ?? 0) < 500) {
8811
+ this.addRecoverySkips(deviceId, { rateLimited: 1 });
8812
+ return result;
8813
+ }
8417
8814
  const regions = this.motionRegions.fresh(deviceId, now);
8418
- if (regions.length === 0) return result;
8815
+ if (regions.length === 0) {
8816
+ this.addRecoverySkips(deviceId, { noMotion: 1 });
8817
+ return result;
8818
+ }
8419
8819
  const step = selectRecoveryStep(steps);
8420
8820
  if (step === null) return result;
8421
8821
  if (result.width <= 0 || result.height <= 0) return result;
@@ -8423,7 +8823,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8423
8823
  width: frame.frameRef.width,
8424
8824
  height: frame.frameRef.height
8425
8825
  };
8426
- const crops = planRecoveryCrops({
8826
+ const planInput = {
8427
8827
  regions,
8428
8828
  detections: result.detections.filter((d) => d.kind === "first-level").map((d) => ({
8429
8829
  x: d.bbox.x / result.width,
@@ -8432,8 +8832,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8432
8832
  h: d.bbox.height / result.height
8433
8833
  })),
8434
8834
  native
8435
- });
8436
- if (crops.length === 0) return result;
8835
+ };
8836
+ const crops = planRecoveryCrops(planInput);
8837
+ if (crops.length === 0) {
8838
+ const skipped = countRecoverySkips(planInput);
8839
+ this.addRecoverySkips(deviceId, {
8840
+ tooLarge: skipped.tooLarge,
8841
+ covered: skipped.covered
8842
+ });
8843
+ return result;
8844
+ }
8437
8845
  this.lastRecoveryAt.set(deviceId, now);
8438
8846
  const log = this.ctx.logger.withTags({ deviceId });
8439
8847
  const started = Date.now();
@@ -8473,7 +8881,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8473
8881
  recovered: added,
8474
8882
  ms: Date.now() - started,
8475
8883
  now,
8476
- classes: fused.slice(result.detections.length).map((d) => d.macroClass)
8884
+ classes: fused.slice(result.detections.length).map((d) => `${d.macroClass}:${d.score.toFixed(3)}`)
8477
8885
  });
8478
8886
  return added > 0 ? {
8479
8887
  ...result,
@@ -8522,6 +8930,26 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8522
8930
  lease.release(reason);
8523
8931
  }
8524
8932
  }
8933
+ /**
8934
+ * Fold a frame's non-run into the per-camera skip counters, drained by the
8935
+ * next report line (D378). Counters, never lines: a skip happens several
8936
+ * times a second per camera and a line per skip would be the noisiest thing
8937
+ * in the log.
8938
+ */
8939
+ addRecoverySkips(deviceId, delta) {
8940
+ const current = this.recoverySkips.get(deviceId) ?? {
8941
+ rateLimited: 0,
8942
+ noMotion: 0,
8943
+ tooLarge: 0,
8944
+ covered: 0
8945
+ };
8946
+ this.recoverySkips.set(deviceId, {
8947
+ rateLimited: current.rateLimited + (delta.rateLimited ?? 0),
8948
+ noMotion: current.noMotion + (delta.noMotion ?? 0),
8949
+ tooLarge: current.tooLarge + (delta.tooLarge ?? 0),
8950
+ covered: current.covered + (delta.covered ?? 0)
8951
+ });
8952
+ }
8525
8953
  /** Aggregate the pass per camera and report once per {@link RECOVERY_LOG_INTERVAL_MS}. */
8526
8954
  reportRecovery(deviceId, sample) {
8527
8955
  const stats = this.recoveryStats.get(deviceId) ?? {
@@ -8548,6 +8976,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8548
8976
  }
8549
8977
  });
8550
8978
  if (sample.now - stats.reportedAt >= RECOVERY_LOG_INTERVAL_MS) {
8979
+ const skips = this.recoverySkips.get(deviceId) ?? {
8980
+ rateLimited: 0,
8981
+ noMotion: 0,
8982
+ tooLarge: 0,
8983
+ covered: 0
8984
+ };
8551
8985
  this.ctx.logger.info("small-subject recovery: pass report", {
8552
8986
  tags: { deviceId },
8553
8987
  meta: {
@@ -8555,9 +8989,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8555
8989
  crops: next.crops,
8556
8990
  recovered: next.recovered,
8557
8991
  avgMsPerPass: next.passes > 0 ? Math.round(next.ms / next.passes) : 0,
8558
- windowMs: sample.now - stats.reportedAt
8992
+ windowMs: sample.now - stats.reportedAt,
8993
+ skipRateLimited: skips.rateLimited,
8994
+ skipNoMotion: skips.noMotion,
8995
+ skipTooLarge: skips.tooLarge,
8996
+ skipCovered: skips.covered
8559
8997
  }
8560
8998
  });
8999
+ this.recoverySkips.delete(deviceId);
8561
9000
  this.recoveryStats.set(deviceId, {
8562
9001
  passes: 0,
8563
9002
  crops: 0,