@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
@@ -1,7 +1,7 @@
1
- import { $t as number, Et as errMsg, Ht as nodePin, Jt as array, Lt as createEvent, Nt as BaseAddon, Qt as literal, R as customAction, U as deriveDetailCropRect, V as defineCustomActions, Yt as boolean, Z as failureContributionCapability, Zt as lazy, _ as FailureCounters, dt as pickNativeLeaseOverride, en as object, in as EventCategory, m as DEVICE_BACKEND_TO_FORMAT, nn as string, p as DEFAULT_NATIVE_LEASE_SETTINGS, pt as pipelineRunnerCapability, qt as _enum, tt as loadContributionCapability, u as DEFAULT_DETAIL_CROP_CONVENTION, ut as pickDetailCropConvention } from "../dist-mYDK58Jp.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-xRsaPwBa.mjs";
3
- import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-BGBAmtEp.mjs";
4
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-C0R1DDC0.mjs";
1
+ import { $t as literal, Dt as errMsg, Jt as _enum, Pt as BaseAddon, Qt as lazy, R as customAction, Rt as createEvent, U as deriveDetailCropRect, Ut as nodePin, V as defineCustomActions, Xt as boolean, Yt as array, Z as failureContributionCapability, _ as FailureCounters, dt as pickDetailCropConvention, en as number, ft as pickNativeLeaseOverride, in as string, m as DEVICE_BACKEND_TO_FORMAT, mt as pipelineRunnerCapability, on as EventCategory, p as DEFAULT_NATIVE_LEASE_SETTINGS, tn as object, tt as loadContributionCapability, u as DEFAULT_DETAIL_CROP_CONVENTION } from "../dist-Y4BsF_EC.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-B5ouEQYN.mjs";
3
+ import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-CplzzTmN.mjs";
4
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-_7zrToTo.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
7
7
  import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
@@ -1970,6 +1970,169 @@ function buildBenchRunPipelineInput(req) {
1970
1970
  ...req.plane ? { plane: req.plane } : {}
1971
1971
  };
1972
1972
  }
