@camstack/addon-post-analysis 1.2.664 → 1.2.665

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 (16) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{clip-model-registry-Ch9Sz-Qb.mjs → clip-model-registry-DZTzG2p4.mjs} +292 -132
  3. package/dist/{clip-model-registry-TeNUAAPP.js → clip-model-registry-nn_STiGt.js} +297 -131
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CXG7YPYq.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Hy74pMMo.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BXTAZYaJ.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Br6eWzp_.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-BZ_D3_sF.mjs → hostInit-BAYLsfoZ.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +316 -38
  12. package/dist/pipeline-analytics/index.mjs +316 -38
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CIt_RDlm.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-4TTlyJ66.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-TeNUAAPP.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-nn_STiGt.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -12284,6 +12284,16 @@ var NcStateSummaryTestSchema = require_clip_model_registry.object({
12284
12284
  }))
12285
12285
  });
12286
12286
  //#endregion
12287
+ //#region src/notification-center/template-defaults.ts
12288
+ function servedTemplateDefaults(texts) {
12289
+ return [{
12290
+ id: "appliance-cycle",
12291
+ kind: "sensor",
12292
+ title: texts.raw("appliance.finished.title") ?? "",
12293
+ body: texts.raw("appliance.finished.body") ?? ""
12294
+ }];
12295
+ }
12296
+ //#endregion
12287
12297
  //#region src/notification-center/clock-text.ts
12288
12298
  /**
12289
12299
  * `{{time}}` — hours and minutes in the NOTIFICATION language.
@@ -13682,7 +13692,10 @@ var ncActions = require_clip_model_registry.defineCustomActions({
13682
13692
  catalog: require_clip_model_registry.array(require_clip_model_registry.NcConditionDescriptorSchema),
13683
13693
  taxonomy: require_clip_model_registry.NcTaxonomySchema
13684
13694
  })),
13685
- "nc.getTemplateCatalog": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({ vars: require_clip_model_registry.array(require_clip_model_registry.NcTemplateVarDescriptorSchema) })),
13695
+ "nc.getTemplateCatalog": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({
13696
+ vars: require_clip_model_registry.array(require_clip_model_registry.NcTemplateVarDescriptorSchema),
13697
+ defaults: require_clip_model_registry.array(require_clip_model_registry.NcTemplateDefaultSchema)
13698
+ })),
13686
13699
  "nc.previewTemplate": require_clip_model_registry.customAction(require_clip_model_registry.NcTemplatePreviewInputSchema, require_clip_model_registry.NcTemplatePreviewSchema, { kind: "mutation" }),
13687
13700
  "nc.createRule": require_clip_model_registry.customAction(require_clip_model_registry.object({ rule: require_clip_model_registry.NcRuleInputSchema }), require_clip_model_registry.object({ rule: require_clip_model_registry.NcRuleSchema }), {
13688
13701
  kind: "mutation",
@@ -14462,7 +14475,10 @@ function makeNcActionHandlers(deps) {
14462
14475
  catalog: [...require_clip_model_registry.NC_CONDITION_CATALOG],
14463
14476
  taxonomy: require_clip_model_registry.NC_TAXONOMY
14464
14477
  }),
14465
- "nc.getTemplateCatalog": async () => ({ vars: require_clip_model_registry.servedTemplateVars() }),
14478
+ "nc.getTemplateCatalog": async () => ({
14479
+ vars: require_clip_model_registry.servedTemplateVars(),
14480
+ defaults: deps.previewTexts === void 0 ? [] : servedTemplateDefaults(deps.previewTexts)
14481
+ }),
14466
14482
  "nc.previewTemplate": async (input) => {
14467
14483
  const texts = deps.previewTexts;
14468
14484
  if (texts === void 0) throw new Error("nc.previewTemplate: text catalog not wired");
@@ -49951,7 +49967,7 @@ async function encodeRgbCropToJpeg(bytes, width, height) {
49951
49967
  * what lets the caller stop base64-ing purely to be understood.
49952
49968
  */
