@camstack/addon-post-analysis 1.2.181 → 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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BB70nZUm.js");
5
+ const require_dist = require("../dist-_fedHYU3.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -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);
@@ -35980,6 +35982,7 @@ var EMPTY_CASCADE_TALLY = {
35980
35982
  faces: 0,
35981
35983
  plates: 0,
35982
35984
  embeddings: 0,
35985
+ summaries: 0,
35983
35986
  bytesReclaimed: 0
35984
35987
  };
35985
35988
  /** Fold a leaf store's receipts into a running tally. */
@@ -36023,6 +36026,13 @@ function addLeafCounts(tally, counts) {
36023
36026
  bytesReclaimed
36024
36027
  };
36025
36028
  break;
36029
+ case "summaries":
36030
+ next = {
36031
+ ...next,
36032
+ summaries: next.summaries + c.rows,
36033
+ bytesReclaimed
36034
+ };
36035
+ break;
36026
36036
  }
36027
36037
  }
36028
36038
  return next;
@@ -36036,6 +36046,7 @@ function mergeCascadeTallies(a, b) {
36036
36046
  faces: a.faces + b.faces,
36037
36047
  plates: a.plates + b.plates,
36038
36048
  embeddings: a.embeddings + b.embeddings,
36049
+ summaries: a.summaries + b.summaries,
36039
36050
  bytesReclaimed: a.bytesReclaimed + b.bytesReclaimed
36040
36051
  };
36041
36052
  }
@@ -36098,6 +36109,7 @@ function absentCascadeLegs(stores) {
36098
36109
  if (!stores.faceStore) absent.push("faces");
36099
36110
  if (!stores.plateStore) absent.push("plates");
36100
36111
  if (!stores.objectEmbeddingStore) absent.push("embeddings");
36112
+ if (!stores.summaryStore) absent.push("summaries");
36101
36113
  return absent;
36102
36114
  }