1973
+ /**
1974
+ * How long a box stays "seen".
1975
+ *
1976
+ * Sized on the gate's own clock, not on a guess: `maxDeferralMs` defaults to
1977
+ * 2 000 ms, so a birth's whole life as a candidate is 2 s. A window shorter
1978
+ * than that would re-cut the same subject inside one deferral; a much longer
1979
+ * one would stop a subject that genuinely left and returned from being cut
1980
+ * again. 3 000 ms is the deferral budget plus one 1 s margin.
1981
+ */
1982
+ var BIRTH_EVIDENCE_NOVELTY_WINDOW_MS = 3e3;
1983
+ /**
1984
+ * Padding around the box, as a fraction of its size.
1985
+ *
1986
+ * 0.15 deliberately equals `CONFIRMATION_CROP_PADDING` in post-analysis: the
1987
+ * gate promotes a secondary subject by mapping a crop-space box back into the
1988
+ * frame through the rectangle the crop was cut from, and the two derivations
1989
+ * disagreeing is how a promoted box lands somewhere the pixels never were.
1990
+ */
1991
+ var BIRTH_EVIDENCE_PADDING = .15;
1992
+ /**
1993
+ * Boxes remembered per camera. A frame with more subjects than this keeps the
1994
+ * most recent — the cap is about bounding a crowd, and a box that fell out is
1995
+ * merely eligible for a cut it will not get twice (the encode budget above is
1996
+ * the real spend). 64 is 16x the tile tier's per-frame subject cap.
1997
+ */
1998
+ var REMEMBERED_BOXES_PER_DEVICE = 64;
1999
+ /**
2000
+ * Per-camera memory of the boxes recently on screen.
2001
+ *
2002
+ * Per CAMERA and never global: a box on 617 says nothing about 615, and a
2003
+ * shared structure would make one camera's traffic decide another's cuts —
2004
+ * which is the exact shape of the node-wide sharing this whole change moves
2005
+ * away from.
2006
+ *
2007
+ * Judged per CLASS. A dog at its owner's knee is 100% contained in the person's
2008
+ * box; a class-blind test would call it "already seen" and it would reach the
2009
+ * gate with no pixels — the failure D372, D377 and D378 each fought at a
2010
+ * different layer.
2011
+ */
2012
+ var BirthNoveltyMemory = class {
2013
+ byDevice = /* @__PURE__ */ new Map();
2014
+ /**
2015
+ * Is this box a subject the camera has not been showing?
2016
+ *
2017
+ * Pure: it does not record anything. The caller records the whole frame with
2018
+ * {@link note}, once, so a box that lost the encode budget is still counted
2019
+ * as seen — otherwise the cap turns into a permanent re-cut of the same
2020
+ * subjects.
2021
+ */
2022
+ isNovel(deviceId, box, macroClass, nowMs) {
2023
+ const boxes = this.byDevice.get(deviceId);
2024
+ if (!boxes) return true;
2025
+ for (const remembered of boxes) {
2026
+ if (nowMs - remembered.atMs > 3e3) continue;
2027
+ if (remembered.macroClass !== macroClass) continue;
2028
+ if (iou(remembered.rect, box) >= .3) return false;
2029
+ }
2030
+ return true;
2031
+ }
2032
+ /** Record a frame's detections and drop everything past the window / the cap. */
2033
+ note(deviceId, detections, nowMs) {
2034
+ const kept = (this.byDevice.get(deviceId) ?? []).filter((b) => nowMs - b.atMs <= BIRTH_EVIDENCE_NOVELTY_WINDOW_MS);
2035
+ for (const d of detections) {
2036
+ if (!isUsable$1(d.bbox)) continue;
2037
+ kept.push({
2038
+ rect: d.bbox,
2039
+ macroClass: d.macroClass,
2040
+ atMs: nowMs
2041
+ });
2042
+ }
2043
+ this.byDevice.set(deviceId, kept.length > REMEMBERED_BOXES_PER_DEVICE ? kept.slice(kept.length - REMEMBERED_BOXES_PER_DEVICE) : kept);
2044
+ }
2045
+ /** How many boxes this camera currently remembers (tests / diagnostics). */
2046
+ rememberedCount(deviceId) {
2047
+ return this.byDevice.get(deviceId)?.length ?? 0;
2048
+ }
2049
+ /** Forget a camera wholesale (detach, pipeline reset). */
2050
+ clearDevice(deviceId) {
2051
+ this.byDevice.delete(deviceId);
2052
+ }
2053
+ };
2054
+ /**
2055
+ * The cuts worth taking on this frame, highest confidence first, capped.
2056
+ *
2057
+ * Records EVERY usable detection in `memory` — including the ones the cap
2058
+ * refused — so the budget bounds the spend without turning into a leak.
2059
+ */
2060
+ function planBirthEvidence(deviceId, detections, frame, memory, nowMs, maxPerFrame = 2) {
2061
+ if (frame.width <= 0 || frame.height <= 0) return [];
2062
+ const usable = detections.map((d, index) => ({
2063
+ d,
2064
+ index
2065
+ })).filter(({ d }) => isUsable$1(d.bbox));
2066
+ const novel = usable.filter(({ d }) => memory.isNovel(deviceId, d.bbox, d.macroClass, nowMs));
2067
+ memory.note(deviceId, usable.map(({ d }) => ({
2068
+ bbox: d.bbox,
2069
+ macroClass: d.macroClass
2070
+ })), nowMs);
2071
+ const plan = [];
2072
+ for (const { d, index } of [...novel].sort((a, b) => b.d.score - a.d.score)) {
2073
+ if (plan.length >= maxPerFrame) break;
2074
+ const rect = padAndClamp(d.bbox, frame);
2075
+ if (rect === null) continue;
2076
+ plan.push({
2077
+ index,
2078
+ rect,
2079
+ target: targetFor(rect)
2080
+ });
2081
+ }
2082
+ return plan.sort((a, b) => a.index - b.index);
2083
+ }
2084
+ /**
2085
+ * The padded, clamped source rectangle — or `null` for a box that is not worth
2086
+ * an encode. A degenerate box is DROPPED rather than clamped: a zero-area crop
2087
+ * serves nothing and a NaN one would poison the encoder's geometry.
2088
+ */
2089
+ function padAndClamp(box, frame) {
2090
+ if (Math.min(box.width, box.height) < 128) return null;
2091
+ const padX = box.width * BIRTH_EVIDENCE_PADDING;
2092
+ const padY = box.height * BIRTH_EVIDENCE_PADDING;
2093
+ const left = Math.max(0, Math.floor(box.x - padX));
2094
+ const top = Math.max(0, Math.floor(box.y - padY));
2095
+ const right = Math.min(frame.width, Math.ceil(box.x + box.width + padX));
2096
+ const bottom = Math.min(frame.height, Math.ceil(box.y + box.height + padY));
2097
+ const width = right - left;
2098
+ const height = bottom - top;
2099
+ if (width <= 0 || height <= 0) return null;
2100
+ return {
2101
+ x: left,
2102
+ y: top,
2103
+ width,
2104
+ height
2105
+ };
2106
+ }
2107
+ /** Encoded size: capped, aspect-preserving, never upscaled. */
2108
+ function targetFor(rect) {
2109
+ if (rect.width <= 320) return {
2110
+ width: rect.width,
2111
+ height: rect.height
2112
+ };
2113
+ const scale = 320 / rect.width;
2114
+ return {
2115
+ width: 320,
2116
+ height: Math.max(1, Math.round(rect.height * scale))
2117
+ };
2118
+ }
2119
+ /** A box with finite, positive extent. */
2120
+ function isUsable$1(box) {
2121
+ return Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height) && box.width > 0 && box.height > 0;
2122
+ }
2123
+ /** Intersection over union of two absolute-pixel rectangles. */
2124
+ function iou(a, b) {
2125
+ const left = Math.max(a.x, b.x);
2126
+ const top = Math.max(a.y, b.y);
2127
+ const right = Math.min(a.x + a.width, b.x + b.width);
2128
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
2129
+ const w = right - left;
2130
+ const h = bottom - top;
2131
+ if (w <= 0 || h <= 0) return 0;
2132
+ const intersection = w * h;
2133
+ const union = a.width * a.height + b.width * b.height - intersection;
2134
+ return union <= 0 ? 0 : intersection / union;
2135
+ }
1973
2136
  //#endregion
