@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.
- package/dist/{dist-DEAreP8H.mjs → dist-DvmLRRIq.mjs} +13 -4
- package/dist/{dist-BB70nZUm.js → dist-_fedHYU3.js} +13 -4
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DAWsjr0n.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ffBUTS31.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-CKa5HUFy.mjs → hostInit-x1-ijfAu.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +852 -34
- package/dist/pipeline-analytics/index.mjs +906 -88
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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$
|
|
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$
|
|
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$
|
|
30778
|
+
value: WEIGHT_DWELL * clamp01$2(input.durationMs / DWELL_FULL_MS)
|
|
30777
30779
|
},
|
|
30778
30780
|
{
|
|
30779
30781
|
reason: "proximity",
|
|
30780
|
-
value: WEIGHT_PROXIMITY * clamp01$
|
|
30782
|
+
value: WEIGHT_PROXIMITY * clamp01$2(input.peakBboxAreaFrac / SIZE_FULL)
|
|
30781
30783
|
},
|
|
30782
30784
|
{
|
|
30783
30785
|
reason: "travel",
|
|
30784
|
-
value: WEIGHT_TRAVEL * clamp01$
|
|
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$
|
|
30805
|
+
importance: clamp01$2(sum * STATIC_IMPORTANCE_MULTIPLIER),
|
|
30804
30806
|
reason: best.reason
|
|
30805
30807
|
};
|
|
30806
30808
|
return {
|
|
30807
|
-
importance: clamp01$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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);
|