36103
36115
  function buildAnalyticsCascadeRegistry(stores) {
@@ -36105,6 +36117,7 @@ function buildAnalyticsCascadeRegistry(stores) {
36105
36117
  if (stores.faceStore) registry.push(stores.faceStore);
36106
36118
  if (stores.plateStore) registry.push(stores.plateStore);
36107
36119
  if (stores.objectEmbeddingStore) registry.push(stores.objectEmbeddingStore);
36120
+ if (stores.summaryStore) registry.push(stores.summaryStore);
36108
36121
  return registry;
36109
36122
  }
36110
36123
  /**
@@ -46793,6 +46806,15 @@ var CACHE_CONTROL = "public, max-age=31536000, immutable";
46793
46806
  var THUMB_DEFAULT_SIZE = 160;
46794
46807
  var THUMB_MIN_SIZE = 64;
46795
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
+ }
46796
46818
  /**
46797
46819
  * Parse the `?kind=…` query into a preferred stored media kind. Returns null
46798
46820
  * when unset. The value is a free-form kind token (e.g. `crop`); the resolver
@@ -46865,6 +46887,14 @@ function createEventMediaHandler(deps) {
46865
46887
  res.writeHead(404, { "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}` }).end();
46866
46888
  return;
46867
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
+ }
46868
46898
  const etag = "\"" + media.key + "\"";
46869
46899
  const serverTiming = `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`;
46870
46900
  if (req.headers["if-none-match"] === etag) {
@@ -48335,7 +48365,7 @@ var RetrainAnnotationStore = class {
48335
48365
  };
48336
48366
  //#endregion
48337
48367
  //#region src/pipeline-analytics/retrain/retrain-geometry.ts
48338
- function clamp01(v) {
48368
+ function clamp01$1(v) {
48339
48369
  return v < 0 ? 0 : v > 1 ? 1 : v;
48340
48370
  }
48341
48371
  /** Is this a usable rectangle at all? A zero-area box is not a subject. */
@@ -48411,10 +48441,10 @@ function deriveZoneCropRect(bbox, zone) {
48411
48441
  function deriveSubjectCropRect(bbox, padding) {
48412
48442
  if (!isUsableRect(bbox)) return null;
48413
48443
  const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
48414
- const cropX = clamp01(bbox.x - bbox.w * pad);
48415
- const cropY = clamp01(bbox.y - bbox.h * pad);
48416
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
48417
- 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));
48418
48448
  const cropW = cropRight - cropX;
48419
48449
  const cropH = cropBottom - cropY;
48420
48450
  if (!(cropW > 0) || !(cropH > 0)) return null;
@@ -52665,7 +52695,7 @@ function isSceneSizedBox(bbox, frameWidth, frameHeight) {
52665
52695
  const frame = frameWidth * frameHeight;
52666
52696
  return frame > 0 && bbox.w * bbox.h / frame >= .55;
52667
52697
  }
52668
- function intersectionArea$1(a, b) {
52698
+ function intersectionArea$2(a, b) {
52669
52699
  const ix1 = Math.max(a.x, b.x);
52670
52700
  const iy1 = Math.max(a.y, b.y);
52671
52701
  const ix2 = Math.min(a.x + a.w, b.x + b.w);
@@ -52678,7 +52708,7 @@ function detectionSwallowsEntry(det, parked) {
52678
52708
  const detArea = det.w * det.h;
52679
52709
  if (entryArea <= 0 || detArea < entryArea * 1.35) return false;
52680
52710
  if (!centroidInside(det, centroid(parked))) return false;
52681
- return intersectionArea$1(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52711
+ return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
52682
52712
  }
52683
52713
  function parkedEntryHoldsDetection(entry, detBbox, config) {
52684
52714
  const overlap = iou(entry.bbox, detBbox);
@@ -53510,7 +53540,7 @@ var VEHICLE_CLASSES = [
53510
53540
  "vehicle"
53511
53541
  ];
53512
53542
  /** Area of the intersection of two boxes. `0` when they do not overlap. */
53513
- function intersectionArea(a, b) {
53543
+ function intersectionArea$1(a, b) {
53514
53544
  const left = Math.max(a.x, b.x);
53515
53545
  const right = Math.min(a.x + a.w, b.x + b.w);
53516
53546
  const top = Math.max(a.y, b.y);
@@ -53527,7 +53557,7 @@ function intersectionArea(a, b) {
53527
53557
  function containedFraction(person, vehicle) {
53528
53558
  const area = person.w * person.h;
53529
53559
  if (area <= 0) return 0;
53530
- return intersectionArea(person, vehicle) / area;
53560
+ return intersectionArea$1(person, vehicle) / area;
53531
53561
  }
53532
53562
  /**
53533
53563
  * Is this person box contained in any vehicle box in the same frame?
@@ -56314,6 +56344,359 @@ async function reconcileGallerySamples(deps, options) {
56314
56344
  stoppedByCeiling
56315
56345
  };
56316
56346
  }
56347
+ //#endregion
56348
+ //#region src/pipeline-analytics/summary/summary-store.ts
56349
+ var SUMMARIES_COLLECTION = "pipeline-analytics:summaries";
56350
+ var SUMMARY_MEMBERS_COLLECTION = "pipeline-analytics:summary-members";
56351
+ var SUMMARIES_COLUMNS = [
56352
+ {
56353
+ name: "id",
56354
+ type: "TEXT",
56355
+ primaryKey: true,
56356
+ notNull: true
56357
+ },
56358
+ {
56359
+ name: "deviceId",
56360
+ type: "INTEGER",
56361
+ notNull: true
56362
+ },
56363
+ {
56364
+ name: "openedAt",
56365
+ type: "INTEGER",
56366
+ notNull: true
56367
+ },
56368
+ {
56369
+ name: "lastActiveAt",
56370
+ type: "INTEGER",
56371
+ notNull: true
56372
+ },
56373
+ {
56374
+ name: "sealedAt",
56375
+ type: "INTEGER"
56376
+ },
56377
+ (
56378
+ /** `deviceId` while open, NULL once sealed — the unique "one open per camera". */
56379
+ {
56380
+ name: "openDeviceId",
56381
+ type: "INTEGER"
56382
+ }),
56383
+ {
56384
+ name: "memberCount",
56385
+ type: "INTEGER",
56386
+ notNull: true
56387
+ }
56388
+ ];
56389
+ var SUMMARIES_INDEXES = [{
56390
+ name: "summaries_open_device",
56391
+ columns: ["openDeviceId"],
56392
+ unique: true
56393
+ }, {
56394
+ name: "summaries_device_opened",
56395
+ columns: ["deviceId", "openedAt"]
56396
+ }];
56397
+ var SUMMARY_MEMBERS_COLUMNS = [
56398
+ {
56399
+ name: "id",
56400
+ type: "TEXT",
56401
+ primaryKey: true,
56402
+ notNull: true
56403
+ },
56404
+ {
56405
+ name: "summaryId",
56406
+ type: "TEXT",
56407
+ notNull: true
56408
+ },
56409
+ {
56410
+ name: "trackId",
56411
+ type: "TEXT",
56412
+ notNull: true
56413
+ },
56414
+ {
56415
+ name: "deviceId",
56416
+ type: "INTEGER",
56417
+ notNull: true
56418
+ },
56419
+ (
56420
+ /** When the track joined — named `timestamp` so the orphan audit's declared-owner scope reads it. */
56421
+ {
56422
+ name: "timestamp",
56423
+ type: "INTEGER",
56424
+ notNull: true
56425
+ })
56426
+ ];
56427
+ var SUMMARY_MEMBERS_INDEXES = [{
56428
+ name: "summary_members_summary",
56429
+ columns: ["summaryId"]
56430
+ }, {
56431
+ name: "summary_members_track",
56432
+ columns: ["trackId"]
56433
+ }];
56434
+ function summaryId(deviceId, openedAt) {
56435
+ return `sum-${deviceId}-${openedAt}`;
56436
+ }
56437
+ function memberRowId(summaryIdValue, trackId) {
56438
+ return `${summaryIdValue}:${trackId}`;
56439
+ }
56440
+ function num$1(value, fallback = 0) {
56441
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
56442
+ }
56443
+ function toRow$1(id, data) {
56444
+ return {
56445
+ id,
56446
+ deviceId: num$1(data["deviceId"]),
56447
+ openedAt: num$1(data["openedAt"]),
56448
+ lastActiveAt: num$1(data["lastActiveAt"]),
56449
+ sealedAt: typeof data["sealedAt"] === "number" ? data["sealedAt"] : null,
56450
+ memberCount: num$1(data["memberCount"])
56451
+ };
56452
+ }
56453
+ function isUniqueViolation(err) {
56454
+ return err instanceof Error && /UNIQUE constraint failed/i.test(err.message);
56455
+ }
56456
+ var SummaryStore = class {
56457
+ store;
56458
+ logger;
56459
+ onEvict;
56460
+ constructor(deps) {
56461
+ this.store = deps.store;
56462
+ this.logger = deps.logger;
56463
+ this.onEvict = deps.onEvict;
56464
+ }
56465
+ static async declare(store) {
56466
+ await Promise.all([store.declareCollection.mutate({
56467
+ collection: SUMMARIES_COLLECTION,
56468
+ columns: [...SUMMARIES_COLUMNS],
56469
+ indexes: [...SUMMARIES_INDEXES]
56470
+ }), store.declareCollection.mutate({
56471
+ collection: SUMMARY_MEMBERS_COLLECTION,
56472
+ columns: [...SUMMARY_MEMBERS_COLUMNS],
56473
+ indexes: [...SUMMARY_MEMBERS_INDEXES]
56474
+ })]);
56475
+ }
56476
+ /** The camera's OPEN summary, from the row — `null` when none. */
56477
+ async findOpen(deviceId) {
56478
+ const first = (await this.store.query.query({
56479
+ collection: SUMMARIES_COLLECTION,
56480
+ filter: {
56481
+ where: { openDeviceId: deviceId },
56482
+ limit: 1
56483
+ }
56484
+ }))[0];
56485
+ return first === void 0 ? null : toRow$1(first.id, first.data);
56486
+ }
56487
+ async listOpen() {
56488
+ return (await this.store.query.query({
56489
+ collection: SUMMARIES_COLLECTION,
56490
+ filter: {
56491
+ where: { sealedAt: null },
56492
+ limit: 1e4
56493
+ }
56494
+ })).filter((r) => r.data["openDeviceId"] !== null).map((r) => toRow$1(r.id, r.data));
56495
+ }
56496
+ async membersOf(summaryIdValue) {
56497
+ return (await this.store.query.query({
56498
+ collection: SUMMARY_MEMBERS_COLLECTION,
56499
+ filter: {
56500
+ where: { summaryId: summaryIdValue },
56501
+ limit: 1e4
56502
+ }
56503
+ })).map((r) => ({
56504
+ summaryId: summaryIdValue,
56505
+ trackId: String(r.data["trackId"] ?? ""),
56506
+ deviceId: num$1(r.data["deviceId"]),
56507
+ joinedAt: num$1(r.data["timestamp"])
56508
+ }));
56509
+ }
56510
+ /**
56511
+ * Put a track into the camera's open summary, opening one when there is
56512
+ * none. Racing opens are settled by the store: the loser's insert hits the
56513
+ * unique `openDeviceId` and is re-read as an absorb.
56514
+ */
56515
+ async openOrAbsorb(input) {
56516
+ const existing = await this.findOpen(input.deviceId);
56517
+ if (existing !== null) return this.absorb(existing, input);
56518
+ const id = summaryId(input.deviceId, input.at);
56519
+ try {
56520
+ await this.store.set.mutate({
56521
+ collection: SUMMARIES_COLLECTION,
56522
+ key: id,
56523
+ value: {
56524
+ deviceId: input.deviceId,
56525
+ openedAt: input.at,
56526
+ lastActiveAt: input.at,
56527
+ sealedAt: null,
56528
+ openDeviceId: input.deviceId,
56529
+ memberCount: 1
56530
+ }
56531
+ });
56532
+ } catch (err) {
56533
+ if (!isUniqueViolation(err)) throw err;
56534
+ const winner = await this.findOpen(input.deviceId);
56535
+ if (winner === null) throw err;
56536
+ return this.absorb(winner, input);
56537
+ }
56538
+ await this.addMember(id, input);
56539
+ return {
56540
+ id,
56541
+ deviceId: input.deviceId,
56542
+ openedAt: input.at,
56543
+ lastActiveAt: input.at,
56544
+ sealedAt: null,
56545
+ memberCount: 1
56546
+ };
56547
+ }
56548
+ async absorb(summary, input) {
56549
+ const members = await this.membersOf(summary.id);
56550
+ if (members.some((m) => m.trackId === input.trackId)) return summary;
56551
+ await this.addMember(summary.id, input);
56552
+ const memberCount = members.length + 1;
56553
+ const lastActiveAt = Math.max(summary.lastActiveAt, input.at);
56554
+ await this.store.updateWhere.mutate({
56555
+ collection: SUMMARIES_COLLECTION,
56556
+ filter: { where: {
56557
+ openDeviceId: summary.deviceId,
56558
+ openedAt: summary.openedAt
56559
+ } },
56560
+ data: {
56561
+ memberCount,
56562
+ lastActiveAt
56563
+ }
56564
+ });
56565
+ return {
56566
+ ...summary,
56567
+ memberCount,
56568
+ lastActiveAt
56569
+ };
56570
+ }
56571
+ async addMember(summaryIdValue, input) {
56572
+ await this.store.insertMany.mutate({
56573
+ collection: SUMMARY_MEMBERS_COLLECTION,
56574
+ records: [{
56575
+ id: memberRowId(summaryIdValue, input.trackId),
56576
+ data: {
56577
+ summaryId: summaryIdValue,
56578
+ trackId: input.trackId,
56579
+ deviceId: input.deviceId,
56580
+ timestamp: input.at
56581
+ }
56582
+ }]
56583
+ });
56584
+ }
56585
+ /**
56586
+ * Seal in place. `true` when THIS call sealed it; `false` when the row was
56587
+ * already sealed (or gone) — the second sealer learns, it does not write.
56588
+ */
56589
+ async seal(summary, sealedAt) {
56590
+ const { updated } = await this.store.updateWhere.mutate({
56591
+ collection: SUMMARIES_COLLECTION,
56592
+ filter: { where: {
56593
+ openDeviceId: summary.deviceId,
56594
+ openedAt: summary.openedAt
56595
+ } },
56596
+ data: {
56597
+ sealedAt,
56598
+ openDeviceId: null
56599
+ }
56600
+ });
56601
+ if (updated > 1) this.logger.error("summary seal touched more than one row — the open key is not unique", {
56602
+ tags: { deviceId: summary.deviceId },
56603
+ meta: {
56604
+ summaryId: summary.id,
56605
+ updated
56606
+ }
56607
+ });
56608
+ return updated >= 1;
56609
+ }
56610
+ /**
56611
+ * Track cascade leaf: a member row whose track is gone is gone too, and
56612
+ * the summary's count follows. The envelope itself outlives its members
56613
+ * — it is an age-keyed root (`RETENTION_ROOTS`, `summary`).
56614
+ */
56615
+ async deleteByTracks(trackIds) {
56616
+ if (trackIds.length === 0) return [{
56617
+ family: "summaries",
56618
+ rows: 0,
56619
+ bytes: 0
56620
+ }];
56621
+ const members = await this.store.query.query({
56622
+ collection: SUMMARY_MEMBERS_COLLECTION,
56623
+ filter: {
56624
+ whereIn: { trackId: [...trackIds] },
56625
+ limit: 1e4
56626
+ }
56627
+ });
56628
+ if (members.length === 0) return [{
56629
+ family: "summaries",
56630
+ rows: 0,
56631
+ bytes: 0
56632
+ }];
56633
+ const { deleted } = await this.store.deleteWhere.mutate({
56634
+ collection: SUMMARY_MEMBERS_COLLECTION,
56635
+ filter: { whereIn: { trackId: [...trackIds] } }
56636
+ });
56637
+ const touched = new Set(members.map((m) => String(m.data["summaryId"] ?? "")));
56638
+ for (const summaryIdValue of touched) {
56639
+ const remaining = await this.membersOf(summaryIdValue);
56640
+ const raw = await this.store.get.query({
56641
+ collection: SUMMARIES_COLLECTION,
56642
+ key: summaryIdValue
56643
+ });
56644
+ if (typeof raw !== "object" || raw === null) continue;
56645
+ const row = raw;
56646
+ await this.store.updateWhere.mutate({
56647
+ collection: SUMMARIES_COLLECTION,
56648
+ filter: { where: {
56649
+ deviceId: num$1(row["deviceId"]),
56650
+ openedAt: num$1(row["openedAt"])
56651
+ } },
56652
+ data: { memberCount: remaining.length }
56653
+ });
56654
+ }
56655
+ return [{
56656
+ family: "summaries",
56657
+ rows: deleted,
56658
+ bytes: 0
56659
+ }];
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
+ }
56673
+ /** Age sweep on the device window: everything opened before `cutoffMs`, members included. */
56674
+ async evictBefore(deviceId, cutoffMs) {
56675
+ const rows = await this.store.query.query({
56676
+ collection: SUMMARIES_COLLECTION,
56677
+ filter: {
56678
+ where: { deviceId },
56679
+ whereBetween: { openedAt: [0, cutoffMs - 1] },
56680
+ limit: 1e4
56681
+ }
56682
+ });
56683
+ if (rows.length === 0) return 0;
56684
+ const ids = rows.map((r) => r.id);
56685
+ await this.onEvict?.(ids);
56686
+ await this.store.deleteWhere.mutate({
56687
+ collection: SUMMARY_MEMBERS_COLLECTION,
56688
+ filter: { whereIn: { summaryId: ids } }
56689
+ });
56690
+ const { deleted } = await this.store.deleteWhere.mutate({
56691
+ collection: SUMMARIES_COLLECTION,
56692
+ filter: {
56693
+ where: { deviceId },
56694
+ whereBetween: { openedAt: [0, cutoffMs - 1] }
56695
+ }
56696
+ });
56697
+ return deleted;
56698
+ }
56699
+ };
56317
56700
  /**
56318
56701
  * Where the completion marker lives.
56319
56702
  *
@@ -58609,6 +58992,10 @@ var ANALYTICS_COLLECTIONS = [
58609
58992
  collection: PLATES_COLLECTION,
58610
58993
  classification: derived(PLATE_COLUMNS)
58611
58994
  },
58995
+ {
58996
+ collection: SUMMARY_MEMBERS_COLLECTION,
58997
+ classification: derived(SUMMARY_MEMBERS_COLUMNS)
58998
+ },
58612
58999
  {
58613
59000
  collection: MEDIA_COLLECTION,
58614
59001
  classification: {
@@ -58652,6 +59039,13 @@ var ANALYTICS_COLLECTIONS = [
58652
59039
  rootId: "ops-log"
58653
59040
  }
58654
59041
  },
59042
+ {
59043
+ collection: SUMMARIES_COLLECTION,
59044
+ classification: {
59045
+ kind: "retention-root",
59046
+ rootId: "summary"
59047
+ }
59048
+ },
58655
59049
  {
58656
59050
  collection: IDENTITIES_COLLECTION,
58657
59051
  classification: {
@@ -58811,6 +59205,10 @@ function resolveMediaOwner(row) {
58811
59205
  kind: "event",
58812
59206
  eventId: ownerId
58813
59207
  };
59208
+ case "summary": return {
59209
+ kind: "summary",
59210
+ summaryId: ownerId
59211
+ };
58814
59212
  case "face": return ownerId.startsWith("face-") ? {
58815
59213
  kind: "track",
58816
59214
  trackId: ownerId.slice(FACE_MEDIA_OWNER_PREFIX.length)
@@ -58897,11 +59295,14 @@ var DEFAULT_ORPHAN_LIMITS = {
58897
59295
  function questionsFor(refs) {
58898
59296
  const trackIds = /* @__PURE__ */ new Set();
58899
59297
  const eventIds = /* @__PURE__ */ new Set();
59298
+ const summaryIds = /* @__PURE__ */ new Set();
58900
59299
  for (const ref of refs) if (ref.kind === "track") trackIds.add(ref.trackId);
58901
59300
  else if (ref.kind === "event") eventIds.add(ref.eventId);
59301
+ else if (ref.kind === "summary") summaryIds.add(ref.summaryId);
58902
59302
  return {
58903
59303
  trackIds: [...trackIds],
58904
- eventIds: [...eventIds]
59304
+ eventIds: [...eventIds],
59305
+ summaryIds: [...summaryIds]
58905
59306
  };
58906
59307
  }
58907
59308
  /**
@@ -58933,12 +59334,14 @@ async function auditScope(scope, deps, limits, cursor) {
58933
59334
  break;
58934
59335
  }
58935
59336
  const refs = rows.map((row) => scope.resolve(row));
58936
- const { trackIds, eventIds } = questionsFor(refs);
59337
+ const { trackIds, eventIds, summaryIds } = questionsFor(refs);
58937
59338
  let liveTracks;
58938
59339
  let liveEvents;
59340
+ let liveSummaries;
58939
59341
  try {
58940
59342
  liveTracks = trackIds.length > 0 ? await deps.liveTrackIds(trackIds) : /* @__PURE__ */ new Set();
58941
59343
  liveEvents = eventIds.length > 0 ? await deps.liveEventIds(eventIds) : /* @__PURE__ */ new Set();
59344
+ liveSummaries = summaryIds.length > 0 ? await deps.liveSummaryIds(summaryIds) : /* @__PURE__ */ new Set();
58942
59345
  } catch (err) {
58943
59346
  deps.logger.warn("orphan audit: liveness probe failed — scope abandoned for this run", { meta: {
58944
59347
  collection: scope.collection,
@@ -58962,7 +59365,7 @@ async function auditScope(scope, deps, limits, cursor) {
58962
59365
  tally.unresolvable += 1;
58963
59366
  continue;
58964
59367
  }
58965
- 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;
58966
59369
  const ts = num(row.data, scope.timestampField);
58967
59370
  if (ts !== null && ts > graceFloor) {
58968
59371
  tally.withinGrace += 1;
@@ -59212,12 +59615,14 @@ async function sweepTracklessRoots(deps) {
59212
59615
  cutoffMs
59213
59616
  });
59214
59617
  const sensor = deps.evictSensorEvents ? await deps.evictSensorEvents(deviceId, cutoffMs) : 0;
59618
+ const summaries = deps.evictSummaries ? await deps.evictSummaries(deviceId, cutoffMs) : 0;
59215
59619
  const evicted = {
59216
59620
  motion: events.motion,
59217
59621
  audio: events.audio,
59218
- sensor
59622
+ sensor,
59623
+ summaries
59219
59624
  };
59220
- if (evicted.motion.length + evicted.audio.length + evicted.sensor > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
59625
+ if (evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries > 0) await deps.onEvicted(deviceId, evicted, cutoffs);
59221
59626
  } catch (err) {
59222
59627
  deps.onError(deviceId, err);
59223
59628
  }
@@ -64912,6 +65317,310 @@ function rowFromMetadata(id, metadata) {
64912
65317
  };
64913
65318
  }
64914
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
65510
+ //#region src/pipeline-analytics/summary/summary-sealer.ts
65511
+ function errMsg(err) {
65512
+ return err instanceof Error ? err.message : String(err);
65513
+ }
65514
+ var SummarySealer = class {
65515
+ deps;
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();
65525
+ constructor(deps) {
65526
+ this.deps = deps;
65527
+ }
65528
+ openFor(deviceId) {
65529
+ return this.openMirror.get(deviceId) ?? null;
65530
+ }
65531
+ async onTrackOpened(input) {
65532
+ try {
65533
+ const row = await this.deps.summaries.openOrAbsorb({
65534
+ deviceId: input.deviceId,
65535
+ trackId: input.trackId,
65536
+ at: this.deps.now()
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
+ });
65544
+ } catch (err) {
65545
+ this.deps.logger.warn("summary: could not open or absorb for a track", {
65546
+ tags: { deviceId: input.deviceId },
65547
+ meta: {
65548
+ trackId: input.trackId,
65549
+ error: errMsg(err)
65550
+ }
65551
+ });
65552
+ }
65553
+ }
65554
+ /** One sweep at a time (THE latch). A tick that finds one in flight returns. */
65555
+ async sweep() {
65556
+ if (this.sweepInFlight) return;
65557
+ this.sweepInFlight = true;
65558
+ try {
65559
+ const open = await this.deps.summaries.listOpen();
65560
+ for (const summary of open) await this.sealIfDone(summary);
65561
+ } catch (err) {
65562
+ this.deps.logger.warn("summary sweep: could not list open summaries", { meta: { error: errMsg(err) } });
65563
+ } finally {
65564
+ this.sweepInFlight = false;
65565
+ }
65566
+ }
65567
+ async sealIfDone(summary) {
65568
+ const { id: summaryId, deviceId } = summary;
65569
+ let openTracks;
65570
+ try {
65571
+ openTracks = await this.deps.openTrackIds(deviceId);
65572
+ } catch (err) {
65573
+ this.deps.logger.warn("summary sweep: could not read open tracks — seal aborted this sweep", {
65574
+ tags: { deviceId },
65575
+ meta: {
65576
+ summaryId,
65577
+ error: errMsg(err)
65578
+ }
65579
+ });
65580
+ return;
65581
+ }
65582
+ if (openTracks.length > 0) return;
65583
+ let quiet;
65584
+ try {
65585
+ quiet = await this.deps.isCameraQuiet(deviceId);
65586
+ } catch (err) {
65587
+ this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
65588
+ tags: { deviceId },
65589
+ meta: {
65590
+ summaryId,
65591
+ error: errMsg(err)
65592
+ }
65593
+ });
65594
+ return;
65595
+ }
65596
+ if (!quiet) return;
65597
+ const sealedAt = this.deps.now();
65598
+ try {
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
+ }
65612
+ } catch (err) {
65613
+ this.deps.logger.warn("summary sweep: seal failed — the row stays open for the next sweep", {
65614
+ tags: { deviceId },
65615
+ meta: {
65616
+ summaryId,
65617
+ error: errMsg(err)
65618
+ }
65619
+ });
65620
+ }
65621
+ }
65622
+ };
65623
+ //#endregion
64915
65624
  //#region src/pipeline-analytics/store/settings-raw-mirror.ts
64916
65625
  /** How long a mirrored entry is served before a refresh is started behind the
64917
65626
  * next reader. See the module doc for why this matches the per-kind caches'
@@ -66224,6 +66933,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
66224
66933
  storageMigrationLeaseId = null;
66225
66934
  /** Per-camera history of LINKED-device sensor state changes (Part B). */
66226
66935
  sensorEventStore = null;
66936
+ /** The per-camera session envelope (D359). Bound with the other stores at boot. */
66937
+ summaryStore = null;
66938
+ /** The ONLY writer of a summary's lifecycle (D359). */
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();
66227
66949
  /** Ingest-side reverse index (sensor device → linked camera ids), TTL-cached
66228
66950
  * so the DeviceStateChanged handler stays cheap. */
66229
66951
  linkedCamerasCache = null;
@@ -67462,6 +68184,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
67462
68184
  MediaStore.declare(api.settingsStore),
67463
68185
  EventStore.declare(api.settingsStore),
67464
68186
  SensorEventStore.declare(api.settingsStore),
68187
+ SummaryStore.declare(api.settingsStore),
67465
68188
  IdentityStore.declare(api.settingsStore),
67466
68189
  FaceStore.declare(api.settingsStore),
67467
68190
  PlateStore.declare(api.settingsStore),
@@ -67723,6 +68446,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
67723
68446
  store: api.settingsStore,
67724
68447
  logger: logger.child("SensorEventStore")
67725
68448
  });
68449
+ this.summaryStore = new SummaryStore({
68450
+ store: api.settingsStore,
68451
+ logger: this.ctx.logger.child("summary"),
68452
+ onEvict: async (ids) => {
68453
+ await this.mediaStore?.deleteForOwner("summary", ids);
68454
+ }
68455
+ });
68456
+ this.summarySealer = new SummarySealer({
68457
+ summaries: this.summaryStore,
68458
+ openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
68459
+ isCameraQuiet: async (deviceId) => (this.stationaryRegistry?.count(deviceId) ?? 0) === 0,
68460
+ now: () => Date.now(),
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
+ }
68466
+ });
67726
68467
  this.packageDropDetector = new PackageDropDetector({
67727
68468
  events: eventStore,
67728
68469
  emit: {
@@ -68570,6 +69311,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68570
69311
  this.eventMediaDataPlane = await this.ctx.dataPlane?.serve({
68571
69312
  prefix: "event-media",
68572
69313
  access: planeAccess("event-media"),
69314
+ share: ["events-view"],
68573
69315
  handler
68574
69316
  }) ?? null;
68575
69317
  this.eventMediaBaseUrl = this.eventMediaDataPlane !== null ? `/addon/${this.ctx.id}/event-media` : null;
@@ -69935,6 +70677,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69935
70677
  className: t.className
69936
70678
  }
69937
70679
  });
70680
+ this.summarySealer?.onTrackOpened({
70681
+ deviceId,
70682
+ trackId: id
70683
+ });
69938
70684
  const startPayload = buildTrackLifecyclePayload({
69939
70685
  deviceId,
69940
70686
  trackId: id,
@@ -70152,6 +70898,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70152
70898
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
70153
70899
  else plateCrops += 1;
70154
70900
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
70901
+ this.offerGroupShot(deviceId, result, frameHandle);
70155
70902
  for (const retry of this.collectFirstFrameRetries(deviceId, result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
70156
70903
  const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
70157
70904
  const rasterFallbackCandidates = [];
@@ -71034,6 +71781,63 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71034
71781
  * is the scheduling (coalescing, priority, budget) and the bookkeeping the
71035
71782
  * retry gate reads.
71036
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
+ }
71037
71841
  async persistKeyFrames(deviceId, timestamp, captures, frameHandle) {
71038
71842
  const getNative = this.getNativeKeyFrameRgb;
71039
71843
  const mediaStore = this.mediaStore;
@@ -71178,6 +71982,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71178
71982
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
71179
71983
  * fine-classification attribution. */
71180
71984
  promoteSemanticRecognition(input) {
71985
+ this.recognizedTrackIds.add(input.trackId);
71181
71986
  const ctx = this.ctxIfReady;
71182
71987
  if (ctx === null) return;
71183
71988
  promoteSemanticBestFrame({
@@ -71959,6 +72764,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71959
72764
  async sweepExpiredTracks() {
71960
72765
  if (this.shuttingDown || !this.trackStore) return;
71961
72766
  await this.trackCloser.sweep();
72767
+ await this.summarySealer?.sweep();
71962
72768
  }
71963
72769
  /** Unified retention (Phase 1): earliest retained footage per device — the
71964
72770
  * follow-recordings cutoff source. Cached 10 min: availability answers in
@@ -72055,6 +72861,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72055
72861
  await this.runTrackRetentionSweep(deviceIds, now);
72056
72862
  const eventStore = this.eventStore;
72057
72863
  const sensorEventStore = this.sensorEventStore;
72864
+ const summaryStore = this.summaryStore;
72058
72865
  await sweepTracklessRoots({
72059
72866
  deviceIds,
72060
72867
  nowMs: now,
@@ -72064,15 +72871,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72064
72871
  cutoffMs
72065
72872
  }),
72066
72873
  evictSensorEvents: sensorEventStore ? (deviceId, cutoffMs) => sensorEventStore.evictBefore(deviceId, cutoffMs) : null,
72874
+ evictSummaries: summaryStore ? (deviceId, cutoffMs) => summaryStore.evictBefore(deviceId, cutoffMs) : null,
72067
72875
  onEvicted: async (deviceId, evicted, cutoffs) => {
72068
- const total = evicted.motion.length + evicted.audio.length + evicted.sensor;
72876
+ const total = evicted.motion.length + evicted.audio.length + evicted.sensor + evicted.summaries;
72069
72877
  await this.eventsOpsLog?.append({
72070
72878
  op: "prune",
72071
72879
  reason: "retention",
72072
72880
  deviceId,
72073
72881
  itemsAffected: total,
72074
72882
  bytesReclaimed: 0,
72075
- detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor`
72883
+ detail: `trackless age sweep (${cutoffs.effectiveMode}): ${evicted.motion.length} motion, ${evicted.audio.length} audio, ${evicted.sensor} sensor, ${evicted.summaries} summaries`
72076
72884
  });
72077
72885
  this.ctx.logger.info("analytics trackless event eviction (age sweep)", {
72078
72886
  tags: { deviceId },
@@ -72080,6 +72888,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72080
72888
  motion: evicted.motion.length,
72081
72889
  audio: evicted.audio.length,
72082
72890
  sensor: evicted.sensor,
72891
+ summaries: evicted.summaries,
72083
72892
  mode: cutoffs.effectiveMode,
72084
72893
  cutoffMs: cutoffs.cutoffMs
72085
72894
  }
@@ -73412,6 +74221,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73412
74221
  },
73413
74222
  liveTrackIds: (ids) => trackStore.existingTrackIds(ids),
73414
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
+ },
73415
74229
  reclaim: async (collection, key) => {
73416
74230
  if (collection === "pipeline-analytics:media") {
73417
74231
  await mediaStore.deleteByKey(key);
@@ -74559,7 +75373,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
74559
75373
  mediaStore,
74560
75374
  faceStore: this.faceStore,
74561
75375
  plateStore: this.plateStore,
74562
- objectEmbeddingStore: this.objectEmbeddingStore
75376
+ objectEmbeddingStore: this.objectEmbeddingStore,
75377
+ summaryStore: this.summaryStore
74563
75378
  };
74564
75379
  const absent = absentCascadeLegs(stores);
74565
75380
  if (absent.length > 0) this.ctx.logger.warn("track cascade is missing a leg — its rows will be left behind", { meta: {
@@ -75045,7 +75860,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75045
75860
  if (!file) return null;
75046
75861
  return {
75047
75862
  bytes: Buffer.from(file.base64, "base64"),
75048
- key: file.key
75863
+ key: file.key,
75864
+ deviceId: file.deviceId
75049
75865
  };
75050
75866
  }
75051
75867
  /**
@@ -75059,7 +75875,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75059
75875
  try {
75060
75876
  return {
75061
75877
  bytes: await makeSquareThumb(media.bytes, variant.size),
75062
- key: `${media.key}|t${variant.size}`
75878
+ key: `${media.key}|t${variant.size}`,
75879
+ deviceId: media.deviceId
75063
75880
  };
75064
75881
  } catch (err) {
75065
75882
  this.ctx.logger.debug("event media: thumb variant failed — serving full", { meta: {
@@ -75089,7 +75906,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75089
75906
  const file = await (this.mediaStore?.getByKey(key) ?? Promise.resolve(null));
75090
75907
  return file === null ? null : {
75091
75908
  bytes: Buffer.from(file.base64, "base64"),
75092
- key: file.key
75909
+ key: file.key,
75910
+ deviceId: file.deviceId
75093
75911
  };
75094
75912
  };
75095
75913
  const eventFiles = await listInfo("event", id);