1974
2137
  //#region src/pipeline-runner/capacity/capacity-runner.ts
1975
2138
  /**
@@ -5149,12 +5312,6 @@ var PipelineRunner = class {
5149
5312
  return null;
5150
5313
  }
5151
5314
  };
5152
- /**
5153
- * How much of a region must lie OUTSIDE every full-frame detection for the
5154
- * pass to run on it. A person's own blob (dilated, with shadow) is covered
5155
- * 70–90% by the person box; a person-and-dog blob is covered ~55% by it.
5156
- */
5157
- var RECOVERY_MIN_UNCOVERED_FRACTION = .35;
5158
5315
  function area(r) {
5159
5316
  return Math.max(0, r.w) * Math.max(0, r.h);
5160
5317
  }
@@ -5202,25 +5359,53 @@ function cropsForRegion(region, native) {
5202
5359
  return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
5203
5360
  }
5204
5361
  /**
5205
- * The crops the second pass runs on this frame — possibly none.
5362
+ * The ONE place the two region gates are decided. `planRecoveryCrops` filters
5363
+ * on it and {@link countRecoverySkips} counts it, so the report can never
5364
+ * describe a gate the planner does not actually apply.
5365
+ */
5366
+ function classifyRegion(region, uncovered) {
5367
+ if (area(region) > .25) return "too-large";
5368
+ if (uncovered < .35) return "covered";
5369
+ return "eligible";
5370
+ }
5371
+ /**
5372
+ * WHY A FRAME PLANNED NO CROP, AS A NUMBER (D378).
5206
5373
  *
5207
- * A region qualifies when it is small (`RECOVERY_MAX_REGION_FRAME_FRACTION`)
5208
- * and mostly uncovered by the full-frame boxes
5209
- * (`RECOVERY_MIN_UNCOVERED_FRACTION`). Qualifying regions are served largest
5210
- * uncovered area first under `RECOVERY_MAX_CROPS_PER_FRAME`; a crop whose
5211
- * centre already lies inside a planned crop is folded into it. A frame that
5212
- * cannot hold the smallest crop yields nothing: it is already seen at scale.
5374
+ * The pass report counts the passes that RAN, so "the second pass stops running
5375
+ * when a person is beside the dog" — a plausible reading of
5376
+ * {@link RECOVERY_MAX_REGION_FRAME_FRACTION} and
5377
+ * {@link RECOVERY_MIN_UNCOVERED_FRACTION}, and a live hypothesis on camera 615
5378
+ * — could not be confirmed or killed from the logs at all. This classifies the
5379
+ * regions a frame offered, against the same predicate the planner filters on.
5213
5380
  */