49953
49969
  async function decodeJpegToRgb(jpeg) {
49954
- const { data, info } = await (0, sharp.default)(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : jpeg).raw().toBuffer({ resolveWithObject: true });
49970
+ const { data, info } = await (0, sharp.default)(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : jpeg).removeAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
49955
49971
  return {
49956
49972
  bytes: data,
49957
49973
  width: info.width,
@@ -53037,6 +53053,42 @@ function collectPackageZoneIds(rules) {
53037
53053
  }
53038
53054
  return ids;
53039
53055
  }
53056
+ /**
53057
+ * How much the package zone box grows on every side when asking whether motion
53058
+ * reached it. Mirrors the D144 dilation of the pipeline's burst trigger
53059
+ * (`PACKAGE_ZONE_DILATION` in addon-pipeline, not importable from here): about
53060
+ * one adult's width beside a doormat, the place a courier stands.
53061
+ */
53062
+ var PACKAGE_ZONE_MOTION_DILATION = .5;
53063
+ /**
53064
+ * A motion region covering more than this fraction of the frame is a light
53065
+ * change or the weather, not a person: it never counts as a deliverer.
53066
+ */
53067
+ var PACKAGE_ZONE_MOTION_MAX_REGION_FRAC = .8;
53068
+ /**
53069
+ * Did analyzer motion reach the dilated package zone with a person-sized
53070
+ * region? Unknown geometry answers false: this stamp only ever BACKS the open
53071
+ * of a candidate, so failing closed keeps the light-wedge guard intact.
53072
+ */
53073
+ function delivererMotionNearPackageZone(input) {
53074
+ const { zoneRect, frameWidth, frameHeight } = input;
53075
+ if (zoneRect === null || frameWidth <= 0 || frameHeight <= 0) return false;
53076
+ const dx = zoneRect.w * PACKAGE_ZONE_MOTION_DILATION;
53077
+ const dy = zoneRect.h * PACKAGE_ZONE_MOTION_DILATION;
53078
+ const left = Math.max(0, zoneRect.x - dx);
53079
+ const top = Math.max(0, zoneRect.y - dy);
53080
+ const right = Math.min(1, zoneRect.x + zoneRect.w + dx);
53081
+ const bottom = Math.min(1, zoneRect.y + zoneRect.h + dy);
53082
+ for (const region of input.regions) {
53083
+ const x = region.bbox.x / frameWidth;
53084
+ const y = region.bbox.y / frameHeight;
53085
+ const w = region.bbox.w / frameWidth;
53086
+ const h = region.bbox.h / frameHeight;
53087
+ if (w * h > PACKAGE_ZONE_MOTION_MAX_REGION_FRAC) continue;
53088
+ if (x < right && left < x + w && y < bottom && top < y + h) return true;
53089
+ }
53090
+ return false;
53091
+ }
53040
53092
  //#endregion
53041
53093
  //#region src/pipeline-analytics/pipeline/package/package-collector-mirror.ts
53042
53094
  /**
@@ -53982,6 +54034,15 @@ var PACKAGE_STILLNESS_TTL_MS = 5 * 6e4;
53982
54034
  */
53983
54035
  var PACKAGE_STILLNESS_ASSOC_IOU = .3;
53984
54036
  /**
54037
+ * Lowest score of a below-threshold box that may CONTINUE an open candidate
54038
+ * (D665: a box under the floor may continue, never open). Observed on a real
54039
+ * parcel after the day to IR flip: 0.35 to 0.50.
54040
+ */
54041
+ var PACKAGE_STILLNESS_CONTINUATION_MIN_SCORE = .3;
54042
+ /** Above-floor sightings a candidate needs before a sub-floor box may count:
54043
+ * one confident look plus a doormat-shaped reading is not a parcel. */
54044
+ var PACKAGE_CONTINUATION_MIN_REAL_HITS = 2;
54045
+ /**
53985
54046
  * Two absence reads closer than this are ONE look, not two. A settle burst
53986
54047
  * runs the package step 5 s apart and a poll 45 s apart, so a real second
53987
54048
  * look is always further away; two frames of one moment (a re-dispatch, a
@@ -54031,6 +54092,7 @@ var PackageStillnessTracker = class {
54031
54092
  const next = [...this.byDevice.get(input.deviceId) ?? []];
54032
54093
  const consumed = /* @__PURE__ */ new Set();
54033
54094
  const promoted = [];
54095
+ const continuedLog = [];
54034
54096
  for (const s of input.sightings) {
54035
54097
  const index = this.bestMatch(next, s, consumed);
54036
54098
  if (index < 0) {
@@ -54043,17 +54105,33 @@ var PackageStillnessTracker = class {
54043
54105
  if (prev === void 0) continue;
54044
54106
  consumed.add(prev.id);
54045
54107
  const updated = confirm(prev, input, s);
54046
- const minHits = input.minHits ?? this.config.minHits;
54047
- if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
54048
- next[index] = updated;
54049
- continue;
54050
- }
54051
- const parked = {
54052
- ...updated,
54053
- promotedAt: input.timestamp
54108
+ const parked = this.promoteWhenReady(updated, input);
54109
+ next[index] = parked ?? updated;
54110
+ if (parked !== null) promoted.push(toEntry(parked));
54111
+ }
54112
+ if (input.looked === true && input.continuationBoxes !== void 0) for (let i = 0; i < next.length; i += 1) {
54113
+ const c = next[i];
54114
+ if (c === void 0 || c.promotedAt !== null || consumed.has(c.id)) continue;
54115
+ if (c.realHits < PACKAGE_CONTINUATION_MIN_REAL_HITS) continue;
54116
+ const box = this.continuationFor(c, input.continuationBoxes);
54117
+ if (box === null) continue;
54118
+ consumed.add(c.id);
54119
+ const continued = {
54120
+ ...c,
54121
+ lastSeenAt: input.timestamp,
54122
+ hits: c.hits + 1,
54123
+ absenceReads: [],
54124
+ lastLook: "present",
54125
+ earlyUnbackedReported: false
54054
54126
  };
54055
- next[index] = parked;
54056
- promoted.push(toEntry(parked));
54127
+ continuedLog.push({
54128
+ id: c.id,
54129
+ hits: continued.hits,
54130
+ score: box.score
54131
+ });
54132
+ const parked = this.promoteWhenReady(continued, input);
54133
+ next[i] = parked ?? continued;
54134
+ if (parked !== null) promoted.push(toEntry(parked));
54057
54135
  }
54058
54136
  const present = input.rawPackageBoxes !== void 0 ? withRawPresence(next, consumed, input.rawPackageBoxes, input.timestamp, this.config.assocIou) : next;
54059
54137
  const looked = input.looked === true && input.rawPackageBoxes !== void 0 ? withLook(present, consumed, input, input.rawPackageBoxes, this.config.assocIou) : present;
@@ -54089,7 +54167,8 @@ var PackageStillnessTracker = class {
54089
54167
  ...input.lastActivityAt !== void 0 ? { lastActivityAt: input.lastActivityAt } : {}
54090
54168
  }),
54091
54169
  earlyDepartureIds: [...early],
54092
- earlyEvidenceUnbacked: unbacked
54170
+ earlyEvidenceUnbacked: unbacked,
54171
+ continued: continuedLog
54093
54172
  };
54094
54173
  }
