@camstack/addon-post-analysis 1.2.182 → 1.2.183

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.
@@ -27835,6 +27835,7 @@ var StoragePressureTracker = class {
27835
27835
  var OWNER_KINDS = [
27836
27836
  "event",
27837
27837
  "track",
27838
+ "summary",
27838
27839
  "face",
27839
27840
  "identity",
27840
27841
  "plate",
@@ -28418,7 +28419,8 @@ var MediaStore = class {
28418
28419
  relativePath: path
28419
28420
  })).toString("base64"),
28420
28421
  sizeBytes,
28421
- timestamp
28422
+ timestamp,
28423
+ deviceId: Number(data["deviceId"])
28422
28424
  };
28423
28425
  } catch {
28424
28426
  return null;
@@ -30693,7 +30695,7 @@ async function mapWithConcurrency(items, limit, mapper) {
30693
30695
  * all-signals-maxed track scores exactly 1.0 (the clamp is then a no-op) and an
30694
30696
  * all-zero track scores 0.
30695
30697
  */
30696
- var WEIGHT_CONFIDENCE = .3;
30698
+ var WEIGHT_CONFIDENCE$1 = .3;
30697
30699
  var WEIGHT_CLASS = .2;
30698
30700
  var WEIGHT_DWELL = .2;
30699
30701
  var WEIGHT_PROXIMITY = .12;
@@ -30731,7 +30733,7 @@ var ANIMAL_CLASSES = new Set([
30731
30733
  "cat"
30732
30734
  ]);
30733
30735
  /** Clamp to [0,1]. */
30734
- function clamp01$1(x) {
30736
+ function clamp01$2(x) {
30735
30737
  if (Number.isNaN(x)) return 0;
30736
30738
  if (x < 0) return 0;
30737
30739
  if (x > 1) return 1;
@@ -30765,7 +30767,7 @@ function computeImportance(input) {
30765
30767
  const terms = [
30766
30768
  {
30767
30769
  reason: "confidence",
30768
- value: WEIGHT_CONFIDENCE * clamp01$1(input.peakConfidence)
30770
+ value: WEIGHT_CONFIDENCE$1 * clamp01$2(input.peakConfidence)
30769
30771
  },
30770
30772
  {
30771
30773
  reason: "class",
@@ -30773,15 +30775,15 @@ function computeImportance(input) {
30773
30775
  },
30774
30776
  {
30775
30777
  reason: "dwell",
30776
- value: WEIGHT_DWELL * clamp01$1(input.durationMs / DWELL_FULL_MS)
30778
+ value: WEIGHT_DWELL * clamp01$2(input.durationMs / DWELL_FULL_MS)
30777
30779
  },
30778
30780
  {
30779
30781
  reason: "proximity",
30780
- value: WEIGHT_PROXIMITY * clamp01$1(input.peakBboxAreaFrac / SIZE_FULL)
30782
+ value: WEIGHT_PROXIMITY * clamp01$2(input.peakBboxAreaFrac / SIZE_FULL)
30781
30783
  },
30782
30784
  {
30783
30785
  reason: "travel",
30784
- value: WEIGHT_TRAVEL * clamp01$1(input.totalDistance / 2)
30786
+ value: WEIGHT_TRAVEL * clamp01$2(input.totalDistance / 2)
30785
30787
  },
30786
30788
  {
30787
30789
  reason: "zone",
@@ -30800,11 +30802,11 @@ function computeImportance(input) {
30800
30802
  }
30801
30803
  const identified = input.identityLabel !== void 0 && input.identityLabel.length > 0;
30802
30804
  if (input.netDisplacementFrac !== void 0 && input.pathSpanFrac !== void 0 && input.netDisplacementFrac < .02 && input.pathSpanFrac < .04 && !identified) return {
30803
- importance: clamp01$1(sum * STATIC_IMPORTANCE_MULTIPLIER),
30805
+ importance: clamp01$2(sum * STATIC_IMPORTANCE_MULTIPLIER),
30804
30806
  reason: best.reason
30805
30807
  };
30806
30808
  return {
30807
- importance: clamp01$1(sum),
30809
+ importance: clamp01$2(sum),
30808
30810
  reason: best.reason
30809
30811
  };
30810
30812
  }
@@ -33504,7 +33506,7 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
33504
33506
  * it, so it can be loose enough to see the near misses.
33505
33507
  */
33506
33508
  var CO_MOVING_MIN_CONTAINMENT = .6;
33507
- function intersectionArea$3(a, b) {
33509
+ function intersectionArea$4(a, b) {
33508
33510
  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));
33509
33511
  }
33510
33512
  /**
@@ -33524,7 +33526,7 @@ function findCoMovingPairs(born, live, minContainment = CO_MOVING_MIN_CONTAINMEN
33524
33526
  if (o.className === b.className) continue;
33525
33527
  const areaO = o.bbox.w * o.bbox.h;
33526
33528
  if (areaO <= 0) continue;
33527
- const inter = intersectionArea$3(b.bbox, o.bbox);
33529
+ const inter = intersectionArea$4(b.bbox, o.bbox);
33528
33530
  if (inter <= 0) continue;
33529
33531
  const union = areaB + areaO - inter;
33530
33532
  const containment = inter / Math.min(areaB, areaO);
@@ -33956,7 +33958,7 @@ function pairRiders(detections) {
33956
33958
  * diagonal = 2.085) with the same ~10% headroom the old 1.35 gave it.
33957
33959
  */
33958
33960
  var GROWTH_CENTROID_FACTOR = 2.3;
33959
- function intersectionArea$2(a, b) {
33961
+ function intersectionArea$3(a, b) {
33960
33962
  const x1 = Math.max(a.x, b.x);
33961
33963
  const y1 = Math.max(a.y, b.y);
33962
33964
  const x2 = Math.min(a.x + a.w, b.x + b.w);
@@ -33994,7 +33996,7 @@ function growthAdjacentMatch(track, det, ctx) {
33994
33996
  const larger = Math.max(trackArea, detArea);
33995
33997
  const smaller = Math.min(trackArea, detArea);
33996
33998
  if (larger / smaller < 1.5) return false;
33997
- if (intersectionArea$2(track, det) / smaller >= .5) return false;
33999
+ if (intersectionArea$3(track, det) / smaller >= .5) return false;
33998
34000
  const tc = bboxCentroid(track);
33999
34001
  const dc = bboxCentroid(det);
34000
34002
  return Math.hypot(tc.x - dc.x, tc.y - dc.y) <= GROWTH_CENTROID_FACTOR * Math.hypot(track.w, track.h);
@@ -46804,6 +46806,15 @@ var CACHE_CONTROL = "public, max-age=31536000, immutable";
46804
46806
  var THUMB_DEFAULT_SIZE = 160;
46805
46807
  var THUMB_MIN_SIZE = 64;
46806
46808
  var THUMB_MAX_SIZE = 320;
46809
+ /** Set by the HUB for a `csv_*` share token admitted on this plane; never by a client. */
46810
+ var SHARE_DEVICES_HEADER = "x-camstack-share-devices";
46811
+ /** The device ids a share scope allows, or `null` when the request carries no scope. */
46812
+ function shareScopeDevices(headers) {
46813
+ const raw = headers[SHARE_DEVICES_HEADER];
46814
+ const value = Array.isArray(raw) ? raw.join(",") : raw;
46815
+ if (value === void 0) return null;
46816
+ return new Set(value.split(",").map((part) => Number.parseInt(part.trim(), 10)).filter((id) => Number.isInteger(id) && id >= 0));
46817
+ }
46807
46818
  /**
46808
46819
  * Parse the `?kind=…` query into a preferred stored media kind. Returns null
46809
46820
  * when unset. The value is a free-form kind token (e.g. `crop`); the resolver
@@ -46876,6 +46887,14 @@ function createEventMediaHandler(deps) {
46876
46887
  res.writeHead(404, { "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}` }).end();
46877
46888
  return;
46878
46889
  }
46890
+ const scope = shareScopeDevices(req.headers);
46891
+ if (scope !== null) {
46892
+ const owner = media.deviceId ?? null;
46893
+ if (owner === null || !scope.has(owner)) {
46894
+ res.writeHead(403).end();
46895
+ return;
46896
+ }
46897
+ }
46879
46898
  const etag = "\"" + media.key + "\"";
46880
46899
  const serverTiming = `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`;
46881
46900
  if (req.headers["if-none-match"] === etag) {
@@ -48346,7 +48365,7 @@ var RetrainAnnotationStore = class {
48346
48365
  };
48347
48366
  //#endregion
48348
48367
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
48349
- function clamp01(v) {
48368
+ function clamp01$1(v) {
48350
48369
  return v < 0 ? 0 : v > 1 ? 1 : v;
48351
48370
  }
48352
48371
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -48422,10 +48441,10 @@ function deriveZoneCropRect(bbox, zone) {
48422
48441
  function deriveSubjectCropRect(bbox, padding) {
48423
48442
  if (!isUsableRect(bbox)) return null;
48424
48443
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
48425
- const cropX = clamp01(bbox.x - bbox.w * pad);
48426
- const cropY = clamp01(bbox.y - bbox.h * pad);
48427
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
48428
- const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
48444
+ const cropX = clamp01$1(bbox.x - bbox.w * pad);
48445
+ const cropY = clamp01$1(bbox.y - bbox.h * pad);
48446
+ const cropRight = clamp01$1(bbox.x + bbox.w * (1 + pad));
48447
+ const cropBottom = clamp01$1(bbox.y + bbox.h * (1 + pad));
48429
48448
  const cropW = cropRight - cropX;
48430
48449
  const cropH = cropBottom - cropY;
48431
48450
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -52676,7 +52695,7 @@ function isSceneSizedBox(bbox, frameWidth, frameHeight) {
52676
52695
  const frame = frameWidth * frameHeight;
52677
52696
  return frame > 0 && bbox.w * bbox.h / frame >= .55;
52678
52697
  }
52679
- function intersectionArea$1(a, b) {
52698
+ function intersectionArea$2(a, b) {
52680
52699
  const ix1 = Math.max(a.x, b.x);
52681
52700
  const iy1 = Math.max(a.y, b.y);
52682
52701
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -52689,7 +52708,7 @@ function detectionSwallowsEntry(det, parked) {
52689
52708
  const detArea = det.w * det.h;
52690
52709
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
52691
52710
  if (!centroidInside(det, centroid(parked))) return false;
52692
- return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52711
+ return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52693
52712
  }
52694
52713
  function parkedEntryHoldsDetection(entry, detBbox, config) {
52695
52714
  const overlap = iou(entry.bbox, detBbox);
@@ -53521,7 +53540,7 @@ var VEHICLE_CLASSES = [
53521
53540
  "vehicle"
53522
53541
  ];
53523
53542
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
53524
- function intersectionArea(a, b) {
53543
+ function intersectionArea$1(a, b) {
53525
53544
  const left = Math.max(a.x, b.x);
53526
53545
  const right = Math.min(a.x + a.w, b.x + b.w);
53527
53546
  const top = Math.max(a.y, b.y);
@@ -53538,7 +53557,7 @@ function intersectionArea(a, b) {
53538
53557
  function containedFraction(person, vehicle) {
53539
53558
  const area = person.w * person.h;
53540
53559
  if (area <= 0) return 0;
53541
- return intersectionArea(person, vehicle) / area;
53560
+ return intersectionArea$1(person, vehicle) / area;
53542
53561
  }
53543
53562
  /**
53544
53563
  * Is this person box contained in any vehicle box in the same frame?
@@ -56437,9 +56456,11 @@ function isUniqueViolation(err) {
56437
56456
  var SummaryStore = class {
56438
56457
  store;
56439
56458
  logger;
56459
+ onEvict;
56440
56460
  constructor(deps) {
56441
56461
  this.store = deps.store;
56442
56462
  this.logger = deps.logger;
56463
+ this.onEvict = deps.onEvict;
56443
56464
  }
56444
56465
  static async declare(store) {
56445
56466
  await Promise.all([store.declareCollection.mutate({
@@ -56637,6 +56658,18 @@ var SummaryStore = class {
56637
56658
  bytes: 0
56638
56659
  }];
56639
56660
  }
56661
+ /** The subset of `ids` that still have a row — the orphan audit's probe. Throws on a failed read. */
56662
+ async existingIds(ids) {
56663
+ const found = /* @__PURE__ */ new Set();
56664
+ await Promise.all(ids.map(async (id) => {
56665
+ const raw = await this.store.get.query({
56666
+ collection: SUMMARIES_COLLECTION,
56667
+ key: id
56668
+ });
56669
+ if (typeof raw === "object" && raw !== null) found.add(id);
56670
+ }));
56671
+ return found;
56672
+ }
56640
56673
  /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
56641
56674
  async evictBefore(deviceId, cutoffMs) {
56642
56675
  const rows = await this.store.query.query({
@@ -56649,6 +56682,7 @@ var SummaryStore = class {
56649
56682
  });
56650
56683
  if (rows.length === 0) return 0;
56651
56684
  const ids = rows.map((r) => r.id);
56685
+ await this.onEvict?.(ids);
56652
56686
  await this.store.deleteWhere.mutate({
56653
56687
  collection: SUMMARY_MEMBERS_COLLECTION,
56654
56688
  filter: { whereIn: { summaryId: ids } }
@@ -59171,6 +59205,10 @@ function resolveMediaOwner(row) {
59171
59205
  kind: "event",
59172
59206
  eventId: ownerId
59173
59207
  };
59208
+ case "summary": return {
59209
+ kind: "summary",
59210
+ summaryId: ownerId
59211
+ };
59174
59212
  case "face": return ownerId.startsWith("face-") ? {
59175
59213
  kind: "track",
59176
59214
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -59257,11 +59295,14 @@ var DEFAULT_ORPHAN_LIMITS = {
59257
59295
  function questionsFor(refs) {
59258
59296
  const trackIds = /* @__PURE__ */ new Set();
59259
59297
  const eventIds = /* @__PURE__ */ new Set();
59298
+ const summaryIds = /* @__PURE__ */ new Set();
59260
59299
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
59261
59300
  else if (ref.kind === "event") eventIds.add(ref.eventId);
59301
+ else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
59262
59302
  return {
59263
59303
  trackIds: [...trackIds],
59264
- eventIds: [...eventIds]
59304
+ eventIds: [...eventIds],
59305
+ summaryIds: [...summaryIds]
59265
59306
  };
59266
59307
  }
59267
59308
  /**
@@ -59293,12 +59334,14 @@ async function auditScope(scope, deps, limits, cursor) {
59293
59334
  break;
59294
59335
  }
59295
59336
  const refs = rows.map((row) => scope.resolve(row));
59296
- const { trackIds, eventIds } = questionsFor(refs);
59337
+ const { trackIds, eventIds, summaryIds } = questionsFor(refs);
59297
59338
  let liveTracks;
59298
59339
  let liveEvents;
59340
+ let liveSummaries;
59299
59341
  try {
59300
59342
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
59301
59343
  liveEvents = eventIds.length > 0 ? await deps.liveEventIds(eventIds) : /* @__PURE__ */ new Set();
59344
+ liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
59302
59345
  } catch (err) {
59303
59346
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
59304
59347
  collection: scope.collection,
@@ -59322,7 +59365,7 @@ async function auditScope(scope, deps, limits, cursor) {
59322
59365
  tally.unresolvable += 1;
59323
59366
  continue;
59324
59367
  }
59325
- if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : false) continue;
59368
+ if (ref.kind === "track" ? liveTracks.has(ref.trackId) : ref.kind === "event" ? liveEvents.has(ref.eventId) : ref.kind === "summary" ? liveSummaries.has(ref.summaryId) : false) continue;
59326
59369
  const ts = num(row.data, scope.timestampField);
59327
59370
  if (ts !== null && ts > graceFloor) {
59328
59371
  tally.withinGrace += 1;
@@ -65274,6 +65317,196 @@ function rowFromMetadata(id, metadata) {
65274
65317
  };
65275
65318
  }
65276
65319
  //#endregion
65320
+ //#region src/pipeline-analytics/summary/summary-score.ts
65321
+ /** A member whose own box is covered this much by others is hidden. */
65322
+ var OCCLUDED_COVER_FRACTION = .5;
65323
+ /** A box covering this share of the frame scores full marks on area. */
65324
+ var FULL_AREA_SHARE = .25;
65325
+ var WEIGHT_CONFIDENCE = .3;
65326
+ var WEIGHT_AREA = .25;
65327
+ var WEIGHT_RECOGNITION = .25;
65328
+ var WEIGHT_PLACEMENT = .2;
65329
+ function intersectionArea(a, b) {
65330
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
65331
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
65332
+ return w > 0 && h > 0 ? w * h : 0;
65333
+ }
65334
+ /**
65335
+ * Share of `own` covered by `others`, 0..1. Pairwise intersections summed and
65336
+ * capped at the own area: when two others overlap each other on top of this
65337
+ * box the sum over-counts, which only ever calls a member MORE hidden, never
65338
+ * less — the safe side for a shot meant to show everyone.
65339
+ */
65340
+ function coveredFraction(own, others) {
65341
+ const area = own.width * own.height;
65342
+ if (area <= 0) return 1;
65343
+ const covered = others.reduce((sum, other) => sum + intersectionArea(own, other), 0);
65344
+ return Math.min(1, covered / area);
65345
+ }
65346
+ function clamp01(value) {
65347
+ return Math.max(0, Math.min(1, value));
65348
+ }
65349
+ function placement(box, frameWidth, frameHeight) {
65350
+ const cx = box.x + box.width / 2;
65351
+ const cy = box.y + box.height / 2;
65352
+ const dx = cx - frameWidth / 2;
65353
+ const dy = cy - frameHeight / 2;
65354
+ const halfDiagonal = Math.hypot(frameWidth, frameHeight) / 2;
65355
+ return halfDiagonal <= 0 ? 0 : 1 - clamp01(Math.hypot(dx, dy) / halfDiagonal);
65356
+ }
65357
+ function memberQuality(d, frameWidth, frameHeight) {
65358
+ const frameArea = frameWidth * frameHeight;
65359
+ const cappedArea = clamp01((frameArea <= 0 ? 0 : d.bbox.width * d.bbox.height / frameArea) / FULL_AREA_SHARE);
65360
+ return WEIGHT_CONFIDENCE * clamp01(d.confidence) + WEIGHT_AREA * cappedArea + WEIGHT_RECOGNITION * (d.recognized ? 1 : 0) + WEIGHT_PLACEMENT * placement(d.bbox, frameWidth, frameHeight);
65361
+ }
65362
+ /** Score one frame for one summary. Pure; never throws on empty input. */
65363
+ function scoreSummaryFrame(input) {
65364
+ const present = input.detections.filter((d) => input.members.has(d.trackId));
65365
+ const visible = present.filter((d) => coveredFraction(d.bbox, present.filter((o) => o !== d).map((o) => o.bbox)) < OCCLUDED_COVER_FRACTION);
65366
+ if (visible.length === 0) return {
65367
+ visible: 0,
65368
+ quality: 0
65369
+ };
65370
+ const quality = visible.reduce((sum, d) => sum + memberQuality(d, input.frameWidth, input.frameHeight), 0) / visible.length;
65371
+ return {
65372
+ visible: visible.length,
65373
+ quality
65374
+ };
65375
+ }
65376
+ /**
65377
+ * Lexicographic: more visible members wins outright; only equal counts fall
65378
+ * through to quality. Positive when `a` is the better shot.
65379
+ */
65380
+ function compareSummaryScores(a, b) {
65381
+ if (a.visible !== b.visible) return a.visible - b.visible;
65382
+ return a.quality - b.quality;
65383
+ }
65384
+ /** A candidate replaces the best only when it is STRICTLY better — a tie keeps
65385
+ * the pixels already materialised (they cost a native round trip). */
65386
+ function isBetterSummaryScore(candidate, best) {
65387
+ if (candidate.visible === 0) return false;
65388
+ return best === null || compareSummaryScores(candidate, best) > 0;
65389
+ }
65390
+ //#endregion
65391
+ //#region src/pipeline-analytics/summary/group-shot.ts
65392
+ /**
65393
+ * The running argmax of one open summary's group shot.
65394
+ *
65395
+ * RAM only, on purpose: it decides WHEN pixels are worth a native round trip
65396
+ * (only when the maximum improves — D9/D18), it is not the record of the
65397
+ * shot. The record is the media row the caller writes under the summary's
65398
+ * id; after a restart an open summary simply starts its argmax again and
65399
+ * replaces the row the next time it beats what it can remember, which is
65400
+ * nothing. That is a weaker shot for one session, never a wrong one.
65401
+ */
65402
+ var GroupShotTracker = class {
65403
+ best = /* @__PURE__ */ new Map();
65404
+ /**
65405
+ * Offer a frame. Returns `true` when it beats the summary's best so far —
65406
+ * the caller's cue to materialise the pixels for THIS frame, now, while its
65407
+ * handle is still leased.
65408
+ */
65409
+ offer(summaryId, score, frame) {
65410
+ if (!isBetterSummaryScore(score, (this.best.get(summaryId) ?? null)?.score ?? null)) return false;
65411
+ this.best.set(summaryId, {
65412
+ score,
65413
+ frame
65414
+ });
65415
+ return true;
65416
+ }
65417
+ bestOf(summaryId) {
65418
+ return this.best.get(summaryId) ?? null;
65419
+ }
65420
+ /** A sealed or evicted summary takes no more frames; drop its state. */
65421
+ forget(summaryId) {
65422
+ this.best.delete(summaryId);
65423
+ }
65424
+ get size() {
65425
+ return this.best.size;
65426
+ }
65427
+ };
65428
+ //#endregion
65429
+ //#region src/pipeline-analytics/summary/group-shot-materialiser.ts
65430
+ async function persistSummaryGroupShot(deps, input) {
65431
+ const { deviceId, summaryId, frameHandle } = input;
65432
+ const drop = {
65433
+ tags: { deviceId },
65434
+ meta: {
65435
+ summaryId,
65436
+ nodeId: frameHandle.nodeId
65437
+ }
65438
+ };
65439
+ try {
65440
+ const result = await deps.fetchNativeFullFrame(frameHandle, input.maxWidth);
65441
+ const native = result?.frame;
65442
+ if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
65443
+ deps.logger.warn("group shot native miss — the previous shot stands", {
65444
+ ...drop,
65445
+ meta: {
65446
+ ...drop.meta,
65447
+ shmId: frameHandle.shmId
65448
+ }
65449
+ });
65450
+ return { kind: "native-miss" };
65451
+ }
65452
+ if (!keyFrameAcceptsTier(result.tier)) {
65453
+ deps.logger.warn("group shot RAM tier rejected — the previous shot stands", {
65454
+ ...drop,
65455
+ meta: {
65456
+ ...drop.meta,
65457
+ shmId: frameHandle.shmId,
65458
+ tier: result.tier
65459
+ }
65460
+ });
65461
+ return { kind: "ram-tier" };
65462
+ }
65463
+ const { keyFrame, keyFrameSmall } = await deps.encodeVariants(native);
65464
+ const keyFrameKey = await deps.media.putReplacing({
65465
+ deviceId,
65466
+ ownerKind: "summary",
65467
+ ownerId: summaryId,
65468
+ kind: "keyFrame",
65469
+ timestamp: input.timestamp,
65470
+ data: keyFrame
65471
+ });
65472
+ try {
65473
+ await deps.media.putReplacing({
65474
+ deviceId,
65475
+ ownerKind: "summary",
65476
+ ownerId: summaryId,
65477
+ kind: "keyFrameSmall",
65478
+ timestamp: input.timestamp,
65479
+ data: keyFrameSmall
65480
+ });
65481
+ } catch (err) {
65482
+ deps.logger.warn("group shot web companion write failed — the native shot is stored", {
65483
+ ...drop,
65484
+ meta: {
65485
+ ...drop.meta,
65486
+ error: require_dist.errMsg(err)
65487
+ }
65488
+ });
65489
+ }
65490
+ return {
65491
+ kind: "landed",
65492
+ keyFrameKey
65493
+ };
65494
+ } catch (err) {
65495
+ const error = require_dist.errMsg(err);
65496
+ deps.logger.warn("group shot capture failed", {
65497
+ ...drop,
65498
+ meta: {
65499
+ ...drop.meta,
65500
+ error
65501
+ }
65502
+ });
65503
+ return {
65504
+ kind: "error",
65505
+ error
65506
+ };
65507
+ }
65508
+ }
65509
+ //#endregion
65277
65510
  //#region src/pipeline-analytics/summary/summary-sealer.ts
65278
65511
  function errMsg(err) {
65279
65512
  return err instanceof Error ? err.message : String(err);
@@ -65281,16 +65514,33 @@ function errMsg(err) {
65281
65514
  var SummarySealer = class {
65282
65515
  deps;
65283
65516
  sweepInFlight = false;
65517
+ /**
65518
+ * What the FRAME path may ask at frame-rate: the open summary of a camera
65519
+ * and the members it has seen THIS process open. Fed only by this class's
65520
+ * own writes, never the authority (D224): a fresh process answers `null`
65521
+ * until a track opens, and a summary open across a restart is scored on the
65522
+ * members that open after it — a weaker shot, never a wrong one.
65523
+ */
65524
+ openMirror = /* @__PURE__ */ new Map();
65284
65525
  constructor(deps) {
65285
65526
  this.deps = deps;
65286
65527
  }
65528
+ openFor(deviceId) {
65529
+ return this.openMirror.get(deviceId) ?? null;
65530
+ }
65287
65531
  async onTrackOpened(input) {
65288
65532
  try {
65289
- await this.deps.summaries.openOrAbsorb({
65533
+ const row = await this.deps.summaries.openOrAbsorb({
65290
65534
  deviceId: input.deviceId,
65291
65535
  trackId: input.trackId,
65292
65536
  at: this.deps.now()
65293
65537
  });
65538
+ const mirror = this.openMirror.get(input.deviceId);
65539
+ if (mirror !== void 0 && mirror.id === row.id) mirror.members.add(input.trackId);
65540
+ else this.openMirror.set(input.deviceId, {
65541
+ id: row.id,
65542
+ members: new Set([input.trackId])
65543
+ });
65294
65544
  } catch (err) {
65295
65545
  this.deps.logger.warn("summary: could not open or absorb for a track", {
65296
65546
  tags: { deviceId: input.deviceId },
@@ -65346,13 +65596,19 @@ var SummarySealer = class {
65346
65596
  if (!quiet) return;
65347
65597
  const sealedAt = this.deps.now();
65348
65598
  try {
65349
- if (await this.deps.summaries.seal(summary, sealedAt)) this.deps.logger.info("summary sealed", {
65350
- tags: { deviceId },
65351
- meta: {
65352
- summaryId,
65353
- sealedAt
65354
- }
65355
- });
65599
+ if (await this.deps.summaries.seal(summary, sealedAt)) {
65600
+ const mirror = this.openMirror.get(deviceId);
65601
+ const members = mirror?.id === summaryId ? mirror.members : /* @__PURE__ */ new Set();
65602
+ if (mirror?.id === summaryId) this.openMirror.delete(deviceId);
65603
+ this.deps.onSealed?.(summaryId, members);
65604
+ this.deps.logger.info("summary sealed", {
65605
+ tags: { deviceId },
65606
+ meta: {
65607
+ summaryId,
65608
+ sealedAt
65609
+ }
65610
+ });
65611
+ }
65356
65612
  } catch (err) {
65357
65613
  this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
65358
65614
  tags: { deviceId },
@@ -66681,6 +66937,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
66681
66937
  summaryStore = null;
66682
66938
  /** The ONLY writer of a summary's lifecycle (D359). */
66683
66939
  summarySealer = null;
66940
+ /** The running argmax of every open summary's group shot (D359 phase 3). RAM only. */
66941
+ groupShots = new GroupShotTracker();
66942
+ /**
66943
+ * Tracks a gallery match has named, for the group-shot score's recognition
66944
+ * term. "Recognised so far" rather than "at that instant": the match arrives
66945
+ * on the detail plane, after the frame; the frames that follow carry it.
66946
+ * Pruned when the track's summary seals, so it is bounded by open summaries.
66947
+ */
66948
+ recognizedTrackIds = /* @__PURE__ */ new Set();
66684
66949
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
66685
66950
  * so the DeviceStateChanged handler stays cheap. */
66686
66951
  linkedCamerasCache = null;
@@ -68183,14 +68448,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68183
68448
  });
68184
68449
  this.summaryStore = new SummaryStore({
68185
68450
  store: api.settingsStore,
68186
- logger: this.ctx.logger.child("summary")
68451
+ logger: this.ctx.logger.child("summary"),
68452
+ onEvict: async (ids) => {
68453
+ await this.mediaStore?.deleteForOwner("summary", ids);
68454
+ }
68187
68455
  });
68188
68456
  this.summarySealer = new SummarySealer({
68189
68457
  summaries: this.summaryStore,
68190
68458
  openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
68191
68459
  isCameraQuiet: async (deviceId) => (this.stationaryRegistry?.count(deviceId) ?? 0) === 0,
68192
68460
  now: () => Date.now(),
68193
- logger: this.ctx.logger.child("summary-sealer")
68461
+ logger: this.ctx.logger.child("summary-sealer"),
68462
+ onSealed: (summaryId, members) => {
68463
+ this.groupShots.forget(summaryId);
68464
+ for (const trackId of members) this.recognizedTrackIds.delete(trackId);
68465
+ }
68194
68466
  });
68195
68467
  this.packageDropDetector = new PackageDropDetector({
68196
68468
  events: eventStore,
@@ -69039,6 +69311,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69039
69311
  this.eventMediaDataPlane = await this.ctx.dataPlane?.serve({
69040
69312
  prefix: "event-media",
69041
69313
  access: planeAccess("event-media"),
69314
+ share: ["events-view"],
69042
69315
  handler
69043
69316
  }) ?? null;
69044
69317
  this.eventMediaBaseUrl = this.eventMediaDataPlane !== null ? `/addon/${this.ctx.id}/event-media` : null;
@@ -70625,6 +70898,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70625
70898
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
70626
70899
  else plateCrops += 1;
70627
70900
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
70901
+ this.offerGroupShot(deviceId, result, frameHandle);
70628
70902
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
70629
70903
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
70630
70904
  const rasterFallbackCandidates = [];
@@ -71507,6 +71781,63 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71507
71781
  * is the scheduling (coalescing, priority, budget) and the bookkeeping the
71508
71782
  * retry gate reads.
71509
71783
  */
71784
+ /**
71785
+ * Offer one processed frame to the camera's OPEN summary (D359 phase 3).
71786
+ * Metadata only, at frame-rate: the frame is scored against the members the
71787
+ * sealer has mirrored, and the pixels are fetched — by handle, at the owning
71788
+ * node, as a native key frame — only when this frame beats the best so far.
71789
+ */
71790
+ offerGroupShot(deviceId, result, frameHandle) {
71791
+ const open = this.summarySealer?.openFor(deviceId);
71792
+ if (!open || frameHandle === void 0) return;
71793
+ const score = scoreSummaryFrame({
71794
+ members: open.members,
71795
+ frameWidth: result.frameWidth,
71796
+ frameHeight: result.frameHeight,
71797
+ detections: result.tracked.map((t) => ({
71798
+ trackId: t.trackId,
71799
+ bbox: {
71800
+ x: t.bbox.x,
71801
+ y: t.bbox.y,
71802
+ width: t.bbox.w,
71803
+ height: t.bbox.h
71804
+ },
71805
+ confidence: t.confidence,
71806
+ recognized: this.recognizedTrackIds.has(t.trackId)
71807
+ }))
71808
+ });
71809
+ if (!this.groupShots.offer(open.id, score, { timestamp: result.timestamp })) return;
71810
+ const getNative = this.getNativeKeyFrameRgb;
71811
+ const mediaStore = this.mediaStore;
71812
+ if (!getNative || !mediaStore) return;
71813
+ const summaryId = open.id;
71814
+ this.captureScheduler.request({
71815
+ deviceId,
71816
+ kind: "keyFrame",
71817
+ coalesceKey: `summary:${summaryId}:keyFrame`,
71818
+ exec: () => persistSummaryGroupShot({
71819
+ fetchNativeFullFrame: getNative,
71820
+ encodeVariants: encodeKeyFrameVariants,
71821
+ media: mediaStore,
71822
+ logger: this.ctx.logger
71823
+ }, {
71824
+ deviceId,
71825
+ summaryId,
71826
+ timestamp: result.timestamp,
71827
+ frameHandle,
71828
+ maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
71829
+ })
71830
+ }).then((outcome) => {
71831
+ if (outcome?.kind === "landed") this.ctx.logger.debug("summary group shot landed", {
71832
+ tags: { deviceId },
71833
+ meta: {
71834
+ summaryId,
71835
+ visible: score.visible,
71836
+ quality: Number(score.quality.toFixed(3))
71837
+ }
71838
+ });
71839
+ }).catch(() => {});
71840
+ }
71510
71841
  async persistKeyFrames(deviceId, timestamp, captures, frameHandle) {
71511
71842
  const getNative = this.getNativeKeyFrameRgb;
71512
71843
  const mediaStore = this.mediaStore;
@@ -71651,6 +71982,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71651
71982
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
71652
71983
  * fine-classification attribution. */
71653
71984
  promoteSemanticRecognition(input) {
71985
+ this.recognizedTrackIds.add(input.trackId);
71654
71986
  const ctx = this.ctxIfReady;
71655
71987
  if (ctx === null) return;
71656
71988
  promoteSemanticBestFrame({
@@ -73889,6 +74221,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73889
74221
  },
73890
74222
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
73891
74223
  liveEventIds: (ids) => eventStore.existingEventIds(ids),
74224
+ liveSummaryIds: (ids) => {
74225
+ const summaries = this.summaryStore;
74226
+ if (!summaries) throw new Error("summary store not built — probe unavailable");
74227
+ return summaries.existingIds(ids);
74228
+ },
73892
74229
  reclaim: async (collection, key) => {
73893
74230
  if (collection === "pipeline-analytics:media") {
73894
74231
  await mediaStore.deleteByKey(key);
@@ -75523,7 +75860,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75523
75860
  if (!file) return null;
75524
75861
  return {
75525
75862
  bytes: Buffer.from(file.base64, "base64"),
75526
- key: file.key
75863
+ key: file.key,
75864
+ deviceId: file.deviceId
75527
75865
  };
75528
75866
  }
75529
75867
  /**
@@ -75537,7 +75875,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75537
75875
  try {
75538
75876
  return {
75539
75877
  bytes: await makeSquareThumb(media.bytes, variant.size),
75540
- key: `${media.key}|t${variant.size}`
75878
+ key: `${media.key}|t${variant.size}`,
75879
+ deviceId: media.deviceId
75541
75880
  };
75542
75881
  } catch (err) {
75543
75882
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
@@ -75567,7 +75906,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75567
75906
  const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
75568
75907
  return file === null ? null : {
75569
75908
  bytes: Buffer.from(file.base64, "base64"),
75570
- key: file.key
75909
+ key: file.key,
75910
+ deviceId: file.deviceId
75571
75911
  };
75572
75912
  };
75573
75913
  const eventFiles = await listInfo("event", id);