5381
+ function countRecoverySkips(input) {
5382
+ let tooLarge = 0;
5383
+ let covered = 0;
5384
+ let eligible = 0;
5385
+ for (const region of input.regions) switch (classifyRegion(region, uncoveredFraction(region, input.detections))) {
5386
+ case "too-large":
5387
+ tooLarge += 1;
5388
+ break;
5389
+ case "covered":
5390
+ covered += 1;
5391
+ break;
5392
+ case "eligible":
5393
+ eligible += 1;
5394
+ break;
5395
+ }
5396
+ return {
5397
+ tooLarge,
5398
+ covered,
5399
+ eligible
5400
+ };
5401
+ }
5214
5402
  function planRecoveryCrops(input) {
5215
5403
  const { native } = input;
5216
5404
  if (Math.min(native.width, native.height) < 640) return [];
5217
5405
  const ranked = input.regions.map((region) => ({
5218
5406
  region,
5219
5407
  uncovered: uncoveredFraction(region, input.detections)
5220
- })).filter(({ region, uncovered }) => {
5221
- if (area(region) > .25) return false;
5222
- return uncovered >= RECOVERY_MIN_UNCOVERED_FRACTION;
5223
- }).toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
5408
+ })).filter(({ region, uncovered }) => classifyRegion(region, uncovered) === "eligible").toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
5224
5409
  const planned = [];
5225
5410
  for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
5226
5411
  if (planned.length >= 2) return planned;
