@camstack/addon-post-analysis 1.2.182 → 1.2.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg$1, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-DvmLRRIq.mjs";
1
+ import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg$1, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-CNIOT48v.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -27813,6 +27813,7 @@ var StoragePressureTracker = class {
27813
27813
  var OWNER_KINDS = [
27814
27814
  "event",
27815
27815
  "track",
27816
+ "summary",
27816
27817
  "face",
27817
27818
  "identity",
27818
27819
  "plate",
@@ -28396,7 +28397,8 @@ var MediaStore = class {
28396
28397
  relativePath: path
28397
28398
  })).toString("base64"),
28398
28399
  sizeBytes,
28399
- timestamp
28400
+ timestamp,
28401
+ deviceId: Number(data["deviceId"])
28400
28402
  };
28401
28403
  } catch {
28402
28404
  return null;
@@ -30670,7 +30672,7 @@ async function mapWithConcurrency(items, limit, mapper) {
30670
30672
  * all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
30671
30673
  * all-zero track scores 0.
30672
30674
  */
30673
- var WEIGHT_CONFIDENCE = .3;
30675
+ var WEIGHT_CONFIDENCE$1 = .3;
30674
30676
  var WEIGHT_CLASS = .2;
30675
30677
  var WEIGHT_DWELL = .2;
30676
30678
  var WEIGHT_PROXIMITY = .12;
@@ -30708,7 +30710,7 @@ var ANIMAL_CLASSES = new Set([
30708
30710
  "cat"
30709
30711
  ]);
30710
30712
  /** Clamp to [0,1]. */
30711
- function clamp01$1(x) {
30713
+ function clamp01$2(x) {
30712
30714
  if (Number.isNaN(x)) return 0;
30713
30715
  if (x < 0) return 0;
30714
30716
  if (x > 1) return 1;
@@ -30742,7 +30744,7 @@ function computeImportance(input) {
30742
30744
  const terms = [
30743
30745
  {
30744
30746
  reason: "confidence",
30745
- value: WEIGHT_CONFIDENCE * clamp01$1(input.peakConfidence)
30747
+ value: WEIGHT_CONFIDENCE$1 * clamp01$2(input.peakConfidence)
30746
30748
  },
30747
30749
  {
30748
30750
  reason: "class",
@@ -30750,15 +30752,15 @@ function computeImportance(input) {
30750
30752
  },
30751
30753
  {
30752
30754
  reason: "dwell",
30753
- value: WEIGHT_DWELL * clamp01$1(input.durationMs / DWELL_FULL_MS)
30755
+ value: WEIGHT_DWELL * clamp01$2(input.durationMs / DWELL_FULL_MS)
30754
30756
  },
30755
30757
  {
30756
30758
  reason: "proximity",
30757
- value: WEIGHT_PROXIMITY * clamp01$1(input.peakBboxAreaFrac / SIZE_FULL)
30759
+ value: WEIGHT_PROXIMITY * clamp01$2(input.peakBboxAreaFrac / SIZE_FULL)
30758
30760
  },
30759
30761
  {
30760
30762
  reason: "travel",
30761
- value: WEIGHT_TRAVEL * clamp01$1(input.totalDistance / 2)
30763
+ value: WEIGHT_TRAVEL * clamp01$2(input.totalDistance / 2)
30762
30764
  },
30763
30765
  {
30764
30766
  reason: "zone",
@@ -30777,11 +30779,11 @@ function computeImportance(input) {
30777
30779
  }
30778
30780
  const identified = input.identityLabel !== void 0 && input.identityLabel.length > 0;
30779
30781
  if (input.netDisplacementFrac !== void 0 && input.pathSpanFrac !== void 0 && input.netDisplacementFrac < .02 && input.pathSpanFrac < .04 && !identified) return {
30780
- importance: clamp01$1(sum * STATIC_IMPORTANCE_MULTIPLIER),
30782
+ importance: clamp01$2(sum * STATIC_IMPORTANCE_MULTIPLIER),
30781
30783
  reason: best.reason
30782
30784
  };
30783
30785
  return {
30784
- importance: clamp01$1(sum),
30786
+ importance: clamp01$2(sum),
30785
30787
  reason: best.reason
30786
30788
  };
30787
30789
  }
@@ -33479,7 +33481,7 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
33479
33481
  * it, so it can be loose enough to see the near misses.
33480
33482
  */
33481
33483
  var CO_MOVING_MIN_CONTAINMENT = .6;
33482
- function intersectionArea$3(a, b) {
33484
+ function intersectionArea$4(a, b) {
33483
33485
  return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
33484
33486
  }
33485
33487
  /**
@@ -33499,7 +33501,7 @@ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMEN
33499
33501
  if (o.className === b.className) continue;
33500
33502
  const areaO = o.bbox.w * o.bbox.h;
33501
33503
  if (areaO <= 0) continue;
33502
- const inter = intersectionArea$3(b.bbox, o.bbox);
33504
+ const inter = intersectionArea$4(b.bbox, o.bbox);
33503
33505
  if (inter <= 0) continue;
33504
33506
  const union = areaB + areaO - inter;
33505
33507
  const containment = inter / Math.min(areaB, areaO);
@@ -33931,7 +33933,7 @@ function pairRiders(detections) {
33931
33933
  * diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
33932
33934
  */
33933
33935
  var GROWTH_CENTROID_FACTOR = 2.3;
33934
- function intersectionArea$2(a, b) {
33936
+ function intersectionArea$3(a, b) {
33935
33937
  const x1 = Math.max(a.x, b.x);
33936
33938
  const y1 = Math.max(a.y, b.y);
33937
33939
  const x2 = Math.min(a.x + a.w, b.x + b.w);
@@ -33969,7 +33971,7 @@ function growthAdjacentMatch(track, det, ctx) {
33969
33971
  const larger = Math.max(trackArea, detArea);
33970
33972
  const smaller = Math.min(trackArea, detArea);
33971
33973
  if (larger / smaller < 1.5) return false;
33972
- if (intersectionArea$2(track, det) / smaller >= .5) return false;
33974
+ if (intersectionArea$3(track, det) / smaller >= .5) return false;
33973
33975
  const tc = bboxCentroid(track);
33974
33976
  const dc = bboxCentroid(det);
33975
33977
  return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
@@ -46730,6 +46732,15 @@ var CACHE_CONTROL = "public, max-age=31536000, immutable";
46730
46732
  var THUMB_DEFAULT_SIZE = 160;
46731
46733
  var THUMB_MIN_SIZE = 64;
46732
46734
  var THUMB_MAX_SIZE = 320;
46735
+ /** Set by the HUB for a `csv_*` share token admitted on this plane; never by a client. */
46736
+ var SHARE_DEVICES_HEADER = "x-camstack-share-devices";
46737
+ /** The device ids a share scope allows, or `null` when the request carries no scope. */
46738
+ function shareScopeDevices(headers) {
46739
+ const raw = headers[SHARE_DEVICES_HEADER];
46740
+ const value = Array.isArray(raw) ? raw.join(",") : raw;
46741
+ if (value === void 0) return null;
46742
+ return new Set(value.split(",").map((part) => Number.parseInt(part.trim(), 10)).filter((id) => Number.isInteger(id) && id >= 0));
46743
+ }
46733
46744
  /**
46734
46745
  * Parse the `?kind=…` query into a preferred stored media kind. Returns null
46735
46746
  * when unset. The value is a free-form kind token (e.g. `crop`); the resolver
@@ -46802,6 +46813,14 @@ function createEventMediaHandler(deps) {
46802
46813
  res.writeHead(404, { "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}` }).end();
46803
46814
  return;
46804
46815
  }
46816
+ const scope = shareScopeDevices(req.headers);
46817
+ if (scope !== null) {
46818
+ const owner = media.deviceId ?? null;
46819
+ if (owner === null || !scope.has(owner)) {
46820
+ res.writeHead(403).end();
46821
+ return;
46822
+ }
46823
+ }
46805
46824
  const etag = "\"" + media.key + "\"";
46806
46825
  const serverTiming = `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`;
46807
46826
  if (req.headers["if-none-match"] === etag) {
@@ -48272,7 +48291,7 @@ var RetrainAnnotationStore = class {
48272
48291
  };
48273
48292
  //#endregion
48274
48293
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
48275
- function clamp01(v) {
48294
+ function clamp01$1(v) {
48276
48295
  return v < 0 ? 0 : v > 1 ? 1 : v;
48277
48296
  }
48278
48297
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -48348,10 +48367,10 @@ function deriveZoneCropRect(bbox, zone) {
48348
48367
  function deriveSubjectCropRect(bbox, padding) {
48349
48368
  if (!isUsableRect(bbox)) return null;
48350
48369
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
48351
- const cropX = clamp01(bbox.x - bbox.w * pad);
48352
- const cropY = clamp01(bbox.y - bbox.h * pad);
48353
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
48354
- const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
48370
+ const cropX = clamp01$1(bbox.x - bbox.w * pad);
48371
+ const cropY = clamp01$1(bbox.y - bbox.h * pad);
48372
+ const cropRight = clamp01$1(bbox.x + bbox.w * (1 + pad));
48373
+ const cropBottom = clamp01$1(bbox.y + bbox.h * (1 + pad));
48355
48374
  const cropW = cropRight - cropX;
48356
48375
  const cropH = cropBottom - cropY;
48357
48376
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -52602,7 +52621,7 @@ function isSceneSizedBox(bbox, frameWidth, frameHeight) {
52602
52621
  const frame = frameWidth * frameHeight;
52603
52622
  return frame > 0 && bbox.w * bbox.h / frame >= .55;
52604
52623
  }
52605
- function intersectionArea$1(a, b) {
52624
+ function intersectionArea$2(a, b) {
52606
52625
  const ix1 = Math.max(a.x, b.x);
52607
52626
  const iy1 = Math.max(a.y, b.y);
52608
52627
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -52615,7 +52634,7 @@ function detectionSwallowsEntry(det, parked) {
52615
52634
  const detArea = det.w * det.h;
52616
52635
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
52617
52636
  if (!centroidInside(det, centroid(parked))) return false;
52618
- return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52637
+ return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52619
52638
  }
52620
52639
  function parkedEntryHoldsDetection(entry, detBbox, config) {
52621
52640
  const overlap = iou(entry.bbox, detBbox);
@@ -53447,7 +53466,7 @@ var VEHICLE_CLASSES = [
53447
53466
  "vehicle"
53448
53467
  ];
53449
53468
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
53450
- function intersectionArea(a, b) {
53469
+ function intersectionArea$1(a, b) {
53451
53470
  const left = Math.max(a.x, b.x);
53452
53471
  const right = Math.min(a.x + a.w, b.x + b.w);
53453
53472
  const top = Math.max(a.y, b.y);
@@ -53464,7 +53483,7 @@ function intersectionArea(a, b) {
53464
53483
  function containedFraction(person, vehicle) {
53465
53484
  const area = person.w * person.h;
53466
53485
  if (area <= 0) return 0;
53467
- return intersectionArea(person, vehicle) / area;
53486
+ return intersectionArea$1(person, vehicle) / area;
53468
53487
  }
53469
53488
  /**
53470
53489
  * Is this person box contained in any vehicle box in the same frame?
@@ -56363,9 +56382,11 @@ function isUniqueViolation(err) {
56363
56382
  var SummaryStore = class {
56364
56383
  store;
56365
56384
  logger;
56385
+ onEvict;
56366
56386
  constructor(deps) {
56367
56387
  this.store = deps.store;
56368
56388
  this.logger = deps.logger;
56389
+ this.onEvict = deps.onEvict;
56369
56390
  }
56370
56391
  static async declare(store) {
56371
56392
  await Promise.all([store.declareCollection.mutate({
@@ -56563,6 +56584,18 @@ var SummaryStore = class {
56563
56584
  bytes: 0
56564
56585
  }];
56565
56586
  }
56587
+ /** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
56588
+ async existingIds(ids) {
56589
+ const found = /* @__PURE__ */ new Set();
56590
+ await Promise.all(ids.map(async (id) => {
56591
+ const raw = await this.store.get.query({
56592
+ collection: SUMMARIES_COLLECTION,
56593
+ key: id
56594
+ });
56595
+ if (typeof raw === "object" && raw !== null) found.add(id);
56596
+ }));
56597
+ return found;
56598
+ }
56566
56599
  /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
56567
56600
  async evictBefore(deviceId, cutoffMs) {
56568
56601
  const rows = await this.store.query.query({
@@ -56575,6 +56608,7 @@ var SummaryStore = class {
56575
56608
  });
56576
56609
  if (rows.length === 0) return 0;
56577
56610
  const ids = rows.map((r) => r.id);
56611
+ await this.onEvict?.(ids);
56578
56612
  await this.store.deleteWhere.mutate({
56579
56613
  collection: SUMMARY_MEMBERS_COLLECTION,
56580
56614
  filter: { whereIn: { summaryId: ids } }
@@ -59097,6 +59131,10 @@ function resolveMediaOwner(row) {
59097
59131
  kind: "event",
59098
59132
  eventId: ownerId
59099
59133
  };
59134
+ case "summary": return {
59135
+ kind: "summary",
59136
+ summaryId: ownerId
59137
+ };
59100
59138
  case "face": return ownerId.startsWith("face-") ? {
59101
59139
  kind: "track",
59102
59140
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -59183,11 +59221,14 @@ var DEFAULT_ORPHAN_LIMITS = {
59183
59221
  function questionsFor(refs) {
59184
59222
  const trackIds = /* @__PURE__ */ new Set();
59185
59223
  const eventIds = /* @__PURE__ */ new Set();
59224
+ const summaryIds = /* @__PURE__ */ new Set();
59186
59225
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
59187
59226
  else if (ref.kind === "event") eventIds.add(ref.eventId);
59227
+ else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
59188
59228
  return {
59189
59229
  trackIds: [...trackIds],
59190
- eventIds: [...eventIds]
59230
+ eventIds: [...eventIds],
59231
+ summaryIds: [...summaryIds]
59191
59232
  };
59192
59233
  }
59193
59234
  /**
@@ -59219,12 +59260,14 @@ async function auditScope(scope, deps, limits, cursor) {
59219
59260
  break;
59220
59261
  }
59221
59262
  const refs = rows.map((row) => scope.resolve(row));
59222
- const { trackIds, eventIds } = questionsFor(refs);
59263
+ const { trackIds, eventIds, summaryIds } = questionsFor(refs);
59223
59264
  let liveTracks;
59224
59265
  let liveEvents;
59266
+ let liveSummaries;
59225
59267
  try {
59226
59268
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
59227
59269
  liveEvents = eventIds.length > 0 ? await deps.liveEventIds(eventIds) : /* @__PURE__ */ new Set();
59270
+ liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
59228
59271
  } catch (err) {
59229
59272
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
59230
59273
  collection: scope.collection,
@@ -59248,7 +59291,7 @@ async function auditScope(scope, deps, limits, cursor) {
59248
59291
  tally.unresolvable += 1;
59249
59292
  continue;
59250
59293
  }
59251
- if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : false) continue;
59294
+ if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
59252
59295
  const ts = num(row.data, scope.timestampField);
59253
59296
  if (ts !== null && ts > graceFloor) {
59254
59297
  tally.withinGrace += 1;
@@ -65200,6 +65243,196 @@ function rowFromMetadata(id, metadata) {
65200
65243
  };
65201
65244
  }
65202
65245
  //#endregion
65246
+ //#region src/pipeline-analytics/summary/summary-score.ts
65247
+ /** A member whose own box is covered this much by others is hidden. */
65248
+ var OCCLUDED_COVER_FRACTION = .5;
65249
+ /** A box covering this share of the frame scores full marks on area. */
65250
+ var FULL_AREA_SHARE = .25;
65251
+ var WEIGHT_CONFIDENCE = .3;
65252
+ var WEIGHT_AREA = .25;
65253
+ var WEIGHT_RECOGNITION = .25;
65254
+ var WEIGHT_PLACEMENT = .2;
65255
+ function intersectionArea(a, b) {
65256
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
65257
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
65258
+ return w > 0 && h > 0 ? w * h : 0;
65259
+ }
65260
+ /**
65261
+ * Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
65262
+ * capped at the own area: when two others overlap each other on top of this
65263
+ * box the sum over-counts, which only ever calls a member MORE hidden, never
65264
+ * less — the safe side for a shot meant to show everyone.
65265
+ */
65266
+ function coveredFraction(own, others) {
65267
+ const area = own.width * own.height;
65268
+ if (area <= 0) return 1;
65269
+ const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
65270
+ return Math.min(1, covered / area);
65271
+ }
65272
+ function clamp01(value) {
65273
+ return Math.max(0, Math.min(1, value));
65274
+ }
65275
+ function placement(box, frameWidth, frameHeight) {
65276
+ const cx = box.x + box.width / 2;
65277
+ const cy = box.y + box.height / 2;
65278
+ const dx = cx - frameWidth / 2;
65279
+ const dy = cy - frameHeight / 2;
65280
+ const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
65281
+ return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
65282
+ }
65283
+ function memberQuality(d, frameWidth, frameHeight) {
65284
+ const frameArea = frameWidth * frameHeight;
65285
+ const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
65286
+ return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
65287
+ }
65288
+ /** Score one frame for one summary. Pure; never throws on empty input. */
65289
+ function scoreSummaryFrame(input) {
65290
+ const present = input.detections.filter((d) => input.members.has(d.trackId));
65291
+ const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
65292
+ if (visible.length === 0) return {
65293
+ visible: 0,
65294
+ quality: 0
65295
+ };
65296
+ const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
65297
+ return {
65298
+ visible: visible.length,
65299
+ quality
65300
+ };
65301
+ }
65302
+ /**
65303
+ * Lexicographic: more visible members wins outright; only equal counts fall
65304
+ * through to quality. Positive when `a` is the better shot.
65305
+ */
65306
+ function compareSummaryScores(a, b) {
65307
+ if (a.visible !== b.visible) return a.visible - b.visible;
65308
+ return a.quality - b.quality;
65309
+ }
65310
+ /** A candidate replaces the best only when it is STRICTLY better — a tie keeps
65311
+ * the pixels already materialised (they cost a native round trip). */
65312
+ function isBetterSummaryScore(candidate, best) {
65313
+ if (candidate.visible === 0) return false;
65314
+ return best === null || compareSummaryScores(candidate, best) > 0;
65315
+ }
65316
+ //#endregion
65317
+ //#region src/pipeline-analytics/summary/group-shot.ts
65318
+ /**
65319
+ * The running argmax of one open summary's group shot.
65320
+ *
65321
+ * RAM only, on purpose: it decides WHEN pixels are worth a native round trip
65322
+ * (only when the maximum improves — D9/D18), it is not the record of the
65323
+ * shot. The record is the media row the caller writes under the summary's
65324
+ * id; after a restart an open summary simply starts its argmax again and
65325
+ * replaces the row the next time it beats what it can remember, which is
65326
+ * nothing. That is a weaker shot for one session, never a wrong one.
65327
+ */
65328
+ var GroupShotTracker = class {
65329
+ best = /* @__PURE__ */ new Map();
65330
+ /**
65331
+ * Offer a frame. Returns `true` when it beats the summary's best so far —
65332
+ * the caller's cue to materialise the pixels for THIS frame, now, while its
65333
+ * handle is still leased.
65334
+ */
65335
+ offer(summaryId, score, frame) {
65336
+ if (!isBetterSummaryScore(score, (this.best.get(summaryId) ?? null)?.score ?? null)) return false;
65337
+ this.best.set(summaryId, {
65338
+ score,
65339
+ frame
65340
+ });
65341
+ return true;
65342
+ }
65343
+ bestOf(summaryId) {
65344
+ return this.best.get(summaryId) ?? null;
65345
+ }
65346
+ /** A sealed or evicted summary takes no more frames; drop its state. */
65347
+ forget(summaryId) {
65348
+ this.best.delete(summaryId);
65349
+ }
65350
+ get size() {
65351
+ return this.best.size;
65352
+ }
65353
+ };
65354
+ //#endregion
65355
+ //#region src/pipeline-analytics/summary/group-shot-materialiser.ts
65356
+ async function persistSummaryGroupShot(deps, input) {
65357
+ const { deviceId, summaryId, frameHandle } = input;
65358
+ const drop = {
65359
+ tags: { deviceId },
65360
+ meta: {
65361
+ summaryId,
65362
+ nodeId: frameHandle.nodeId
65363
+ }
65364
+ };
65365
+ try {
65366
+ const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
65367
+ const native = result?.frame;
65368
+ if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
65369
+ deps.logger.warn("group shot native miss — the previous shot stands", {
65370
+ ...drop,
65371
+ meta: {
65372
+ ...drop.meta,
65373
+ shmId: frameHandle.shmId
65374
+ }
65375
+ });
65376
+ return { kind: "native-miss" };
65377
+ }
65378
+ if (!keyFrameAcceptsTier(result.tier)) {
65379
+ deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
65380
+ ...drop,
65381
+ meta: {
65382
+ ...drop.meta,
65383
+ shmId: frameHandle.shmId,
65384
+ tier: result.tier
65385
+ }
65386
+ });
65387
+ return { kind: "ram-tier" };
65388
+ }
65389
+ const { keyFrame, keyFrameSmall } = await deps.encodeVariants(native);
65390
+ const keyFrameKey = await deps.media.putReplacing({
65391
+ deviceId,
65392
+ ownerKind: "summary",
65393
+ ownerId: summaryId,
65394
+ kind: "keyFrame",
65395
+ timestamp: input.timestamp,
65396
+ data: keyFrame
65397
+ });
65398
+ try {
65399
+ await deps.media.putReplacing({
65400
+ deviceId,
65401
+ ownerKind: "summary",
65402
+ ownerId: summaryId,
65403
+ kind: "keyFrameSmall",
65404
+ timestamp: input.timestamp,
65405
+ data: keyFrameSmall
65406
+ });
65407
+ } catch (err) {
65408
+ deps.logger.warn("group shot web companion write failed — the native shot is stored", {
65409
+ ...drop,
65410
+ meta: {
65411
+ ...drop.meta,
65412
+ error: errMsg$1(err)
65413
+ }
65414
+ });
65415
+ }
65416
+ return {
65417
+ kind: "landed",
65418
+ keyFrameKey
65419
+ };
65420
+ } catch (err) {
65421
+ const error = errMsg$1(err);
65422
+ deps.logger.warn("group shot capture failed", {
65423
+ ...drop,
65424
+ meta: {
65425
+ ...drop.meta,
65426
+ error
65427
+ }
65428
+ });
65429
+ return {
65430
+ kind: "error",
65431
+ error
65432
+ };
65433
+ }
65434
+ }
65435
+ //#endregion
65203
65436
  //#region src/pipeline-analytics/summary/summary-sealer.ts
65204
65437
  function errMsg(err) {
65205
65438
  return err instanceof Error ? err.message : String(err);
@@ -65207,16 +65440,33 @@ function errMsg(err) {
65207
65440
  var SummarySealer = class {
65208
65441
  deps;
65209
65442
  sweepInFlight = false;
65443
+ /**
65444
+ * What the FRAME path may ask at frame-rate: the open summary of a camera
65445
+ * and the members it has seen THIS process open. Fed only by this class's
65446
+ * own writes, never the authority (D224): a fresh process answers `null`
65447
+ * until a track opens, and a summary open across a restart is scored on the
65448
+ * members that open after it — a weaker shot, never a wrong one.
65449
+ */
65450
+ openMirror = /* @__PURE__ */ new Map();
65210
65451
  constructor(deps) {
65211
65452
  this.deps = deps;
65212
65453
  }
65454
+ openFor(deviceId) {
65455
+ return this.openMirror.get(deviceId) ?? null;
65456
+ }
65213
65457
  async onTrackOpened(input) {
65214
65458
  try {
65215
- await this.deps.summaries.openOrAbsorb({
65459
+ const row = await this.deps.summaries.openOrAbsorb({
65216
65460
  deviceId: input.deviceId,
65217
65461
  trackId: input.trackId,
65218
65462
  at: this.deps.now()
65219
65463
  });
65464
+ const mirror = this.openMirror.get(input.deviceId);
65465
+ if (mirror !== void 0 && mirror.id === row.id) mirror.members.add(input.trackId);
65466
+ else this.openMirror.set(input.deviceId, {
65467
+ id: row.id,
65468
+ members: new Set([input.trackId])
65469
+ });
65220
65470
  } catch (err) {
65221
65471
  this.deps.logger.warn("summary: could not open or absorb for a track", {
65222
65472
  tags: { deviceId: input.deviceId },
@@ -65272,13 +65522,19 @@ var SummarySealer = class {
65272
65522
  if (!quiet) return;
65273
65523
  const sealedAt = this.deps.now();
65274
65524
  try {
65275
- if (await this.deps.summaries.seal(summary, sealedAt)) this.deps.logger.info("summary sealed", {
65276
- tags: { deviceId },
65277
- meta: {
65278
- summaryId,
65279
- sealedAt
65280
- }
65281
- });
65525
+ if (await this.deps.summaries.seal(summary, sealedAt)) {
65526
+ const mirror = this.openMirror.get(deviceId);
65527
+ const members = mirror?.id === summaryId ? mirror.members : /* @__PURE__ */ new Set();
65528
+ if (mirror?.id === summaryId) this.openMirror.delete(deviceId);
65529
+ this.deps.onSealed?.(summaryId, members);
65530
+ this.deps.logger.info("summary sealed", {
65531
+ tags: { deviceId },
65532
+ meta: {
65533
+ summaryId,
65534
+ sealedAt
65535
+ }
65536
+ });
65537
+ }
65282
65538
  } catch (err) {
65283
65539
  this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
65284
65540
  tags: { deviceId },
@@ -66607,6 +66863,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
66607
66863
  summaryStore = null;
66608
66864
  /** The ONLY writer of a summary's lifecycle (D359). */
66609
66865
  summarySealer = null;
66866
+ /** The running argmax of every open summary's group shot (D359 phase 3). RAM only. */
66867
+ groupShots = new GroupShotTracker();
66868
+ /**
66869
+ * Tracks a gallery match has named, for the group-shot score's recognition
66870
+ * term. "Recognised so far" rather than "at that instant": the match arrives
66871
+ * on the detail plane, after the frame; the frames that follow carry it.
66872
+ * Pruned when the track's summary seals, so it is bounded by open summaries.
66873
+ */
66874
+ recognizedTrackIds = /* @__PURE__ */ new Set();
66610
66875
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
66611
66876
  * so the DeviceStateChanged handler stays cheap. */
66612
66877
  linkedCamerasCache = null;
@@ -68109,14 +68374,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68109
68374
  });
68110
68375
  this.summaryStore = new SummaryStore({
68111
68376
  store: api.settingsStore,
68112
- logger: this.ctx.logger.child("summary")
68377
+ logger: this.ctx.logger.child("summary"),
68378
+ onEvict: async (ids) => {
68379
+ await this.mediaStore?.deleteForOwner("summary", ids);
68380
+ }
68113
68381
  });
68114
68382
  this.summarySealer = new SummarySealer({
68115
68383
  summaries: this.summaryStore,
68116
68384
  openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
68117
68385
  isCameraQuiet: async (deviceId) => (this.stationaryRegistry?.count(deviceId) ?? 0) === 0,
68118
68386
  now: () => Date.now(),
68119
- logger: this.ctx.logger.child("summary-sealer")
68387
+ logger: this.ctx.logger.child("summary-sealer"),
68388
+ onSealed: (summaryId, members) => {
68389
+ this.groupShots.forget(summaryId);
68390
+ for (const trackId of members) this.recognizedTrackIds.delete(trackId);
68391
+ }
68120
68392
  });
68121
68393
  this.packageDropDetector = new PackageDropDetector({
68122
68394
  events: eventStore,
@@ -68965,6 +69237,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
68965
69237
  this.eventMediaDataPlane = await this.ctx.dataPlane?.serve({
68966
69238
  prefix: "event-media",
68967
69239
  access: planeAccess("event-media"),
69240
+ share: ["events-view"],
68968
69241
  handler
68969
69242
  }) ?? null;
68970
69243
  this.eventMediaBaseUrl = this.eventMediaDataPlane !== null ? `/addon/${this.ctx.id}/event-media` : null;
@@ -70551,6 +70824,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70551
70824
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
70552
70825
  else plateCrops += 1;
70553
70826
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
70827
+ this.offerGroupShot(deviceId, result, frameHandle);
70554
70828
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
70555
70829
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
70556
70830
  const rasterFallbackCandidates = [];
@@ -71433,6 +71707,63 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71433
71707
  * is the scheduling (coalescing, priority, budget) and the bookkeeping the
71434
71708
  * retry gate reads.
71435
71709
  */
71710
+ /**
71711
+ * Offer one processed frame to the camera's OPEN summary (D359 phase 3).
71712
+ * Metadata only, at frame-rate: the frame is scored against the members the
71713
+ * sealer has mirrored, and the pixels are fetched — by handle, at the owning
71714
+ * node, as a native key frame — only when this frame beats the best so far.
71715
+ */
71716
+ offerGroupShot(deviceId, result, frameHandle) {
71717
+ const open = this.summarySealer?.openFor(deviceId);
71718
+ if (!open || frameHandle === void 0) return;
71719
+ const score = scoreSummaryFrame({
71720
+ members: open.members,
71721
+ frameWidth: result.frameWidth,
71722
+ frameHeight: result.frameHeight,
71723
+ detections: result.tracked.map((t) => ({
71724
+ trackId: t.trackId,
71725
+ bbox: {
71726
+ x: t.bbox.x,
71727
+ y: t.bbox.y,
71728
+ width: t.bbox.w,
71729
+ height: t.bbox.h
71730
+ },
71731
+ confidence: t.confidence,
71732
+ recognized: this.recognizedTrackIds.has(t.trackId)
71733
+ }))
71734
+ });
71735
+ if (!this.groupShots.offer(open.id, score, { timestamp: result.timestamp })) return;
71736
+ const getNative = this.getNativeKeyFrameRgb;
71737
+ const mediaStore = this.mediaStore;
71738
+ if (!getNative || !mediaStore) return;
71739
+ const summaryId = open.id;
71740
+ this.captureScheduler.request({
71741
+ deviceId,
71742
+ kind: "keyFrame",
71743
+ coalesceKey: `summary:${summaryId}:keyFrame`,
71744
+ exec: () => persistSummaryGroupShot({
71745
+ fetchNativeFullFrame: getNative,
71746
+ encodeVariants: encodeKeyFrameVariants,
71747
+ media: mediaStore,
71748
+ logger: this.ctx.logger
71749
+ }, {
71750
+ deviceId,
71751
+ summaryId,
71752
+ timestamp: result.timestamp,
71753
+ frameHandle,
71754
+ maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
71755
+ })
71756
+ }).then((outcome) => {
71757
+ if (outcome?.kind === "landed") this.ctx.logger.debug("summary group shot landed", {
71758
+ tags: { deviceId },
71759
+ meta: {
71760
+ summaryId,
71761
+ visible: score.visible,
71762
+ quality: Number(score.quality.toFixed(3))
71763
+ }
71764
+ });
71765
+ }).catch(() => {});
71766
+ }
71436
71767
  async persistKeyFrames(deviceId, timestamp, captures, frameHandle) {
71437
71768
  const getNative = this.getNativeKeyFrameRgb;
71438
71769
  const mediaStore = this.mediaStore;
@@ -71577,6 +71908,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71577
71908
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
71578
71909
  * fine-classification attribution. */
71579
71910
  promoteSemanticRecognition(input) {
71911
+ this.recognizedTrackIds.add(input.trackId);
71580
71912
  const ctx = this.ctxIfReady;
71581
71913
  if (ctx === null) return;
71582
71914
  promoteSemanticBestFrame({
@@ -73815,6 +74147,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73815
74147
  },
73816
74148
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
73817
74149
  liveEventIds: (ids) => eventStore.existingEventIds(ids),
74150
+ liveSummaryIds: (ids) => {
74151
+ const summaries = this.summaryStore;
74152
+ if (!summaries) throw new Error("summary store not built — probe unavailable");
74153
+ return summaries.existingIds(ids);
74154
+ },
73818
74155
  reclaim: async (collection, key) => {
73819
74156
  if (collection === "pipeline-analytics:media") {
73820
74157
  await mediaStore.deleteByKey(key);
@@ -75449,7 +75786,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75449
75786
  if (!file) return null;
75450
75787
  return {
75451
75788
  bytes: Buffer.from(file.base64, "base64"),
75452
- key: file.key
75789
+ key: file.key,
75790
+ deviceId: file.deviceId
75453
75791
  };
75454
75792
  }
75455
75793
  /**
@@ -75463,7 +75801,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75463
75801
  try {
75464
75802
  return {
75465
75803
  bytes: await makeSquareThumb(media.bytes, variant.size),
75466
- key: `${media.key}|t${variant.size}`
75804
+ key: `${media.key}|t${variant.size}`,
75805
+ deviceId: media.deviceId
75467
75806
  };
75468
75807
  } catch (err) {
75469
75808
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
@@ -75493,7 +75832,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75493
75832
  const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
75494
75833
  return file === null ? null : {
75495
75834
  bytes: Buffer.from(file.base64, "base64"),
75496
- key: file.key
75835
+ key: file.key,
75836
+ deviceId: file.deviceId
75497
75837
  };
75498
75838
  };
75499
75839
  const eventFiles = await listInfo("event", id);