54095
54174
  /** Why complete early evidence may NOT conclude, or null when it may. */
@@ -54116,7 +54195,8 @@ var PackageStillnessTracker = class {
54116
54195
  ...input.lastActivityAt !== void 0 ? { lastActivityAt: input.lastActivityAt } : {}
54117
54196
  }),
54118
54197
  earlyDepartureIds: [],
54119
- earlyEvidenceUnbacked: []
54198
+ earlyEvidenceUnbacked: [],
54199
+ continued: []
54120
54200
  };
54121
54201
  }
54122
54202
  /** The devices holding a candidate — what a sweep has to visit. */
@@ -54180,6 +54260,43 @@ var PackageStillnessTracker = class {
54180
54260
  /** Best same-class candidate over the association floor, ignoring any already
54181
54261
  * consumed by an earlier sighting in THIS frame (two boxes never collapse
54182
54262
  * into one parcel). */
54263
+ continuationFor(c, boxes) {
54264
+ let best = null;
54265
+ let bestIou = this.config.assocIou;
54266
+ for (const b of boxes) {
54267
+ if (b.score < PACKAGE_STILLNESS_CONTINUATION_MIN_SCORE) continue;
54268
+ const overlap = iou$2(c.bbox, b.bbox);
54269
+ if (overlap >= bestIou) {
54270
+ bestIou = overlap;
54271
+ best = b;
54272
+ }
54273
+ }
54274
+ return best;
54275
+ }
54276
+ promoteWhenReady(candidate, input) {
54277
+ const updated = this.backedLate(candidate, input);
54278
+ const minHits = input.minHits ?? this.config.minHits;
54279
+ return updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs ? {
54280
+ ...updated,
54281
+ promotedAt: input.timestamp
54282
+ } : null;
54283
+ }
54284
+ /**
54285
+ * Zone motion learned AFTER the open still backs the candidate when its stamp
54286
+ * lies in the window around the FIRST sighting (the same slack a pick-up
54287
+ * gets): the backing stays anchored at the open, a later passer-by never
54288
+ * validates scenery (D1003).
54289
+ */
54290
+ backedLate(c, input) {
54291
+ if (c.activityBackedAtOpen || c.promotedAt !== null) return c;
54292
+ const at = input.lastDelivererMotionAt;
54293
+ const windowMs = input.activityWindowMs ?? 0;
54294
+ if (at === void 0 || windowMs <= 0) return c;
54295
+ return at >= c.firstSeenAt - windowMs && at <= c.firstSeenAt + this.config.roundGapMs ? {
54296
+ ...c,
54297
+ activityBackedAtOpen: true
54298
+ } : c;
54299
+ }
54183
54300
  bestMatch(candidates, s, consumed) {
54184
54301
  let bestIndex = -1;
54185
54302
  let bestIou = this.config.assocIou;
@@ -54196,7 +54313,7 @@ var PackageStillnessTracker = class {
54196
54313
  }
54197
54314
  openCandidate(input, s) {
54198
54315
  const windowMs = input.activityWindowMs ?? 0;
54199
- const activityBackedAtOpen = windowMs <= 0 || input.lastActivityAt !== void 0 && input.timestamp - input.lastActivityAt <= windowMs;
54316
+ const activityBackedAtOpen = windowMs <= 0 || input.lastActivityAt !== void 0 && input.timestamp - input.lastActivityAt <= windowMs || input.lastDelivererMotionAt !== void 0 && input.timestamp - input.lastDelivererMotionAt <= windowMs;
54200
54317
  return {
54201
54318
  id: this.newId(),
54202
54319
  deviceId: input.deviceId,
@@ -54207,6 +54324,7 @@ var PackageStillnessTracker = class {
54207
54324
  firstSeenAt: input.timestamp,
54208
54325
  lastSeenAt: input.timestamp,
54209
54326
  hits: 1,
54327
+ realHits: 1,
54210
54328
  promotedAt: null,
54211
54329
  absenceReads: [],
54212
54330
  lastLook: null,
@@ -54229,6 +54347,7 @@ function confirm(prev, input, s) {
54229
54347
  frameHeight: input.frameHeight,
54230
54348
  lastSeenAt: input.timestamp,
54231
54349
  hits: prev.hits + 1,
54350
+ realHits: prev.realHits + 1,
54232
54351
  absenceReads: [],
54233
54352
  lastLook: "present",
54234
54353
  earlyUnbackedReported: false,
@@ -54765,6 +54884,9 @@ var FrameState = class {
54765
54884
  * which would let the streetlight wedge vouch for its own delivery.
54766
54885
  */
54767
54886
  lastNonPackageActivityAt = /* @__PURE__ */ new Map();
54887
+ /** Last analyzer motion near a package drop-off zone, per device (D1003).
54888
+ * Backs the OPEN of a candidate only; never a pick-up witness. */
54889
+ lastPackageZoneMotionAt = /* @__PURE__ */ new Map();
54768
54890
  /**
54769
54891
  * WHO was in the drop-off zone, and when (D404). The pick-up is CONCLUDED
54770
54892
  * minutes after the fact, so the collector must be remembered while it is
@@ -58124,6 +58246,7 @@ function nextPackageLookDropEpisodes(previous, drops) {
58124
58246
  //#region src/pipeline-analytics/pipeline/package/package-look-evidence.ts
58125
58247
  function packageLookEvidence(input) {
58126
58248
  const raw = [];
58249
+ const continuation = [];
58127
58250
  for (const d of input.frame.detections) {
58128
58251
  if (d.kind !== "first-level" || !input.packageClasses.has(d.macroClass)) continue;
58129
58252
  raw.push({
@@ -58136,11 +58259,16 @@ function packageLookEvidence(input) {
58136
58259
  for (const d of input.frame.discarded ?? []) {
58137
58260
  if (!input.packageClasses.has(d.macroClass)) continue;
58138
58261
  const [x1, y1, x2, y2] = d.bbox;
58139
- raw.push({
58262
+ const box = {
58140
58263
  x: x1,
58141
58264
  y: y1,
58142
58265
  w: x2 - x1,
58143
58266
  h: y2 - y1
58267
+ };
58268
+ raw.push(box);
58269
+ if (d.reason === "below-threshold") continuation.push({
58270
+ bbox: box,
58271
+ score: d.score
58144
58272
  });
58145
58273
  }
58146
58274
  return {
@@ -58152,6 +58280,7 @@ function packageLookEvidence(input) {
58152
58280
  h: t.bbox.h
58153
58281
  })),
58154
58282
  rawPackageBoxes: raw,
58283
+ continuationBoxes: continuation,
58155
58284
  earlyPickup: input.earlyPickupEnabled
58156
58285
  };
58157
58286
  }
@@ -58339,6 +58468,13 @@ function clamp01(value) {
58339
58468
  }
58340
58469
  //#endregion
58341
58470
  //#region src/pipeline-analytics/pipeline/package/package-scene-diff.ts
58471
+ /**
58472
+ * Zone-crop scene difference for a packages rule (D931).
58473
+ *
58474
+ * A uniform brightness shift is removed by subtracting the median luma delta,
58475
+ * so a shadow that lifts the whole crop leaves no blob. What remains is a
58476
+ * connected region. The package model is not consulted here.
58477
+ */
58342
58478
  /** Residual, after the median shift, that counts as a changed pixel. */
58343
58479
  var PACKAGE_SCENE_DIFF_LUMA_THRESHOLD = 28;
58344
58480
  /** A changed region this large, relative to the zone, is the zone itself. */
@@ -58353,12 +58489,26 @@ var PACKAGE_SCENE_DIFF_SETTLE_MS = 3e3;
58353
58489
  var PACKAGE_SCENE_DIFF_LOOK_GAP_MS = 2e3;
58354
58490
  /** Confidence stamped on a scene-diff sighting the model did not box. */
58355
58491
  var PACKAGE_SCENE_DIFF_CONFIDENCE = .5;
58492
+ /** True when the crop was shot in infrared (no colour). */
58493
+ function isIrCrop(crop) {
58494
+ return crop.chromaSpread < IR_CHROMA_THRESHOLD;
58495
+ }
58356
58496
  function diffZoneLuma(input) {
58357
58497
  const { before, after } = input;
58358
58498
  if (before.width !== after.width || before.height !== after.height || before.luma.length !== after.luma.length) return {
58359
58499
  blobs: [],
58360
58500
  rejections: [{ reason: "crop-mismatch" }]
58361
58501
  };
58502
+ if (isIrCrop(before) !== isIrCrop(after)) return {
58503
+ blobs: [],
58504
+ rejections: [{
58505
+ reason: "lighting-mode-changed",
58506
+ chroma: {
58507
+ before: before.chromaSpread,
58508
+ after: after.chromaSpread
58509
+ }
58510
+ }]
58511
+ };
58362
58512
  const median = medianDelta(before.luma, after.luma);
58363
58513
  const mask = residualMask(before.luma, after.luma, median);
58364
58514
  const zonePx = before.width * before.height;
@@ -58573,6 +58723,7 @@ function okCrop(rgb, width, height, jpeg) {
58573
58723
  width,
58574
58724
  height,
58575
58725
  luma: lumaFromRgb(rgb, width, height),
58726
+ chromaSpread: chromaSpreadFromRgb(rgb, width, height),
58576
58727
  jpeg: new Uint8Array(jpeg)
58577
58728
  };
58578
58729
  }
@@ -58589,6 +58740,46 @@ function lumaFromRgb(bytes, width, height) {
58589
58740
  }
58590
58741
  return luma;
58591
58742
  }
58743
+ /** The grid `IR_CHROMA_THRESHOLD` was calibrated on (`chromaSpread`, scene-crop.ts). */
58744
+ var CHROMA_GRID = 32;
58745
+ /**
58746
+ * Mean of max(r,g,b) - min(r,g,b), 0..255, measured on a 32x32 box-averaged
58747
+ * copy of the crop: the same measure and grid as `chromaSpread` in
58748
+ * `scene-crop.ts` (which needs sharp), so `IR_CHROMA_THRESHOLD` means the same
58749
+ * thing. Averaging first matters: sensor noise and JPEG chroma speckle on an IR
58750
+ * frame cancel out, where per-pixel they read as colour.
58751
+ */
58752
+ function chromaSpreadFromRgb(bytes, width, height) {
58753
+ if (width <= 0 || height <= 0 || bytes.length < width * height * 3) return 0;
58754
+ const gw = Math.min(CHROMA_GRID, width);
58755
+ const gh = Math.min(CHROMA_GRID, height);
58756
+ const sum = new Float64Array(gw * gh * 3);
58757
+ const count = new Float64Array(gw * gh);
58758
+ for (let y = 0; y < height; y++) {
58759
+ const cy = Math.min(gh - 1, Math.floor(y * gh / height));
58760
+ for (let x = 0; x < width; x++) {
58761
+ const cx = Math.min(gw - 1, Math.floor(x * gw / width));
58762
+ const cell = cy * gw + cx;
58763
+ const o = (y * width + x) * 3;
58764
+ sum[cell * 3] = (sum[cell * 3] ?? 0) + (bytes[o] ?? 0);
58765
+ sum[cell * 3 + 1] = (sum[cell * 3 + 1] ?? 0) + (bytes[o + 1] ?? 0);
58766
+ sum[cell * 3 + 2] = (sum[cell * 3 + 2] ?? 0) + (bytes[o + 2] ?? 0);
58767
+ count[cell] = (count[cell] ?? 0) + 1;
58768
+ }
58769
+ }
58770
+ let total = 0;
58771
+ let cells = 0;
58772
+ for (let cell = 0; cell < gw * gh; cell++) {
58773
+ const n = count[cell] ?? 0;
58774
+ if (n === 0) continue;
58775
+ const r = (sum[cell * 3] ?? 0) / n;
58776
+ const g = (sum[cell * 3 + 1] ?? 0) / n;
58777
+ const b = (sum[cell * 3 + 2] ?? 0) / n;
58778
+ total += Math.max(r, g, b) - Math.min(r, g, b);
58779
+ cells += 1;
58780
+ }
58781
+ return cells === 0 ? 0 : total / cells;
58782
+ }
58592
58783
  //#endregion
58593
58784
  //#region src/pipeline-analytics/pipeline/package/package-scene-step.ts
58594
58785
  /**
@@ -58688,6 +58879,14 @@ function compareAfterLeave(prev, input) {
58688
58879
  minAreaFrac: input.minAreaFrac,
58689
58880
  occluders: input.occluders
58690
58881
  });
58882
+ if (diff.rejections.some((r) => r.reason === "lighting-mode-changed")) return {
58883
+ state: {
58884
+ ...rememberClear(prev, input).state,
58885
+ lastDiffAt: input.timestamp
58886
+ },
58887
+ sightings: [],
58888
+ rejections: diff.rejections
58889
+ };
58691
58890
  const blocked = diff.rejections.some((r) => r.reason === "overlaps-person" || r.reason === "overlaps-vehicle");
58692
58891
  if (diff.blobs.length === 0) {
58693
58892
  if (blocked) return {
@@ -58731,6 +58930,20 @@ function confirmPending(prev, input) {
58731
58930
  sightings: [],
58732
58931
  rejections: []
58733
58932
  };
58933
+ if (isIrCrop(prev.after) !== isIrCrop(input.crop)) return {
58934
+ state: {
58935
+ ...rememberClear(prev, input).state,
58936
+ lastDiffAt: input.timestamp
58937
+ },
58938
+ sightings: [],
58939
+ rejections: [{
58940
+ reason: "lighting-mode-changed",
58941
+ chroma: {
58942
+ before: prev.after.chromaSpread,
58943
+ after: input.crop.chromaSpread
58944
+ }
58945
+ }]
58946
+ };
58734
58947
  const sightings = [];
58735
58948
  const rejections = [];
58736
58949
  const still = [];
@@ -58777,7 +58990,7 @@ function judgeBlob(prev, input, blob) {
58777
58990
  bbox: blob.bbox
58778
58991
  } };
58779
58992
  const stable = meanAbsInBox(after, crop, blob.bbox) <= PACKAGE_SCENE_DIFF_STABLE_MEAN;
58780
- const present = meanAbsInBox(before, crop, blob.bbox) >= PACKAGE_SCENE_DIFF_LUMA_THRESHOLD;
58993
+ const present = isIrCrop(before) !== isIrCrop(crop) || meanAbsInBox(before, crop, blob.bbox) >= PACKAGE_SCENE_DIFF_LUMA_THRESHOLD;
58781
58994
  if (!stable || !present) return { rejection: {
58782
58995
  reason: "unstable",
58783
58996
  bbox: blob.bbox,
@@ -58945,7 +59158,8 @@ var PackageSceneCoordinator = class {
58945
59158
  const crop = {
58946
59159
  width: fetched.width,
58947
59160
  height: fetched.height,
58948
- luma: fetched.luma
59161
+ luma: fetched.luma,
59162
+ chromaSpread: fetched.chromaSpread
58949
59163
  };
58950
59164
  const stepped = stepPackageScene({
58951
59165
  deviceId,
@@ -59011,6 +59225,10 @@ var PackageSceneCoordinator = class {
59011
59225
  tags: { deviceId },
59012
59226
  meta: {
59013
59227
  reason: rejection.reason,
59228
+ ...rejection.chroma !== void 0 ? {
59229
+ chromaBefore: Number(rejection.chroma.before.toFixed(2)),
59230
+ chromaAfter: Number(rejection.chroma.after.toFixed(2))
59231
+ } : {},
59014
59232
  ...rejection.areaFrac !== void 0 ? { areaFrac: Number(rejection.areaFrac.toFixed(5)) } : {}
59015
59233
  }
59016
59234
  });
@@ -59119,14 +59337,8 @@ function planPackageSceneFrame(deviceId, input) {
59119
59337
  frameWidth: input.frameWidth,
59120
59338
  frameHeight: input.frameHeight
59121
59339
  });
59122
- const sceneRect = sceneZones.size === 0 ? null : packageZoneCropRect({
59123
- zones: input.zones,
59124
- packageZoneIds: sceneZones,
59125
- frameWidth: input.frameWidth,
59126
- frameHeight: input.frameHeight
59127
- });
59128
59340
  return {
59129
- sightings: sceneRect === null ? input.sightings : withoutSceneCenters(input, sceneRect),
59341
+ sightings: input.sightings,
59130
59342
  note: {
59131
59343
  deviceId,
59132
59344
  timestamp: input.timestamp,
@@ -59169,15 +59381,6 @@ function occludersOf(tracked) {
59169
59381
  }
59170
59382
  return out;
59171
59383
  }
59172
- function withoutSceneCenters(input, rect) {
59173
- if (input.frameWidth <= 0 || input.frameHeight <= 0) return input.sightings;
59174
- return input.sightings.filter((sighting) => !centerInside(sighting.bbox, rect, input));
59175
- }
59176
- function centerInside(bbox, rect, input) {
59177
- const cx = (bbox.x + bbox.w / 2) / input.frameWidth;
59178
- const cy = (bbox.y + bbox.h / 2) / input.frameHeight;
59179
- return cx >= rect.x && cx < rect.x + rect.w && cy >= rect.y && cy < rect.y + rect.h;
59180
- }
59181
59384
  //#endregion
59182
59385
  //#region src/pipeline-analytics/pipeline/package/package-llm-confirm.ts
59183
59386
  /**
@@ -71157,6 +71360,7 @@ Object.freeze({
71157
71360
  "addon-pages-source": ["listPages"],
71158
71361
  "addon-routes": ["getRoutes"],
71159
71362
  "addon-widgets-source": ["listWidgets"],
71363
+ "board-key-source": ["listSources"],
71160
71364
  "broker": ["list", "listProviders"],
71161
71365
  "calendar-source": ["listCalendars", "listEvents"],
71162
71366
  "custom-model-registry": ["listModels"],
@@ -72685,7 +72889,7 @@ function buildGlobalSettingsSchema(cap) {
72685
72889
  type: "slider",
72686
72890
  key: "packageDropActivityWindowSec",
72687
72891
  label: "Deliverer window",
72688
- description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Motion elsewhere on the camera does not count. 0 disables the requirement.",
72892
+ description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Person-sized motion beside the drop-off zone also counts when nobody was tracked. Motion elsewhere on the camera does not count. 0 disables the requirement.",
72689
72893
  min: 0,
72690
72894
  max: 1800,
72691
72895
  step: 30,
@@ -87205,6 +87409,13 @@ var NC_STILL_WAIT_MAX_AGE_MS = 3e4;
87205
87409
  function canStillGainMedia(entry) {
87206
87410
  return entry.recordKind === "object-event";
87207
87411
  }
87412
+ /** The variables that carry a finished cycle's run summary. */
87413
+ var CYCLE_SUMMARY_VARS = [
87414
+ "duration",
87415
+ "energy",
87416
+ "afterDuration",
87417
+ "usedEnergy"
87418
+ ];
87208
87419
  var NcDispatcher = class {
87209
87420
  deps;
87210
87421
  targetCache = null;
@@ -88108,6 +88319,7 @@ var NcDispatcher = class {
88108
88319
  }
88109
88320
  /** Rules already warned about a deprecated variable in this process — once each. */
88110
88321
  deprecatedVarWarned = /* @__PURE__ */ new Set();
88322
+ cycleSummaryWarned = /* @__PURE__ */ new Set();
88111
88323
  /**
88112
88324
  * A rule template written by an older client may still say `{{camera}}`. It
88113
88325
  * renders (the alias), and the rule is named once so the operator can edit it.
@@ -88127,6 +88339,27 @@ var NcDispatcher = class {
88127
88339
  });
88128
88340
  }
88129
88341
  /**
88342
+ * A FINISHED-cycle row whose own template places none of the four cycle
88343
+ * variables delivers no run summary (D391: accepted, produced less). Said
88344
+ * once per rule; the operator's text is never touched.
88345
+ */
88346
+ warnCycleSummaryDropped(entry) {
88347
+ const deviceId = entry.payload.subject.applianceCycle?.deviceId;
88348
+ const template = entry.payload.template;
88349
+ if (deviceId === void 0 || template === void 0) return;
88350
+ if (this.cycleSummaryWarned.has(entry.ruleId)) return;
88351
+ const used = new Set([...templatePlaceholders(template.title ?? ""), ...templatePlaceholders(template.body ?? "")]);
88352
+ if (CYCLE_SUMMARY_VARS.some((name) => used.has(name))) return;
88353
+ this.cycleSummaryWarned.add(entry.ruleId);
88354
+ this.deps.logger.warn("appliance cycle finished but the rule template places no run summary — the message carries no duration or energy", {
88355
+ tags: {
88356
+ deviceId,
88357
+ ruleId: entry.ruleId
88358
+ },
88359
+ meta: { placeholders: [...CYCLE_SUMMARY_VARS] }
88360
+ });
88361
+ }
88362
+ /**
88130
88363
  * The facts of a finished appliance cycle; unknown on every other row and on
88131
88364
  * any failure (a part that cannot be read is omitted, never a blocked send).
88132
88365
  */
@@ -88171,6 +88404,7 @@ var NcDispatcher = class {
88171
88404
  const cycleFacts = await this.applianceCycleFacts(entry);
88172
88405
  const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels, void 0, cycleFacts);
88173
88406
  this.warnDeprecatedVars(entry);
88407
+ this.warnCycleSummaryDropped(entry);
88174
88408
  const systemCount = systemEvent?.textCount !== void 0 ? { count: systemEvent.textCount } : {};
88175
88409
  const title = renderRowTitle(this.texts, entry, vars);
88176
88410
  const authoredBody = entry.payload.template?.body;
@@ -102153,7 +102387,10 @@ function buildProvider(host) {
102153
102387
  catalog: [...require_clip_model_registry.NC_CONDITION_CATALOG],
102154
102388
  taxonomy: require_clip_model_registry.NC_TAXONOMY
102155
102389
  }),
102156
- getTemplateCatalog: async () => ({ vars: require_clip_model_registry.servedTemplateVars() }),
102390
+ getTemplateCatalog: async () => ({
102391
+ vars: require_clip_model_registry.servedTemplateVars(),
102392
+ defaults: servedTemplateDefaults(host.texts)
102393
+ }),
102157
102394
  previewTemplate: async (input) => previewTemplate(host.texts, input),
102158
102395
  getHistory: async ({ filter }) => {
102159
102396
  const entries = await host.outbox.queryHistory({
@@ -125090,6 +125327,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125090
125327
  this.packageDropDetector?.forgetDevice(data.deviceId);
125091
125328
  this.packageScene?.forget(data.deviceId);
125092
125329
  this.frameState.lastNonPackageActivityAt.delete(data.deviceId);
125330
+ this.frameState.lastPackageZoneMotionAt.delete(data.deviceId);
125093
125331
  this.frameState.packageCollectors.forgetDevice(data.deviceId);
125094
125332
  this.frameState.packageOversizeLogAt.delete(data.deviceId);
125095
125333
  this.frameState.packageLookFrames.delete(data.deviceId);
@@ -125114,6 +125352,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125114
125352
  this.packageDropDetector?.forgetDevice(deviceId);
125115
125353
  this.packageScene?.forget(deviceId);
125116
125354
  this.frameState.lastNonPackageActivityAt.delete(deviceId);
125355
+ this.frameState.lastPackageZoneMotionAt.delete(deviceId);
125117
125356
  this.frameState.packageCollectors.forgetDevice(deviceId);
125118
125357
  this.frameState.packageOversizeLogAt.delete(deviceId);
125119
125358
  this.frameState.packageLookFrames.delete(deviceId);
@@ -127686,6 +127925,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
127686
127925
  detected,
127687
127926
  atMs: timestamp
127688
127927
  });
127928
+ if (detected) await this.stampPackageZoneMotion(deviceId, payload, timestamp);
127689
127929
  if (detected) this.eventStore?.noteMotionSample(deviceId, payload.regionCount, timestamp);
127690
127930
  await this.motionEpisodes?.observe(deviceId, "analyzer", detected, timestamp, {
127691
127931
  regionCount: payload.regionCount,
@@ -127704,6 +127944,28 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
127704
127944
  });
127705
127945
  }
127706
127946
  /**
127947
+ * Stamp analyzer motion that reached a package drop-off zone (D1003): the
127948
+ * courier who never became an L1 track still backs the OPEN of a candidate.
127949
+ * Onboard motion carries no regions or dimensions and never stamps.
127950
+ */
127951
+ async stampPackageZoneMotion(deviceId, payload, timestamp) {
127952
+ const proxy = await this.ensureProxy(deviceId);
127953
+ const packageZoneIds = collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []);
127954
+ if (packageZoneIds.size === 0) return;
127955
+ const zoneRect = packageZoneCropRect({
127956
+ zones: proxy?.state.zones.value?.zones ?? [],
127957
+ packageZoneIds,
127958
+ frameWidth: payload.frameWidth,
127959
+ frameHeight: payload.frameHeight
127960
+ });
127961
+ if (delivererMotionNearPackageZone({
127962
+ regions: payload.regions,
127963
+ frameWidth: payload.frameWidth,
127964
+ frameHeight: payload.frameHeight,
127965
+ zoneRect
127966
+ })) this.frameState.lastPackageZoneMotionAt.set(deviceId, timestamp);
127967
+ }
127968
+ /**
127707
127969
  * Persist firmware-driven (onboard / device-activity) motion events into
127708
127970
  * the SAME episode tracker as {@link handleMotionAnalysis}, keyed per
127709
127971
  * `(deviceId, source)` so the two paths can never share — or fight over —
@@ -128036,6 +128298,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128036
128298
  encodeJpeg: request.encodeJpeg
128037
128299
  }, request.pin);
128038
128300
  }
128301
+ zoneMotionStamp(deviceId) {
128302
+ const at = this.frameState.lastPackageZoneMotionAt.get(deviceId);
128303
+ return at === void 0 ? {} : { lastDelivererMotionAt: at };
128304
+ }
128039
128305
  deliverPackageSceneSightings(delivery) {
128040
128306
  const settings = this.packageDropSettingsFromCache(delivery.deviceId);
128041
128307
  const lastActivityAt = this.frameState.lastNonPackageActivityAt.get(delivery.deviceId);
@@ -128049,6 +128315,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128049
128315
  minDwellMs: settings.packageDropDwellSec * 1e3,
128050
128316
  activityWindowMs: settings.packageDropActivityWindowSec * 1e3,
128051
128317
  ...lastActivityAt !== void 0 ? { lastActivityAt } : {},
128318
+ ...this.zoneMotionStamp(delivery.deviceId),
128052
128319
  looked: false
128053
128320
  });
128054
128321
  this.bridgePackageStillness({
@@ -128234,6 +128501,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128234
128501
  minDwellMs: input.settings.packageDropDwellSec * 1e3,
128235
128502
  activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
128236
128503
  ...lastActivityAt !== void 0 ? { lastActivityAt } : {},
128504
+ ...this.zoneMotionStamp(input.deviceId),
128237
128505
  ...lookEvidence
128238
128506
  });
128239
128507
  this.bridgePackageStillness({
@@ -128429,6 +128697,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128429
128697
  });
128430
128698
  }).catch(() => void 0);
128431
128699
  }
128700
+ for (const c of outcome.continued) this.ctx.logger.info("package candidate continued by a sub-floor box", {
128701
+ tags,
128702
+ meta: {
128703
+ entryId: c.id,
128704
+ hits: c.hits,
128705
+ score: c.score,
128706
+ via: input.via
128707
+ }
128708
+ });
128432
128709
  for (const u of outcome.earlyEvidenceUnbacked) this.ctx.logger.info("early pick-up evidence complete — nobody around, left to the TTL", {
128433
128710
  tags,
128434
128711
  meta: {
@@ -128456,6 +128733,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128456
128733
  observedDwellMs,
128457
128734
  requiredDwellMs,
128458
128735
  activityBackedAtOpen: ex.activityBackedAtOpen,
128736
+ lastZoneMotionAt: this.frameState.lastPackageZoneMotionAt.get(input.deviceId) ?? null,
128459
128737
  reasons
128460
128738
  }
128461
128739
  });