@@ -5251,6 +5436,11 @@ function remapCropDetections(detections, crop, view, result, native) {
5251
5436
  if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
5252
5437
  out.push({
5253
5438
  ...detection,
5439
+ nativeRecovery: {
5440
+ score: detection.score,
5441
+ cropSidePx: crop.side,
5442
+ viewPx: view.width
5443
+ },
5254
5444
  bbox: {
5255
5445
  x: (crop.left + x * viewToNative) * nativeToResultX,
5256
5446
  y: (crop.top + y * viewToNative) * nativeToResultY,
@@ -5505,6 +5695,15 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
5505
5695
  var MAX_BOUND_FRAME_CUTTERS = 256;
5506
5696
  /** How often a camera reports its undispatched cuts. */
5507
5697
  var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
5698
+ /**
5699
+ * How often a camera reports what carrying its birth evidence COST (D379).
5700
+ *
5701
+ * The cost of this change is the one thing that must be measured rather than
5702
+ * estimated, so the report is per camera, states encodes / bytes / encode-ms
5703
+ * over the window, and drains its counters — a camera that cut nothing says
5704
+ * nothing until it does.
5705
+ */
5706
+ var BIRTH_EVIDENCE_REPORT_INTERVAL_MS = 6e5;
5508
5707
  var DEFAULT_CONFIG = {
5509
5708
  maxQueueDepth: 30,
5510
5709
  maxConcurrentInferences: 16,
@@ -5866,6 +6065,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
5866
6065
  tileCutDrops = /* @__PURE__ */ new Map();
5867
6066
  tileCutDropLoggedAt = /* @__PURE__ */ new Map();
5868
6067
  /**
6068
+ * Per-camera memory of the boxes recently on screen — what decides whether a
6069
+ * detection is worth cutting its own evidence for (D379). Per CAMERA, never
6070
+ * global: one camera's traffic must never decide another's cuts.
6071
+ */
6072
+ birthNovelty = new BirthNoveltyMemory();
6073
+ /** Per-camera cost of the carried evidence, drained by its report line. */
6074
+ birthEvidenceCost = /* @__PURE__ */ new Map();
6075
+ birthEvidenceReportedAt = /* @__PURE__ */ new Map();
6076
+ /**
5869
6077
  * Node-level RAM store of retained rgb detection frames, keyed by the
5870
6078
  * `FrameHandle` that rides the inference-result event (shm-ring removal
5871
6079
  * 2026-07-16 — replaces the `csr.` shm retention ring). Backs the
@@ -5915,6 +6123,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
5915
6123
  /** Per-camera counters since the last recovery report line. */
5916
6124
  recoveryStats = /* @__PURE__ */ new Map();
5917
6125
  /**
6126
+ * Per-camera counters for the frames on which the pass did NOT run, since the
6127
+ * last report line (D378). "The second pass stops running when a person is
6128
+ * beside the dog" was a live hypothesis on camera 615 and the logs could not
6129
+ * answer it: a report that counts only the passes that ran cannot say why the
6130
+ * others did not. `rateLimited` is the cadence cap, `noMotion` an empty or
6131
+ * stale motion mirror, and `tooLarge` / `covered` the two region gates.
6132
+ */
6133
+ recoverySkips = /* @__PURE__ */ new Map();
6134
+ /**
5918
6135
  * Gates the root-plane `package-detection` step to motion-settle edges +
5919
6136
  * a low-cadence safety poll instead of every frame (packages are
5920
6137
  * stationary — per-frame parcel inference is wasted cost). Consulted in
@@ -6134,7 +6351,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
6134
6351
  this.runner.onResult(async (deviceId, frame, result, _streamType, handle) => {
6135
6352
  this.retention?.noteInferenceResult(deviceId, handle, result);
6136
6353
  this.cutSubjectTiles(deviceId, result, handle);
6137
- this.emitInferenceResult(deviceId, frame, result, handle);
6354
+ const carried = await this.attachBirthEvidence(deviceId, frame, result);
6355
+ this.emitInferenceResult(deviceId, frame, carried, handle);
6138
6356
  const detectionCount = result.detections?.length ?? 0;
6139
6357
  this.runner?.noteDetectionResult(deviceId, detectionCount);
6140
6358
  if (detectionCount > 0 && this.occupancyBurstInFlight.has(deviceId)) this.occupancyBurstFound.add(deviceId);
@@ -7122,6 +7340,178 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7122
7340
  * stays at debug; a miss the caller is handed as `null` is lost work and is a
7123
7341
  * warn, carrying the numbers that decide the fix.
7124
7342
  */
7343
+ /**
7344
+ * Cut each candidate birth's own pixels from the frame that produced it, and
7345
+ * return a result carrying them (D379).
7346
+ *
7347
+ * Returns the SAME result object when there is nothing to cut, which is the
7348
+ * overwhelming majority of frames: a subject already on screen is not a
7349
+ * birth, so it costs a novelty lookup and no encode. A new result object is
7350
+ * built when there is — never a mutation of the one the executor produced.
7351
+ *
7352
+ * Every drop is counted and reported per camera. A branch that silently
7353
+ * dropped the evidence would be indistinguishable from a camera with no
7354
+ * births at all, which is the shape of failure this whole change exists to
7355
+ * remove.
7356
+ */
7357
+ async attachBirthEvidence(deviceId, frame, result) {
7358
+ const detections = result.detections;
7359
+ if (detections.length === 0) return result;
7360
+ const plan = planBirthEvidence(deviceId, detections.map((d) => ({
7361
+ bbox: d.bbox,
7362
+ score: d.score,
7363
+ macroClass: d.macroClass
7364
+ })), {
7365
+ width: result.width,
7366
+ height: result.height
7367
+ }, this.birthNovelty, Date.now());
7368
+ if (plan.length === 0) return result;
7369
+ const cost = this.birthEvidenceCost.get(deviceId) ?? {
7370
+ cut: 0,
7371
+ failed: 0,
7372
+ skippedNoPixels: 0,
7373
+ bytes: 0,
7374
+ encodeMs: 0
7375
+ };
7376
+ this.birthEvidenceCost.set(deviceId, cost);
7377
+ const byIndex = /* @__PURE__ */ new Map();
7378
+ if (!(frame.frameRef !== void 0 || frame.format === "rgb" && frame.data.byteLength >= result.width * result.height * 3)) {
7379
+ cost.skippedNoPixels += plan.length;
7380
+ this.reportBirthEvidenceCost(deviceId);
7381
+ return result;
7382
+ }
7383
+ for (const entry of plan) {
7384
+ const startedAt = Date.now();
7385
+ const jpeg = await this.encodeEvidenceCrop(frame, result, entry);
7386
+ if (jpeg === null) {
7387
+ cost.failed += 1;
7388
+ continue;
7389
+ }
7390
+ cost.cut += 1;
7391
+ cost.bytes += jpeg.byteLength;
7392
+ cost.encodeMs += Date.now() - startedAt;
7393
+ byIndex.set(entry.index, {
7394
+ jpegBase64: jpeg.toString("base64"),
7395
+ rect: entry.rect,
7396
+ tier: "analysis"
7397
+ });
7398
+ }
7399
+ this.reportBirthEvidenceCost(deviceId);
7400
+ if (byIndex.size === 0) return result;
7401
+ return {
7402
+ ...result,
7403
+ detections: detections.map((d, i) => {
7404
+ const evidence = byIndex.get(i);
7405
+ return evidence === void 0 ? d : {
7406
+ ...d,
7407
+ birthEvidence: evidence
7408
+ };
7409
+ })
7410
+ };
7411
+ }
7412
+ /**
7413
+ * One evidence crop, or `null` when neither raster can serve it.
7414
+ *
7415
+ * The process-local frame LEASE is tried first. On the co-located path
7416
+ * `DecodedFrame.data` is deliberately empty and the pixels live only behind
7417
+ * `frameRef` — reading `data` there produced `skippedNoPixels` on roughly half
7418
+ * the cluster's cameras in the first minutes after this shipped, which is
7419
+ * exactly the silent gap this whole change exists to close. It is also the
7420
+ * same lease the recovery pass resolves its views from, so it costs no new
7421
+ * mechanism and no new copy of the frame.
7422
+ *
7423
+ * The raw buffer is the fallback for a frame delivered inline. `format` is
7424
+ * checked rather than assumed: a planar frame read as RGB would produce a crop
7425
+ * of noise that re-detects as nothing — a false SUPPRESSION, the one error
7426
+ * this path must never make.
7427
+ */
7428
+ async encodeEvidenceCrop(frame, result, entry) {
7429
+ const fromLease = frame.frameRef ? await this.encodeEvidenceFromLease(frame.frameRef, entry) : null;
7430
+ if (fromLease !== null) return fromLease;
7431
+ const expectedBytes = result.width * result.height * 3;
7432
+ if (frame.format !== "rgb" || frame.data.byteLength < expectedBytes) return null;
7433
+ try {
7434
+ let pipeline = (await getSharp())(frame.data, { raw: {
7435
+ width: result.width,
7436
+ height: result.height,
7437
+ channels: 3
7438
+ } }).extract({
7439
+ left: entry.rect.x,
7440
+ top: entry.rect.y,
7441
+ width: entry.rect.width,
7442
+ height: entry.rect.height
7443
+ });
7444
+ if (entry.target.width !== entry.rect.width) pipeline = pipeline.resize(entry.target.width, entry.target.height, { fit: "fill" });
7445
+ return await pipeline.jpeg({ quality: 85 }).toBuffer();
7446
+ } catch {
7447
+ return null;
7448
+ }
7449
+ }
7450
+ /** The evidence crop from the process-local lease, JPEG-encoded by the view. */
7451
+ async encodeEvidenceFromLease(ref, entry) {
7452
+ const lease = localFrameRegistry.acquire(ref);
7453
+ if (!lease) return null;
7454
+ let reason = "error";
7455
+ try {
7456
+ const view = await lease.resolve({
7457
+ crop: {
7458
+ left: entry.rect.x,
7459
+ top: entry.rect.y,
7460
+ width: entry.rect.width,
7461
+ height: entry.rect.height
7462
+ },
7463
+ content: {
7464
+ width: entry.target.width,
7465
+ height: entry.target.height
7466
+ },
7467
+ fit: "stretch",
7468
+ format: "jpeg"
7469
+ });
7470
+ if (view.kind !== "view") {
7471
+ reason = "drop";
7472
+ return null;
7473
+ }
7474
+ reason = "success";
7475
+ return Buffer.from(view.data);
7476
+ } catch {
7477
+ return null;
7478
+ } finally {
7479
+ lease.release(reason);
7480
+ }
7481
+ }
7482
+ /**
7483
+ * State, per camera, what carrying the evidence cost over the window — and
7484
+ * drain it. The operator's acceptance condition for D379 is that the runner's
7485
+ * memory and the frame path do not grow beyond a declared budget, and a
7486
+ * budget nobody measures is an estimate.
7487
+ */
7488
+ reportBirthEvidenceCost(deviceId) {
7489
+ const cost = this.birthEvidenceCost.get(deviceId);
7490
+ if (!cost) return;
7491
+ const now = Date.now();
7492
+ const lastAt = this.birthEvidenceReportedAt.get(deviceId) ?? 0;
7493
+ if (lastAt !== 0 && now - lastAt < BIRTH_EVIDENCE_REPORT_INTERVAL_MS) return;
7494
+ const windowMs = lastAt === 0 ? BIRTH_EVIDENCE_REPORT_INTERVAL_MS : now - lastAt;
7495
+ this.birthEvidenceReportedAt.set(deviceId, now);
7496
+ this.ctx.logger.info("birth evidence carried", {
7497
+ tags: { deviceId },
7498
+ meta: {
7499
+ cut: cost.cut,
7500
+ failed: cost.failed,
7501
+ skippedNoPixels: cost.skippedNoPixels,
7502
+ kb: Math.round(cost.bytes / 1024),
7503
+ encodeMs: cost.encodeMs,
7504
+ meanKb: cost.cut > 0 ? Math.round(cost.bytes / cost.cut / 1024) : 0,
7505
+ meanEncodeMs: cost.cut > 0 ? Math.round(cost.encodeMs / cost.cut) : 0,
7506
+ windowMs
7507
+ }
7508
+ });
7509
+ cost.cut = 0;
7510
+ cost.failed = 0;
7511
+ cost.skippedNoPixels = 0;
7512
+ cost.bytes = 0;
7513
+ cost.encodeMs = 0;
7514
+ }
7125
7515
  async resolveNativeCropPixels(input) {
7126
7516
  const b = input.bbox;
7127
7517
  const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
@@ -7638,12 +8028,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7638
8028
  this.motionRegions.forget(deviceId);
7639
8029
  this.lastRecoveryAt.delete(deviceId);
7640
8030
  this.recoveryStats.delete(deviceId);
8031
+ this.recoverySkips.delete(deviceId);
7641
8032
  const warnPrefix = `${deviceId}:`;
7642
8033
  for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
7643
8034
  this.attached.delete(deviceId);
7644
8035
  this.lastMotionAt.delete(deviceId);
7645
8036
  this.lastEmittedCameraMetrics.delete(deviceId);
7646
8037
  this.retention?.forgetDevice(deviceId);
8038
+ this.birthNovelty.clearDevice(deviceId);
8039
+ this.birthEvidenceCost.delete(deviceId);
8040
+ this.birthEvidenceReportedAt.delete(deviceId);
7647
8041
  this.runner?.unregisterCamera(deviceId);
7648
8042
  this.ctx?.logger.info("detachCamera", {
7649
8043
  tags: { deviceId },
@@ -8406,9 +8800,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8406
8800
  const api = this.ctx?.api;
8407
8801
  if (!api || !frame.frameRef) return result;
8408
8802
  const now = Date.now();
8409
- if (now - (this.lastRecoveryAt.get(deviceId) ?? 0) < 500) return result;
8803
+ if (now - (this.lastRecoveryAt.get(deviceId) ?? 0) < 500) {
8804
+ this.addRecoverySkips(deviceId, { rateLimited: 1 });
8805
+ return result;
8806
+ }
8410
8807
  const regions = this.motionRegions.fresh(deviceId, now);
8411
- if (regions.length === 0) return result;
8808
+ if (regions.length === 0) {
8809
+ this.addRecoverySkips(deviceId, { noMotion: 1 });
8810
+ return result;
8811
+ }
8412
8812
  const step = selectRecoveryStep(steps);
8413
8813
  if (step === null) return result;
8414
8814
  if (result.width <= 0 || result.height <= 0) return result;
@@ -8416,7 +8816,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8416
8816
  width: frame.frameRef.width,
8417
8817
  height: frame.frameRef.height
8418
8818
  };
8419
- const crops = planRecoveryCrops({
8819
+ const planInput = {
8420
8820
  regions,
8421
8821
  detections: result.detections.filter((d) => d.kind === "first-level").map((d) => ({
8422
8822
  x: d.bbox.x / result.width,
@@ -8425,8 +8825,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8425
8825
  h: d.bbox.height / result.height
8426
8826
  })),
8427
8827
  native
8428
- });
8429
- if (crops.length === 0) return result;
8828
+ };
8829
+ const crops = planRecoveryCrops(planInput);
8830
+ if (crops.length === 0) {
8831
+ const skipped = countRecoverySkips(planInput);
8832
+ this.addRecoverySkips(deviceId, {
8833
+ tooLarge: skipped.tooLarge,
8834
+ covered: skipped.covered
8835
+ });
8836
+ return result;
8837
+ }
8430
8838
  this.lastRecoveryAt.set(deviceId, now);
8431
8839
  const log = this.ctx.logger.withTags({ deviceId });
8432
8840
  const started = Date.now();
@@ -8466,7 +8874,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8466
8874
  recovered: added,
8467
8875
  ms: Date.now() - started,
8468
8876
  now,
8469
- classes: fused.slice(result.detections.length).map((d) => d.macroClass)
8877
+ classes: fused.slice(result.detections.length).map((d) => `${d.macroClass}:${d.score.toFixed(3)}`)
8470
8878
  });
8471
8879
  return added > 0 ? {
8472
8880
  ...result,
@@ -8515,6 +8923,26 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8515
8923
  lease.release(reason);
8516
8924
  }
8517
8925
  }
8926
+ /**
8927
+ * Fold a frame's non-run into the per-camera skip counters, drained by the
8928
+ * next report line (D378). Counters, never lines: a skip happens several
8929
+ * times a second per camera and a line per skip would be the noisiest thing
8930
+ * in the log.
8931
+ */
8932
+ addRecoverySkips(deviceId, delta) {
8933
+ const current = this.recoverySkips.get(deviceId) ?? {
8934
+ rateLimited: 0,
8935
+ noMotion: 0,
8936
+ tooLarge: 0,
8937
+ covered: 0
8938
+ };
8939
+ this.recoverySkips.set(deviceId, {
8940
+ rateLimited: current.rateLimited + (delta.rateLimited ?? 0),
8941
+ noMotion: current.noMotion + (delta.noMotion ?? 0),
8942
+ tooLarge: current.tooLarge + (delta.tooLarge ?? 0),
8943
+ covered: current.covered + (delta.covered ?? 0)
8944
+ });
8945
+ }
8518
8946
  /** Aggregate the pass per camera and report once per {@link RECOVERY_LOG_INTERVAL_MS}. */
8519
8947
  reportRecovery(deviceId, sample) {
8520
8948
  const stats = this.recoveryStats.get(deviceId) ?? {
@@ -8541,6 +8969,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8541
8969
  }
8542
8970
  });
8543
8971
  if (sample.now - stats.reportedAt >= RECOVERY_LOG_INTERVAL_MS) {
8972
+ const skips = this.recoverySkips.get(deviceId) ?? {
8973
+ rateLimited: 0,
8974
+ noMotion: 0,
8975
+ tooLarge: 0,
8976
+ covered: 0
8977
+ };
8544
8978
  this.ctx.logger.info("small-subject recovery: pass report", {
8545
8979
  tags: { deviceId },
8546
8980
  meta: {
@@ -8548,9 +8982,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8548
8982
  crops: next.crops,
8549
8983
  recovered: next.recovered,
8550
8984
  avgMsPerPass: next.passes > 0 ? Math.round(next.ms / next.passes) : 0,
8551
- windowMs: sample.now - stats.reportedAt
8985
+ windowMs: sample.now - stats.reportedAt,
8986
+ skipRateLimited: skips.rateLimited,
8987
+ skipNoMotion: skips.noMotion,
8988
+ skipTooLarge: skips.tooLarge,
8989
+ skipCovered: skips.covered
8552
8990
  }
8553
8991
  });
8992
+ this.recoverySkips.delete(deviceId);
8554
8993
  this.recoveryStats.set(deviceId, {
8555
8994
  passes: 0,
8556
8995
  crops: 0,
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-HG5geL9t.js");
1
+ const require_dist = require("./dist-CBa1j3On.js");
2
2
  let node_fs = require("node:fs");
3
3
  node_fs = require_dist.__toESM(node_fs);
4
4
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { st as parseProcStatus } from "./dist-mYDK58Jp.mjs";
1
+ import { ct as parseProcStatus } from "./dist-Y4BsF_EC.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts