@camstack/addon-post-analysis 1.2.115 → 1.2.117
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-C0uPGtBv.js → dist-Dk-BdVHw.js} +98 -1
- package/dist/{dist-B13sFjR2.mjs → dist-PMOtjzh2.mjs} +87 -2
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-k_ZbrH_x.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BmR_bLi_.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-m0Jqwk3C.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DlTk9-4b.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DckJUjuL.mjs → hostInit-Ddl2vUu6.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +897 -508
- package/dist/pipeline-analytics/index.mjs +895 -506
- 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-Dk-BdVHw.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");
|
|
@@ -15139,28 +15139,38 @@ function selectSummaryTiles(candidates, opts) {
|
|
|
15139
15139
|
};
|
|
15140
15140
|
}
|
|
15141
15141
|
/**
|
|
15142
|
+
* Ranked leftover after the quota pass — refill a mosaic that dropped
|
|
15143
|
+
* duplicate frames or missing crops without walking the same track twice.
|
|
15144
|
+
*/
|
|
15145
|
+
function remainingSummaryCandidates(kept, taken, rank) {
|
|
15146
|
+
const takenIds = new Set(taken.map((c) => c.trackId));
|
|
15147
|
+
return kept.filter((c) => !takenIds.has(c.trackId)).toSorted(byRankDesc(rank));
|
|
15148
|
+
}
|
|
15149
|
+
/**
|
|
15142
15150
|
* The media a tile is drawn from, best first.
|
|
15143
15151
|
*
|
|
15144
|
-
*
|
|
15145
|
-
*
|
|
15146
|
-
*
|
|
15147
|
-
*
|
|
15152
|
+
* First-level group crop only (`groupCrop`), then the group's native-frame
|
|
15153
|
+
* pointer, then a member's clean full frame. Member `thumbnail` / plate /
|
|
15154
|
+
* face crops stay off this ladder: a successful plate read overwrites the
|
|
15155
|
+
* vehicle first-level crop under the same `thumbnail` kind, and a digest
|
|
15156
|
+
* tile of a targa is the wrong picture. Boxed frames stay absent: a drawn
|
|
15157
|
+
* rectangle is a permanent lie in a digest tile. Duplicate driveways from
|
|
15158
|
+
* tracker ID churn are collapsed by `frameDedupeKey`, not by hiding the
|
|
15159
|
+
* full-frame fallback.
|
|
15148
15160
|
*/
|
|
15149
15161
|
var SUMMARY_MEDIA_LADDER = [
|
|
15150
15162
|
"groupCrop",
|
|
15151
|
-
"
|
|
15163
|
+
"nativeFrame",
|
|
15152
15164
|
"keyFrame",
|
|
15153
|
-
"
|
|
15154
|
-
"fullFrame"
|
|
15155
|
-
"thumbnailSmall",
|
|
15156
|
-
"thumbnail"
|
|
15165
|
+
"keyFrameSmall",
|
|
15166
|
+
"fullFrame"
|
|
15157
15167
|
];
|
|
15158
15168
|
/**
|
|
15159
15169
|
* Pick ONE media row for a track — the cheap question first (index rows only),
|
|
15160
15170
|
* so exactly one blob is ever read.
|
|
15161
15171
|
*
|
|
15162
|
-
* Within a rung the row CLOSEST to the track's peak is chosen;
|
|
15163
|
-
*
|
|
15172
|
+
* Within a rung the row CLOSEST to the track's peak is chosen; a member may
|
|
15173
|
+
* carry more than one full-frame still, so "the first one" would systematically
|
|
15164
15174
|
* pick the moment the subject entered rather than the moment it was best seen.
|
|
15165
15175
|
*/
|
|
15166
15176
|
function pickSummaryMedia(rows, aroundMs) {
|
|
@@ -15171,6 +15181,13 @@ function pickSummaryMedia(rows, aroundMs) {
|
|
|
15171
15181
|
}
|
|
15172
15182
|
return null;
|
|
15173
15183
|
}
|
|
15184
|
+
/**
|
|
15185
|
+
* Same camera, same wall-clock second → same JPEG. Tracker ID churn
|
|
15186
|
+
* otherwise fills a mosaic with eleven copies of one driveway still.
|
|
15187
|
+
*/
|
|
15188
|
+
function frameDedupeKey(deviceId, mediaTimestamp) {
|
|
15189
|
+
return `${deviceId}:${Math.floor(mediaTimestamp / 1e3)}`;
|
|
15190
|
+
}
|
|
15174
15191
|
//#endregion
|
|
15175
15192
|
//#region src/notification-center/summary/summary-window.ts
|
|
15176
15193
|
/**
|
|
@@ -15470,7 +15487,12 @@ var NcSummaryProducer = class {
|
|
|
15470
15487
|
droppedStationary: filtered.droppedStationary
|
|
15471
15488
|
};
|
|
15472
15489
|
}
|
|
15473
|
-
const loaded = await this.loadTiles(
|
|
15490
|
+
const loaded = await this.loadTiles({
|
|
15491
|
+
rule,
|
|
15492
|
+
preferred: selection.tiles,
|
|
15493
|
+
refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
|
|
15494
|
+
maxTiles: rule.maxTiles
|
|
15495
|
+
});
|
|
15474
15496
|
const detections = countDetections({ tracks: selection.tiles.map((t) => ({
|
|
15475
15497
|
firstSeen: t.firstSeen,
|
|
15476
15498
|
lastSeen: t.lastSeen,
|
|
@@ -15528,7 +15550,7 @@ var NcSummaryProducer = class {
|
|
|
15528
15550
|
tiles: loaded.length,
|
|
15529
15551
|
cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
15530
15552
|
camerasDropped: selection.camerasDropped,
|
|
15531
|
-
droppedNoMedia:
|
|
15553
|
+
droppedNoMedia: Math.max(0, rule.maxTiles - loaded.length),
|
|
15532
15554
|
mosaicBytes: mosaic.bytes,
|
|
15533
15555
|
mosaicPublished: mosaic.ref !== null,
|
|
15534
15556
|
enqueued,
|
|
@@ -15592,10 +15614,18 @@ var NcSummaryProducer = class {
|
|
|
15592
15614
|
}
|
|
15593
15615
|
return all;
|
|
15594
15616
|
}
|
|
15595
|
-
/** One blob per tile — the cheap question (index rows) always first.
|
|
15596
|
-
|
|
15617
|
+
/** One blob per tile — the cheap question (index rows) always first.
|
|
15618
|
+
* Duplicate camera-seconds are skipped so tracker ID churn cannot paint
|
|
15619
|
+
* the same driveway eleven times; leftover ranked candidates refill. */
|
|
15620
|
+
async loadTiles(input) {
|
|
15597
15621
|
const out = [];
|
|
15598
|
-
|
|
15622
|
+
const seenFrames = /* @__PURE__ */ new Set();
|
|
15623
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
15624
|
+
const queue = [...input.preferred, ...input.refill];
|
|
15625
|
+
for (const candidate of queue) {
|
|
15626
|
+
if (out.length >= input.maxTiles) break;
|
|
15627
|
+
if (seenIds.has(candidate.trackId)) continue;
|
|
15628
|
+
seenIds.add(candidate.trackId);
|
|
15599
15629
|
const tags = { deviceId: candidate.deviceId };
|
|
15600
15630
|
try {
|
|
15601
15631
|
const rows = await this.deps.listMediaInfo(candidate.trackId);
|
|
@@ -15604,25 +15634,38 @@ var NcSummaryProducer = class {
|
|
|
15604
15634
|
this.deps.logger.debug("summary tile dropped: the track carries no clean media", {
|
|
15605
15635
|
tags,
|
|
15606
15636
|
meta: {
|
|
15607
|
-
ruleId: rule.id,
|
|
15637
|
+
ruleId: input.rule.id,
|
|
15608
15638
|
trackId: candidate.trackId,
|
|
15609
15639
|
mediaRows: rows.length
|
|
15610
15640
|
}
|
|
15611
15641
|
});
|
|
15612
15642
|
continue;
|
|
15613
15643
|
}
|
|
15644
|
+
const frameKey = frameDedupeKey(candidate.deviceId, picked.timestamp);
|
|
15645
|
+
if (seenFrames.has(frameKey)) {
|
|
15646
|
+
this.deps.logger.debug("summary tile dropped: same frame as an earlier tile", {
|
|
15647
|
+
tags,
|
|
15648
|
+
meta: {
|
|
15649
|
+
ruleId: input.rule.id,
|
|
15650
|
+
trackId: candidate.trackId,
|
|
15651
|
+
frameKey
|
|
15652
|
+
}
|
|
15653
|
+
});
|
|
15654
|
+
continue;
|
|
15655
|
+
}
|
|
15614
15656
|
const jpeg = await this.deps.readMedia(picked.key);
|
|
15615
15657
|
if (jpeg === null || jpeg.byteLength === 0) {
|
|
15616
15658
|
this.deps.logger.debug("summary tile dropped: the media blob could not be read", {
|
|
15617
15659
|
tags,
|
|
15618
15660
|
meta: {
|
|
15619
|
-
ruleId: rule.id,
|
|
15661
|
+
ruleId: input.rule.id,
|
|
15620
15662
|
trackId: candidate.trackId,
|
|
15621
15663
|
mediaKey: picked.key
|
|
15622
15664
|
}
|
|
15623
15665
|
});
|
|
15624
15666
|
continue;
|
|
15625
15667
|
}
|
|
15668
|
+
seenFrames.add(frameKey);
|
|
15626
15669
|
out.push({
|
|
15627
15670
|
candidate,
|
|
15628
15671
|
jpeg
|
|
@@ -15631,7 +15674,7 @@ var NcSummaryProducer = class {
|
|
|
15631
15674
|
this.deps.logger.warn("summary tile dropped: reading its media threw", {
|
|
15632
15675
|
tags,
|
|
15633
15676
|
meta: {
|
|
15634
|
-
ruleId: rule.id,
|
|
15677
|
+
ruleId: input.rule.id,
|
|
15635
15678
|
trackId: candidate.trackId,
|
|
15636
15679
|
error: String(err)
|
|
15637
15680
|
}
|
|
@@ -39894,8 +39937,9 @@ var OverlayDetailStateStore = class {
|
|
|
39894
39937
|
*
|
|
39895
39938
|
* See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
|
|
39896
39939
|
* docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
|
|
39897
|
-
* (surface C).
|
|
39898
|
-
*
|
|
39940
|
+
* (surface C). Confirmation is N frames (default 3); dwell default is 0
|
|
39941
|
+
* (an extra bar). The 15 s dwell plus a 5 s round-gap missed a real
|
|
39942
|
+
* parcel on device 615 (2026-08-24 13:40).
|
|
39899
39943
|
*/
|
|
39900
39944
|
var PackageDropSettingsSchema = require_dist.object({
|
|
39901
39945
|
/**
|
|
@@ -39916,13 +39960,20 @@ var PackageDropSettingsSchema = require_dist.object({
|
|
|
39916
39960
|
*/
|
|
39917
39961
|
packageDropEnabled: require_dist.boolean().default(true),
|
|
39918
39962
|
/**
|
|
39919
|
-
*
|
|
39920
|
-
*
|
|
39921
|
-
*
|
|
39922
|
-
*
|
|
39923
|
-
*
|
|
39963
|
+
* Frames the box must be re-seen on before it is a parked package.
|
|
39964
|
+
* Same bar as tracker `minHits`: one frame is a flicker, two can still
|
|
39965
|
+
* be a passer-by's bag, three is "it stayed". A settle burst of three
|
|
39966
|
+
* frames in ~1 s is enough; no second governor round after the courier
|
|
39967
|
+
* left. Device 615 2026-08-24 13:40 expired at 1 round / 1.5 s dwell.
|
|
39924
39968
|
*/
|
|
39925
|
-
|
|
39969
|
+
packageDropMinFrames: require_dist.number().int().min(1).max(20).default(3),
|
|
39970
|
+
/**
|
|
39971
|
+
* Extra OBSERVED dwell (seconds, since first-seen) on top of N frames.
|
|
39972
|
+
* 0 (the default) means the frame count is enough — a courier who drops
|
|
39973
|
+
* and leaves in 2 s still delivers. Raise this to reject a bag briefly
|
|
39974
|
+
* set down and snatched back.
|
|
39975
|
+
*/
|
|
39976
|
+
packageDropDwellSec: require_dist.number().int().min(0).max(3600).default(0),
|
|
39926
39977
|
/** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
|
|
39927
39978
|
packageDropMinBboxAreaFrac: require_dist.number().min(0).max(1).default(.004),
|
|
39928
39979
|
/**
|
|
@@ -39940,13 +39991,15 @@ var PackageDropSettingsSchema = require_dist.object({
|
|
|
39940
39991
|
packageDropPickupEnabled: require_dist.boolean().default(true),
|
|
39941
39992
|
/**
|
|
39942
39993
|
* A parcel needs a deliverer: a stillness candidate only ever promotes to
|
|
39943
|
-
* `delivered` if a non-package track (person/vehicle/animal) was active
|
|
39944
|
-
*
|
|
39945
|
-
* departure only counts as `picked-up` with
|
|
39946
|
-
*
|
|
39947
|
-
*
|
|
39948
|
-
*
|
|
39949
|
-
*
|
|
39994
|
+
* `delivered` if a non-package track (person/vehicle/animal) was active IN
|
|
39995
|
+
* A CONFIGURED PACKAGE DROP-OFF ZONE within this many seconds BEFORE the
|
|
39996
|
+
* box first appeared — and a departure only counts as `picked-up` with
|
|
39997
|
+
* somebody in that zone around the disappearance. Whole-camera motion (a
|
|
39998
|
+
* walker at the far gate) does not count. 0 disables both gates. Device
|
|
39999
|
+
* 615's streetlight wedge cycled delivered→picked-up ~23×/night with
|
|
40000
|
+
* nobody in the drop-off zone. Default 300 s: wide enough to absorb the
|
|
40001
|
+
* motion-settle governor's cadence (the box is often first SEEN minutes
|
|
40002
|
+
* after the courier left).
|
|
39950
40003
|
*/
|
|
39951
40004
|
packageDropActivityWindowSec: require_dist.number().int().min(0).max(3600).default(300),
|
|
39952
40005
|
/**
|
|
@@ -39970,6 +40023,7 @@ function resolvePackageDropSettings(raw) {
|
|
|
39970
40023
|
};
|
|
39971
40024
|
return {
|
|
39972
40025
|
packageDropEnabled: pick("packageDropEnabled"),
|
|
40026
|
+
packageDropMinFrames: pick("packageDropMinFrames"),
|
|
39973
40027
|
packageDropDwellSec: pick("packageDropDwellSec"),
|
|
39974
40028
|
packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
|
|
39975
40029
|
packageDropMaxBboxAreaFrac: pick("packageDropMaxBboxAreaFrac"),
|
|
@@ -40227,29 +40281,6 @@ var DeferredBirthRegistry = class {
|
|
|
40227
40281
|
this.byDevice.delete(deviceKey);
|
|
40228
40282
|
}
|
|
40229
40283
|
};
|
|
40230
|
-
var AnalyticsGroupRecordSchema = require_dist.object({
|
|
40231
|
-
id: require_dist.string(),
|
|
40232
|
-
deviceId: require_dist.number().int(),
|
|
40233
|
-
openedAt: require_dist.number().int(),
|
|
40234
|
-
closedAt: require_dist.number().int(),
|
|
40235
|
-
timestamp: require_dist.number().int(),
|
|
40236
|
-
memberCount: require_dist.number().int(),
|
|
40237
|
-
memberTrackIds: require_dist.array(require_dist.string()).readonly(),
|
|
40238
|
-
className: require_dist.string(),
|
|
40239
|
-
classes: require_dist.array(require_dist.string()).readonly(),
|
|
40240
|
-
/** Relative event-media path, or null when the group has no picture yet. */
|
|
40241
|
-
mediaUrl: require_dist.string().nullable(),
|
|
40242
|
-
singleton: require_dist.boolean()
|
|
40243
|
-
});
|
|
40244
|
-
var AnalyticsGroupMemberSchema = require_dist.object({
|
|
40245
|
-
trackId: require_dist.string(),
|
|
40246
|
-
deviceId: require_dist.number().int(),
|
|
40247
|
-
className: require_dist.string(),
|
|
40248
|
-
firstSeen: require_dist.number().int(),
|
|
40249
|
-
lastSeen: require_dist.number().int(),
|
|
40250
|
-
mediaUrl: require_dist.string().nullable()
|
|
40251
|
-
});
|
|
40252
|
-
var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: require_dist.array(AnalyticsGroupMemberSchema).readonly() });
|
|
40253
40284
|
var ListGroupsInputSchema = require_dist.object({
|
|
40254
40285
|
deviceId: require_dist.number().int().optional(),
|
|
40255
40286
|
sinceMs: require_dist.number().int().optional(),
|
|
@@ -40259,66 +40290,11 @@ var ListGroupsInputSchema = require_dist.object({
|
|
|
40259
40290
|
});
|
|
40260
40291
|
var groupActions = require_dist.defineCustomActions({
|
|
40261
40292
|
"analytics.listGroups": require_dist.customAction(ListGroupsInputSchema, require_dist.object({
|
|
40262
|
-
groups: require_dist.array(AnalyticsGroupRecordSchema).readonly(),
|
|
40293
|
+
groups: require_dist.array(require_dist.AnalyticsGroupRecordSchema).readonly(),
|
|
40263
40294
|
hasMore: require_dist.boolean()
|
|
40264
40295
|
})),
|
|
40265
|
-
"analytics.getGroup": require_dist.customAction(require_dist.object({ groupId: require_dist.string().min(1) }), require_dist.object({ group: AnalyticsGroupDetailSchema.nullable() }))
|
|
40296
|
+
"analytics.getGroup": require_dist.customAction(require_dist.object({ groupId: require_dist.string().min(1) }), require_dist.object({ group: require_dist.AnalyticsGroupDetailSchema.nullable() }))
|
|
40266
40297
|
});
|
|
40267
|
-
//#endregion
|
|
40268
|
-
//#region src/pipeline-analytics/group/group-query.ts
|
|
40269
|
-
/**
|
|
40270
|
-
* Stamp a relative event-media URL.
|
|
40271
|
-
*
|
|
40272
|
-
* Media keys contain `:` and the data-plane serves them by key. Track ids are
|
|
40273
|
-
* bare and fall through to the existing event/track thumbnail resolver. Never
|
|
40274
|
-
* percent-encode: the handler does not URL-decode the path.
|
|
40275
|
-
*/
|
|
40276
|
-
function stampEventMediaUrl(baseUrl, id) {
|
|
40277
|
-
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
40278
|
-
if (id === null || id === void 0 || id.length === 0) return null;
|
|
40279
|
-
return `${baseUrl}/${id}`;
|
|
40280
|
-
}
|
|
40281
|
-
/** Union of every class on the members; `className` is the first, stable order. */
|
|
40282
|
-
function aggregateGroupClasses(tracks) {
|
|
40283
|
-
const seen = /* @__PURE__ */ new Set();
|
|
40284
|
-
const classes = [];
|
|
40285
|
-
for (const track of tracks) for (const name of [track.className, ...track.classes ?? []]) {
|
|
40286
|
-
if (name.length === 0 || seen.has(name)) continue;
|
|
40287
|
-
seen.add(name);
|
|
40288
|
-
classes.push(name);
|
|
40289
|
-
}
|
|
40290
|
-
return {
|
|
40291
|
-
className: classes[0] ?? "unknown",
|
|
40292
|
-
classes
|
|
40293
|
-
};
|
|
40294
|
-
}
|
|
40295
|
-
function toGroupRecord(input) {
|
|
40296
|
-
const { className, classes } = aggregateGroupClasses(input.tracks);
|
|
40297
|
-
const mediaId = input.group.mediaKey ?? (input.group.memberCount === 1 ? input.memberTrackIds[0] : null);
|
|
40298
|
-
return {
|
|
40299
|
-
id: input.group.id,
|
|
40300
|
-
deviceId: input.group.deviceId,
|
|
40301
|
-
openedAt: input.group.openedAt,
|
|
40302
|
-
closedAt: input.group.closedAt,
|
|
40303
|
-
timestamp: input.group.timestamp,
|
|
40304
|
-
memberCount: input.memberTrackIds.length > 0 ? input.memberTrackIds.length : input.group.memberCount,
|
|
40305
|
-
memberTrackIds: [...input.memberTrackIds],
|
|
40306
|
-
className,
|
|
40307
|
-
classes: [...classes],
|
|
40308
|
-
mediaUrl: stampEventMediaUrl(input.eventMediaBaseUrl, mediaId),
|
|
40309
|
-
singleton: input.memberTrackIds.length <= 1
|
|
40310
|
-
};
|
|
40311
|
-
}
|
|
40312
|
-
function toGroupMember(member, track, eventMediaBaseUrl) {
|
|
40313
|
-
return {
|
|
40314
|
-
trackId: member.trackId,
|
|
40315
|
-
deviceId: member.deviceId,
|
|
40316
|
-
className: track?.className ?? "unknown",
|
|
40317
|
-
firstSeen: track?.firstSeen ?? member.timestamp,
|
|
40318
|
-
lastSeen: track?.lastSeen ?? member.timestamp,
|
|
40319
|
-
mediaUrl: stampEventMediaUrl(eventMediaBaseUrl, member.trackId)
|
|
40320
|
-
};
|
|
40321
|
-
}
|
|
40322
40298
|
function trackToLite(track) {
|
|
40323
40299
|
return {
|
|
40324
40300
|
trackId: track.trackId,
|
|
@@ -40328,116 +40304,43 @@ function trackToLite(track) {
|
|
|
40328
40304
|
lastSeen: track.lastSeen
|
|
40329
40305
|
};
|
|
40330
40306
|
}
|
|
40331
|
-
|
|
40332
|
-
|
|
40333
|
-
|
|
40334
|
-
|
|
40335
|
-
|
|
40336
|
-
|
|
40337
|
-
|
|
40338
|
-
|
|
40339
|
-
|
|
40340
|
-
|
|
40341
|
-
|
|
40342
|
-
|
|
40343
|
-
|
|
40344
|
-
const
|
|
40345
|
-
|
|
40346
|
-
|
|
40347
|
-
|
|
40348
|
-
|
|
40349
|
-
if (lite !== null) lites.push(lite);
|
|
40350
|
-
rows.push(toGroupMember(member, lite, ports.eventMediaBaseUrl()));
|
|
40351
|
-
}
|
|
40352
|
-
return {
|
|
40353
|
-
...toGroupRecord({
|
|
40354
|
-
group,
|
|
40355
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
40356
|
-
tracks: lites,
|
|
40357
|
-
eventMediaBaseUrl: ports.eventMediaBaseUrl()
|
|
40358
|
-
}),
|
|
40359
|
-
members: rows
|
|
40360
|
-
};
|
|
40361
|
-
}
|
|
40362
|
-
async function buildGroupRecord(ports, group) {
|
|
40363
|
-
const members = await ports.groupStore.listMembers(group.id);
|
|
40364
|
-
const lites = [];
|
|
40365
|
-
for (const member of members) {
|
|
40366
|
-
const track = await ports.loadTrack(member.trackId);
|
|
40367
|
-
if (track !== null) lites.push(trackToLite(track));
|
|
40368
|
-
}
|
|
40369
|
-
return toGroupRecord({
|
|
40370
|
-
group,
|
|
40371
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
40372
|
-
tracks: lites,
|
|
40373
|
-
eventMediaBaseUrl: ports.eventMediaBaseUrl()
|
|
40307
|
+
/**
|
|
40308
|
+
* Gallery and digest read the same ladder: groupCrop, then the native-frame
|
|
40309
|
+
* pointer, then a member's clean full frame. Plate / face / member
|
|
40310
|
+
* thumbnails never reach the picker — a plate read overwrites the vehicle
|
|
40311
|
+
* first-level crop under the same `thumbnail` kind. A multi with no crop
|
|
40312
|
+
* still has a picture if the sealer stored `nativeKey`.
|
|
40313
|
+
*/
|
|
40314
|
+
var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
|
|
40315
|
+
"keyFrame",
|
|
40316
|
+
"keyFrameSmall",
|
|
40317
|
+
"fullFrame"
|
|
40318
|
+
]);
|
|
40319
|
+
function digestMediaRows(input) {
|
|
40320
|
+
const owned = [];
|
|
40321
|
+
if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
|
|
40322
|
+
key: input.mediaKey,
|
|
40323
|
+
kind: "groupCrop",
|
|
40324
|
+
timestamp: input.timestamp
|
|
40374
40325
|
});
|
|
40375
|
-
|
|
40376
|
-
|
|
40377
|
-
|
|
40378
|
-
|
|
40379
|
-
function groupWideCentralSquareLayout(unionBbox, frame) {
|
|
40380
|
-
return wideCentralSquareLayout(unionBbox, frame, { containSubject: true });
|
|
40381
|
-
}
|
|
40382
|
-
async function cropGroupFromRgb(rgb, frameWidth, frameHeight, unionBbox) {
|
|
40383
|
-
const layout = groupWideCentralSquareLayout(unionBbox, {
|
|
40384
|
-
W: frameWidth,
|
|
40385
|
-
H: frameHeight
|
|
40326
|
+
if (input.nativeKey !== null && input.nativeKey.length > 0) owned.push({
|
|
40327
|
+
key: input.nativeKey,
|
|
40328
|
+
kind: "nativeFrame",
|
|
40329
|
+
timestamp: input.timestamp
|
|
40386
40330
|
});
|
|
40387
|
-
|
|
40388
|
-
return
|
|
40331
|
+
if (owned.length > 0) return owned;
|
|
40332
|
+
return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
|
|
40389
40333
|
}
|
|
40390
40334
|
var MATCH_WINDOW_MS = 1e3;
|
|
40391
40335
|
var MOVE_EPS = .35;
|
|
40336
|
+
/** Live-assigner window: same predicate, recent positions only. */
|
|
40337
|
+
var LIVE_WINDOW_MS = 3e3;
|
|
40392
40338
|
/** Registry stationary is out; audio has no bbox and is never a member or a singleton. */
|
|
40393
40339
|
function isEligibleMember(track) {
|
|
40394
40340
|
if (track.closedReason === "stationary-promotion") return false;
|
|
40395
40341
|
if (track.className === "audio") return false;
|
|
40396
40342
|
return true;
|
|
40397
40343
|
}
|
|
40398
|
-
function temporallyOverlap(a, b, slackMs) {
|
|
40399
|
-
return a.firstSeen <= b.lastSeen + slackMs && b.firstSeen <= a.lastSeen + slackMs;
|
|
40400
|
-
}
|
|
40401
|
-
/** Connected components of temporal overlap (+ slack) per camera. */
|
|
40402
|
-
function sessionsOf(tracks) {
|
|
40403
|
-
const byDevice = /* @__PURE__ */ new Map();
|
|
40404
|
-
for (const t of tracks) {
|
|
40405
|
-
const list = byDevice.get(t.deviceId);
|
|
40406
|
-
if (list) list.push(t);
|
|
40407
|
-
else byDevice.set(t.deviceId, [t]);
|
|
40408
|
-
}
|
|
40409
|
-
const sessions = [];
|
|
40410
|
-
for (const deviceTracks of byDevice.values()) {
|
|
40411
|
-
const n = deviceTracks.length;
|
|
40412
|
-
const parent = deviceTracks.map((_, i) => i);
|
|
40413
|
-
const find = (i) => {
|
|
40414
|
-
let cur = i;
|
|
40415
|
-
while (parent[cur] !== cur) cur = parent[cur];
|
|
40416
|
-
let walk = i;
|
|
40417
|
-
while (parent[walk] !== cur) {
|
|
40418
|
-
const next = parent[walk];
|
|
40419
|
-
parent[walk] = cur;
|
|
40420
|
-
walk = next;
|
|
40421
|
-
}
|
|
40422
|
-
return cur;
|
|
40423
|
-
};
|
|
40424
|
-
const union = (i, j) => {
|
|
40425
|
-
const ri = find(i);
|
|
40426
|
-
const rj = find(j);
|
|
40427
|
-
if (ri !== rj) parent[ri] = rj;
|
|
40428
|
-
};
|
|
40429
|
-
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) if (temporallyOverlap(deviceTracks[i], deviceTracks[j], 3e3)) union(i, j);
|
|
40430
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
40431
|
-
for (let i = 0; i < n; i++) {
|
|
40432
|
-
const root = find(i);
|
|
40433
|
-
const bucket = buckets.get(root);
|
|
40434
|
-
if (bucket) bucket.push(deviceTracks[i]);
|
|
40435
|
-
else buckets.set(root, [deviceTracks[i]]);
|
|
40436
|
-
}
|
|
40437
|
-
sessions.push(...buckets.values());
|
|
40438
|
-
}
|
|
40439
|
-
return sessions;
|
|
40440
|
-
}
|
|
40441
40344
|
function diag(bbox) {
|
|
40442
40345
|
return Math.hypot(bbox.w, bbox.h);
|
|
40443
40346
|
}
|
|
@@ -40459,9 +40362,21 @@ function meanDiag(positions) {
|
|
|
40459
40362
|
for (const p of positions) sum += diag(p.bbox);
|
|
40460
40363
|
return sum / positions.length;
|
|
40461
40364
|
}
|
|
40462
|
-
function
|
|
40463
|
-
|
|
40464
|
-
|
|
40365
|
+
function vecLen(v) {
|
|
40366
|
+
return Math.hypot(v.x, v.y);
|
|
40367
|
+
}
|
|
40368
|
+
function cosine(a, b) {
|
|
40369
|
+
const la = vecLen(a);
|
|
40370
|
+
const lb = vecLen(b);
|
|
40371
|
+
if (la === 0 || lb === 0) return 1;
|
|
40372
|
+
return (a.x * b.x + a.y * b.y) / (la * lb);
|
|
40373
|
+
}
|
|
40374
|
+
function positionsInWindow(track, start, end) {
|
|
40375
|
+
return track.positions.filter((p) => p.timestamp >= start && p.timestamp <= end);
|
|
40376
|
+
}
|
|
40377
|
+
function windowDisplacement(positions) {
|
|
40378
|
+
const first = positions[0];
|
|
40379
|
+
const last = positions[positions.length - 1];
|
|
40465
40380
|
if (!first || !last) return {
|
|
40466
40381
|
x: 0,
|
|
40467
40382
|
y: 0
|
|
@@ -40473,24 +40388,19 @@ function netDisplacement(track) {
|
|
|
40473
40388
|
y: b.y - a.y
|
|
40474
40389
|
};
|
|
40475
40390
|
}
|
|
40476
|
-
function
|
|
40477
|
-
|
|
40478
|
-
}
|
|
40479
|
-
function cosine(a, b) {
|
|
40480
|
-
const la = vecLen(a);
|
|
40481
|
-
const lb = vecLen(b);
|
|
40482
|
-
if (la === 0 || lb === 0) return 1;
|
|
40483
|
-
return (a.x * b.x + a.y * b.y) / (la * lb);
|
|
40484
|
-
}
|
|
40485
|
-
function relativeMotion(track) {
|
|
40486
|
-
const mean = meanDiag(track.positions);
|
|
40391
|
+
function windowRelativeMotion(positions) {
|
|
40392
|
+
const mean = meanDiag(positions);
|
|
40487
40393
|
if (mean <= 0) return 0;
|
|
40488
|
-
return vecLen(
|
|
40394
|
+
return vecLen(windowDisplacement(positions)) / mean;
|
|
40489
40395
|
}
|
|
40490
|
-
/**
|
|
40491
|
-
|
|
40492
|
-
|
|
40493
|
-
|
|
40396
|
+
/**
|
|
40397
|
+
* Same cohesion + crossing veto as {@link pairTogether}, over `[now − 3 s, now]`.
|
|
40398
|
+
* Displacement/cosine use the window, not the whole track.
|
|
40399
|
+
*/
|
|
40400
|
+
function pairTogetherNow(a, b, now) {
|
|
40401
|
+
const start = now - LIVE_WINDOW_MS;
|
|
40402
|
+
const overlapStart = Math.max(a.firstSeen, b.firstSeen, start);
|
|
40403
|
+
const overlapEnd = Math.min(a.lastSeen, b.lastSeen, now);
|
|
40494
40404
|
if (overlapEnd < overlapStart) return false;
|
|
40495
40405
|
let joint = 0;
|
|
40496
40406
|
let close = 0;
|
|
@@ -40507,55 +40417,245 @@ function pairTogether(a, b) {
|
|
|
40507
40417
|
}
|
|
40508
40418
|
if (joint === 0) return false;
|
|
40509
40419
|
if (close / joint < .5) return false;
|
|
40510
|
-
const
|
|
40511
|
-
const
|
|
40512
|
-
|
|
40420
|
+
const wa = positionsInWindow(a, start, now);
|
|
40421
|
+
const wb = positionsInWindow(b, start, now);
|
|
40422
|
+
const aMoves = windowRelativeMotion(wa) >= MOVE_EPS;
|
|
40423
|
+
const bMoves = windowRelativeMotion(wb) >= MOVE_EPS;
|
|
40424
|
+
if (aMoves && bMoves && cosine(windowDisplacement(wa), windowDisplacement(wb)) < .3) return false;
|
|
40513
40425
|
return true;
|
|
40514
40426
|
}
|
|
40515
|
-
|
|
40516
|
-
|
|
40517
|
-
|
|
40518
|
-
|
|
40519
|
-
|
|
40520
|
-
|
|
40521
|
-
|
|
40522
|
-
|
|
40523
|
-
|
|
40524
|
-
|
|
40525
|
-
|
|
40526
|
-
|
|
40527
|
-
|
|
40528
|
-
|
|
40529
|
-
|
|
40530
|
-
|
|
40531
|
-
|
|
40532
|
-
|
|
40533
|
-
|
|
40534
|
-
|
|
40535
|
-
|
|
40536
|
-
|
|
40537
|
-
|
|
40538
|
-
|
|
40539
|
-
const
|
|
40540
|
-
|
|
40541
|
-
|
|
40542
|
-
|
|
40543
|
-
|
|
40544
|
-
|
|
40545
|
-
|
|
40546
|
-
|
|
40547
|
-
|
|
40548
|
-
|
|
40549
|
-
|
|
40550
|
-
|
|
40551
|
-
|
|
40552
|
-
|
|
40553
|
-
|
|
40554
|
-
|
|
40555
|
-
|
|
40427
|
+
var GroupLiveAssigner = class {
|
|
40428
|
+
nextId;
|
|
40429
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
40430
|
+
groups = /* @__PURE__ */ new Map();
|
|
40431
|
+
membership = /* @__PURE__ */ new Map();
|
|
40432
|
+
/** `${trackId}>${groupId}` → first instant the join predicate held. */
|
|
40433
|
+
joinSince = /* @__PURE__ */ new Map();
|
|
40434
|
+
/** trackId → first instant they failed pair against every remaining member. */
|
|
40435
|
+
leaveSince = /* @__PURE__ */ new Map();
|
|
40436
|
+
constructor(nextId = () => (0, node_crypto.randomUUID)()) {
|
|
40437
|
+
this.nextId = nextId;
|
|
40438
|
+
}
|
|
40439
|
+
observe(track, now) {
|
|
40440
|
+
this.snapshots.set(track.trackId, track);
|
|
40441
|
+
if (!isEligibleMember(track)) {
|
|
40442
|
+
this.removeFromGroup(track.trackId);
|
|
40443
|
+
return;
|
|
40444
|
+
}
|
|
40445
|
+
if (!this.membership.has(track.trackId)) this.birthSingleton(track, now);
|
|
40446
|
+
this.considerLeave(track, now);
|
|
40447
|
+
const groupId = this.membership.get(track.trackId);
|
|
40448
|
+
const group = groupId !== void 0 ? this.groups.get(groupId) : void 0;
|
|
40449
|
+
if (group) for (const id of [...group.members]) {
|
|
40450
|
+
if (id === track.trackId) continue;
|
|
40451
|
+
const snap = this.snapshots.get(id);
|
|
40452
|
+
if (snap) this.considerLeave(snap, now);
|
|
40453
|
+
}
|
|
40454
|
+
this.considerJoin(track, now);
|
|
40455
|
+
}
|
|
40456
|
+
groupOf(trackId) {
|
|
40457
|
+
return this.membership.get(trackId);
|
|
40458
|
+
}
|
|
40459
|
+
snapshot(trackId) {
|
|
40460
|
+
return this.snapshots.get(trackId);
|
|
40461
|
+
}
|
|
40462
|
+
liveGroups() {
|
|
40463
|
+
return [...this.groups.values()].map((g) => this.toLive(g));
|
|
40464
|
+
}
|
|
40465
|
+
idleGroups(now, idleMs) {
|
|
40466
|
+
return this.liveGroups().filter((g) => now >= g.lastActiveAt + idleMs);
|
|
40467
|
+
}
|
|
40468
|
+
drop(ids) {
|
|
40469
|
+
for (const id of ids) {
|
|
40470
|
+
const g = this.groups.get(id);
|
|
40471
|
+
if (!g) continue;
|
|
40472
|
+
for (const member of g.members) this.membership.delete(member);
|
|
40473
|
+
this.groups.delete(id);
|
|
40474
|
+
}
|
|
40475
|
+
}
|
|
40476
|
+
birthSingleton(track, now) {
|
|
40477
|
+
const id = this.nextId();
|
|
40478
|
+
this.groups.set(id, {
|
|
40479
|
+
id,
|
|
40480
|
+
deviceId: track.deviceId,
|
|
40481
|
+
openedAt: now,
|
|
40482
|
+
members: new Set([track.trackId])
|
|
40556
40483
|
});
|
|
40484
|
+
this.membership.set(track.trackId, id);
|
|
40557
40485
|
}
|
|
40558
|
-
|
|
40486
|
+
considerLeave(track, now) {
|
|
40487
|
+
const groupId = this.membership.get(track.trackId);
|
|
40488
|
+
if (groupId === void 0) return;
|
|
40489
|
+
const group = this.groups.get(groupId);
|
|
40490
|
+
if (!group || group.members.size < 2) {
|
|
40491
|
+
this.leaveSince.delete(track.trackId);
|
|
40492
|
+
return;
|
|
40493
|
+
}
|
|
40494
|
+
if ([...group.members].filter((id) => id !== track.trackId).some((id) => {
|
|
40495
|
+
const other = this.snapshots.get(id);
|
|
40496
|
+
return other !== void 0 && pairTogetherNow(track, other, now);
|
|
40497
|
+
})) {
|
|
40498
|
+
this.leaveSince.delete(track.trackId);
|
|
40499
|
+
return;
|
|
40500
|
+
}
|
|
40501
|
+
const since = this.leaveSince.get(track.trackId) ?? now;
|
|
40502
|
+
this.leaveSince.set(track.trackId, since);
|
|
40503
|
+
if (now - since < 3e3) return;
|
|
40504
|
+
this.peel(track, now);
|
|
40505
|
+
}
|
|
40506
|
+
peel(track, now) {
|
|
40507
|
+
this.removeFromGroup(track.trackId);
|
|
40508
|
+
this.leaveSince.delete(track.trackId);
|
|
40509
|
+
this.birthSingleton(track, now);
|
|
40510
|
+
}
|
|
40511
|
+
considerJoin(track, now) {
|
|
40512
|
+
const mineId = this.membership.get(track.trackId);
|
|
40513
|
+
if (mineId === void 0) return;
|
|
40514
|
+
const mine = this.groups.get(mineId);
|
|
40515
|
+
if (!mine || mine.members.size !== 1) {
|
|
40516
|
+
this.clearJoinClocks(track.trackId);
|
|
40517
|
+
return;
|
|
40518
|
+
}
|
|
40519
|
+
let best;
|
|
40520
|
+
for (const group of this.groups.values()) {
|
|
40521
|
+
if (group.id === mineId || group.deviceId !== track.deviceId) continue;
|
|
40522
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
40523
|
+
let together = false;
|
|
40524
|
+
for (const memberId of group.members) {
|
|
40525
|
+
const other = this.snapshots.get(memberId);
|
|
40526
|
+
if (!other) continue;
|
|
40527
|
+
if (pairTogetherNow(track, other, now)) {
|
|
40528
|
+
together = true;
|
|
40529
|
+
gap = Math.min(gap, centroidGap(track, other, now));
|
|
40530
|
+
}
|
|
40531
|
+
}
|
|
40532
|
+
if (!together) continue;
|
|
40533
|
+
if (best === void 0 || gap < best.gap) best = {
|
|
40534
|
+
groupId: group.id,
|
|
40535
|
+
gap
|
|
40536
|
+
};
|
|
40537
|
+
}
|
|
40538
|
+
if (best === void 0) {
|
|
40539
|
+
this.clearJoinClocks(track.trackId);
|
|
40540
|
+
return;
|
|
40541
|
+
}
|
|
40542
|
+
const key = `${track.trackId}>${best.groupId}`;
|
|
40543
|
+
for (const existing of [...this.joinSince.keys()]) if (existing.startsWith(`${track.trackId}>`) && existing !== key) this.joinSince.delete(existing);
|
|
40544
|
+
const since = this.joinSince.get(key) ?? now;
|
|
40545
|
+
this.joinSince.set(key, since);
|
|
40546
|
+
if (now - since < 1500) return;
|
|
40547
|
+
this.absorb(track.trackId, mineId, best.groupId);
|
|
40548
|
+
}
|
|
40549
|
+
absorb(trackId, fromId, intoId) {
|
|
40550
|
+
const into = this.groups.get(intoId);
|
|
40551
|
+
if (!into) return;
|
|
40552
|
+
into.members.add(trackId);
|
|
40553
|
+
this.membership.set(trackId, intoId);
|
|
40554
|
+
this.groups.delete(fromId);
|
|
40555
|
+
this.clearJoinClocks(trackId);
|
|
40556
|
+
this.leaveSince.delete(trackId);
|
|
40557
|
+
}
|
|
40558
|
+
removeFromGroup(trackId) {
|
|
40559
|
+
const groupId = this.membership.get(trackId);
|
|
40560
|
+
this.membership.delete(trackId);
|
|
40561
|
+
this.clearJoinClocks(trackId);
|
|
40562
|
+
this.leaveSince.delete(trackId);
|
|
40563
|
+
if (groupId === void 0) return;
|
|
40564
|
+
const group = this.groups.get(groupId);
|
|
40565
|
+
if (!group) return;
|
|
40566
|
+
group.members.delete(trackId);
|
|
40567
|
+
if (group.members.size === 0) this.groups.delete(groupId);
|
|
40568
|
+
}
|
|
40569
|
+
clearJoinClocks(trackId) {
|
|
40570
|
+
for (const key of [...this.joinSince.keys()]) if (key.startsWith(`${trackId}>`)) this.joinSince.delete(key);
|
|
40571
|
+
}
|
|
40572
|
+
toLive(group) {
|
|
40573
|
+
let lastActiveAt = group.openedAt;
|
|
40574
|
+
for (const id of group.members) {
|
|
40575
|
+
const snap = this.snapshots.get(id);
|
|
40576
|
+
if (snap && snap.lastSeen > lastActiveAt) lastActiveAt = snap.lastSeen;
|
|
40577
|
+
}
|
|
40578
|
+
return {
|
|
40579
|
+
id: group.id,
|
|
40580
|
+
deviceId: group.deviceId,
|
|
40581
|
+
memberTrackIds: [...group.members],
|
|
40582
|
+
openedAt: group.openedAt,
|
|
40583
|
+
lastActiveAt
|
|
40584
|
+
};
|
|
40585
|
+
}
|
|
40586
|
+
};
|
|
40587
|
+
function centroidGap(a, b, now) {
|
|
40588
|
+
const pa = a.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, a.positions[0]);
|
|
40589
|
+
const pb = b.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, b.positions[0]);
|
|
40590
|
+
if (!pa || !pb) return Number.POSITIVE_INFINITY;
|
|
40591
|
+
const ca = bboxCentroid(pa.bbox);
|
|
40592
|
+
const cb = bboxCentroid(pb.bbox);
|
|
40593
|
+
return Math.hypot(ca.x - cb.x, ca.y - cb.y);
|
|
40594
|
+
}
|
|
40595
|
+
//#endregion
|
|
40596
|
+
//#region src/pipeline-analytics/group/group-session.ts
|
|
40597
|
+
/** Same idle as the notification burst (`NC_GROUP_DEFAULT_IDLE_SEC`). */
|
|
40598
|
+
var GROUP_SESSION_IDLE_MS = 3e4;
|
|
40599
|
+
//#endregion
|
|
40600
|
+
//#region src/pipeline-analytics/group/group-live-buffer.ts
|
|
40601
|
+
function toGroupableTrack(track) {
|
|
40602
|
+
return {
|
|
40603
|
+
trackId: track.trackId,
|
|
40604
|
+
deviceId: track.deviceId,
|
|
40605
|
+
className: track.className,
|
|
40606
|
+
firstSeen: track.firstSeen,
|
|
40607
|
+
lastSeen: track.lastSeen,
|
|
40608
|
+
positions: track.positions.map((p) => ({
|
|
40609
|
+
...p,
|
|
40610
|
+
bbox: { ...p.bbox }
|
|
40611
|
+
})),
|
|
40612
|
+
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
40613
|
+
};
|
|
40614
|
+
}
|
|
40615
|
+
var GroupLiveBuffer = class {
|
|
40616
|
+
assigner;
|
|
40617
|
+
primed = /* @__PURE__ */ new Set();
|
|
40618
|
+
constructor(nextId) {
|
|
40619
|
+
this.assigner = new GroupLiveAssigner(nextId);
|
|
40620
|
+
}
|
|
40621
|
+
observe(track, now) {
|
|
40622
|
+
this.assigner.observe(track, now);
|
|
40623
|
+
}
|
|
40624
|
+
groupOf(trackId) {
|
|
40625
|
+
return this.assigner.groupOf(trackId);
|
|
40626
|
+
}
|
|
40627
|
+
snapshot(trackId) {
|
|
40628
|
+
return this.assigner.snapshot(trackId);
|
|
40629
|
+
}
|
|
40630
|
+
liveGroups() {
|
|
40631
|
+
return this.assigner.liveGroups();
|
|
40632
|
+
}
|
|
40633
|
+
idleGroups(now, idleMs = GROUP_SESSION_IDLE_MS) {
|
|
40634
|
+
return this.assigner.idleGroups(now, idleMs);
|
|
40635
|
+
}
|
|
40636
|
+
drop(ids) {
|
|
40637
|
+
this.assigner.drop(ids);
|
|
40638
|
+
}
|
|
40639
|
+
isPrimed(deviceId) {
|
|
40640
|
+
return this.primed.has(deviceId);
|
|
40641
|
+
}
|
|
40642
|
+
markPrimed(deviceId) {
|
|
40643
|
+
this.primed.add(deviceId);
|
|
40644
|
+
}
|
|
40645
|
+
};
|
|
40646
|
+
//#endregion
|
|
40647
|
+
//#region src/pipeline-analytics/group/group-crop.ts
|
|
40648
|
+
/** Always containSubject: true. Never the 0.5 delivered threshold. */
|
|
40649
|
+
function groupWideCentralSquareLayout(unionBbox, frame) {
|
|
40650
|
+
return wideCentralSquareLayout(unionBbox, frame, { containSubject: true });
|
|
40651
|
+
}
|
|
40652
|
+
async function cropGroupFromRgb(rgb, frameWidth, frameHeight, unionBbox) {
|
|
40653
|
+
const layout = groupWideCentralSquareLayout(unionBbox, {
|
|
40654
|
+
W: frameWidth,
|
|
40655
|
+
H: frameHeight
|
|
40656
|
+
});
|
|
40657
|
+
const { crop: slab } = await extractCrop(rgb, frameWidth, frameHeight, layout.fetch);
|
|
40658
|
+
return composeWideCentralSquareThumbnail(slab, layout);
|
|
40559
40659
|
}
|
|
40560
40660
|
//#endregion
|
|
40561
40661
|
//#region src/pipeline-analytics/group/group-instant.ts
|
|
@@ -40572,6 +40672,7 @@ function clusterGroups(tracks) {
|
|
|
40572
40672
|
*
|
|
40573
40673
|
* Retrospective. No new capture path.
|
|
40574
40674
|
*/
|
|
40675
|
+
var GROUP_MEDIA_MATCH_MS = 2e3;
|
|
40575
40676
|
function nearest(positions, t, windowMs) {
|
|
40576
40677
|
let best;
|
|
40577
40678
|
let bestDt = windowMs + 1;
|
|
@@ -40680,55 +40781,85 @@ function chooseAnchorTrackId(members, ownsCleanFrame) {
|
|
|
40680
40781
|
return life !== 0 ? life : a.trackId.localeCompare(b.trackId);
|
|
40681
40782
|
})[0].trackId;
|
|
40682
40783
|
}
|
|
40683
|
-
//#endregion
|
|
40684
|
-
//#region src/pipeline-analytics/group/group-session.ts
|
|
40685
40784
|
/**
|
|
40686
|
-
*
|
|
40785
|
+
* The RGB source for a group crop is a full native frame. Thumbnails are
|
|
40786
|
+
* already single-subject crops and cannot substitute for union-bbox geometry.
|
|
40687
40787
|
*
|
|
40688
|
-
*
|
|
40689
|
-
*
|
|
40690
|
-
*
|
|
40691
|
-
*
|
|
40692
|
-
* Merge and split cannot occur: connected components of a closed session is a
|
|
40693
|
-
* partition. Say it out loud so a later live-group attempt does not reach
|
|
40694
|
-
* for them here.
|
|
40788
|
+
* Prefer a keyFrame within {@link GROUP_MEDIA_MATCH_MS} of the chosen instant;
|
|
40789
|
+
* if none, take the nearest keyFrame anyway — dropping the crop because the
|
|
40790
|
+
* best-shot was 3 s off the instant is how a recognised person left the
|
|
40791
|
+
* digest with no crop.
|
|
40695
40792
|
*/
|
|
40696
|
-
|
|
40697
|
-
|
|
40698
|
-
|
|
40699
|
-
|
|
40793
|
+
function pickCleanPixelRow(rows, instantMs) {
|
|
40794
|
+
const ranked = (maxDt) => {
|
|
40795
|
+
const candidates = [];
|
|
40796
|
+
for (const row of rows) {
|
|
40797
|
+
const rank = row.kind === "keyFrame" ? 0 : row.kind === "keyFrameSmall" ? 1 : -1;
|
|
40798
|
+
if (rank < 0) continue;
|
|
40799
|
+
const dt = Math.abs(row.timestamp - instantMs);
|
|
40800
|
+
if (maxDt !== null && dt > maxDt) continue;
|
|
40801
|
+
candidates.push({
|
|
40802
|
+
key: row.key,
|
|
40803
|
+
dt,
|
|
40804
|
+
rank
|
|
40805
|
+
});
|
|
40806
|
+
}
|
|
40807
|
+
return candidates.toSorted((a, b) => a.rank - b.rank || a.dt - b.dt)[0];
|
|
40808
|
+
};
|
|
40809
|
+
const near = ranked(GROUP_MEDIA_MATCH_MS);
|
|
40810
|
+
if (near !== void 0) return {
|
|
40811
|
+
key: near.key,
|
|
40812
|
+
usedFallback: false
|
|
40813
|
+
};
|
|
40814
|
+
const any = ranked(null);
|
|
40815
|
+
if (any === void 0) return null;
|
|
40816
|
+
return {
|
|
40817
|
+
key: any.key,
|
|
40818
|
+
usedFallback: true
|
|
40819
|
+
};
|
|
40700
40820
|
}
|
|
40701
|
-
|
|
40821
|
+
//#endregion
|
|
40822
|
+
//#region src/pipeline-analytics/group/group-live-replay.ts
|
|
40823
|
+
function sliceTrackAt(track, now) {
|
|
40824
|
+
const positions = track.positions.filter((p) => p.timestamp <= now);
|
|
40825
|
+
const last = positions[positions.length - 1];
|
|
40702
40826
|
return {
|
|
40703
|
-
|
|
40704
|
-
lastSeen:
|
|
40827
|
+
...track,
|
|
40828
|
+
lastSeen: last?.timestamp ?? track.firstSeen,
|
|
40829
|
+
positions
|
|
40705
40830
|
};
|
|
40706
40831
|
}
|
|
40707
|
-
|
|
40708
|
-
|
|
40709
|
-
|
|
40710
|
-
|
|
40711
|
-
|
|
40712
|
-
|
|
40713
|
-
|
|
40714
|
-
|
|
40715
|
-
|
|
40716
|
-
|
|
40717
|
-
|
|
40718
|
-
const ready = [];
|
|
40719
|
-
for (const [deviceId, tracks] of byDevice) {
|
|
40720
|
-
const deviceActive = active.filter((a) => a.deviceId === deviceId);
|
|
40721
|
-
for (const session of sessionsOf(tracks)) {
|
|
40722
|
-
const span = sessionSpan(session);
|
|
40723
|
-
if (now < span.lastSeen + idleMs) continue;
|
|
40724
|
-
if (deviceActive.some((a) => overlaps(span, a))) continue;
|
|
40725
|
-
ready.push(session);
|
|
40832
|
+
function replayInto(tracks, observer) {
|
|
40833
|
+
const events = [];
|
|
40834
|
+
const byId = /* @__PURE__ */ new Map();
|
|
40835
|
+
for (const track of tracks) {
|
|
40836
|
+
byId.set(track.trackId, track);
|
|
40837
|
+
if (track.positions.length === 0) {
|
|
40838
|
+
events.push({
|
|
40839
|
+
t: track.lastSeen,
|
|
40840
|
+
trackId: track.trackId
|
|
40841
|
+
});
|
|
40842
|
+
continue;
|
|
40726
40843
|
}
|
|
40844
|
+
for (const p of track.positions) events.push({
|
|
40845
|
+
t: p.timestamp,
|
|
40846
|
+
trackId: track.trackId
|
|
40847
|
+
});
|
|
40848
|
+
}
|
|
40849
|
+
events.sort((a, b) => a.t - b.t || a.trackId.localeCompare(b.trackId));
|
|
40850
|
+
let lastKey = "";
|
|
40851
|
+
for (const e of events) {
|
|
40852
|
+
const key = `${e.trackId}@${e.t}`;
|
|
40853
|
+
if (key === lastKey) continue;
|
|
40854
|
+
lastKey = key;
|
|
40855
|
+
const track = byId.get(e.trackId);
|
|
40856
|
+
if (!track) continue;
|
|
40857
|
+
observer.observe(sliceTrackAt(track, e.t), e.t);
|
|
40727
40858
|
}
|
|
40728
|
-
return ready;
|
|
40729
40859
|
}
|
|
40730
|
-
function
|
|
40731
|
-
|
|
40860
|
+
function replayLive(tracks, assigner = new GroupLiveAssigner()) {
|
|
40861
|
+
replayInto(tracks, assigner);
|
|
40862
|
+
return assigner;
|
|
40732
40863
|
}
|
|
40733
40864
|
//#endregion
|
|
40734
40865
|
//#region src/pipeline-analytics/group/group-sealer.ts
|
|
@@ -40736,9 +40867,9 @@ var DEFAULT_FRAME = {
|
|
|
40736
40867
|
width: 640,
|
|
40737
40868
|
height: 360
|
|
40738
40869
|
};
|
|
40739
|
-
var SEAL_LOOKBACK_MS = 10 * 6e4;
|
|
40740
|
-
var BACKFILL_LOOKBACK_MS = 2880 * 6e4;
|
|
40741
40870
|
var QUERY_LIMIT = 2e3;
|
|
40871
|
+
/** Crash recover: replay this camera only, on its next activity. */
|
|
40872
|
+
var GROUP_CRASH_LOOKBACK_MS = 2 * 6e4;
|
|
40742
40873
|
function toInstant(t) {
|
|
40743
40874
|
return {
|
|
40744
40875
|
trackId: t.trackId,
|
|
@@ -40752,93 +40883,164 @@ function toInstant(t) {
|
|
|
40752
40883
|
confidence: t.importance
|
|
40753
40884
|
};
|
|
40754
40885
|
}
|
|
40886
|
+
function memberBoxesAt(members, at, frame) {
|
|
40887
|
+
const w = Math.max(frame.width, 1);
|
|
40888
|
+
const h = Math.max(frame.height, 1);
|
|
40889
|
+
const boxes = [];
|
|
40890
|
+
for (const member of members) {
|
|
40891
|
+
let best = member.positions[0];
|
|
40892
|
+
let bestDt = Number.POSITIVE_INFINITY;
|
|
40893
|
+
for (const p of member.positions) {
|
|
40894
|
+
const dt = Math.abs(p.timestamp - at);
|
|
40895
|
+
if (dt <= 1e3 && dt < bestDt) {
|
|
40896
|
+
best = p;
|
|
40897
|
+
bestDt = dt;
|
|
40898
|
+
}
|
|
40899
|
+
}
|
|
40900
|
+
if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
|
|
40901
|
+
boxes.push({
|
|
40902
|
+
bbox: {
|
|
40903
|
+
x: best.bbox.x / w,
|
|
40904
|
+
y: best.bbox.y / h,
|
|
40905
|
+
w: best.bbox.w / w,
|
|
40906
|
+
h: best.bbox.h / h
|
|
40907
|
+
},
|
|
40908
|
+
label: member.className,
|
|
40909
|
+
observedAt: best.timestamp
|
|
40910
|
+
});
|
|
40911
|
+
}
|
|
40912
|
+
return boxes;
|
|
40913
|
+
}
|
|
40914
|
+
function toSealer(track) {
|
|
40915
|
+
return {
|
|
40916
|
+
trackId: track.trackId,
|
|
40917
|
+
deviceId: track.deviceId,
|
|
40918
|
+
className: track.className,
|
|
40919
|
+
firstSeen: track.firstSeen,
|
|
40920
|
+
lastSeen: track.lastSeen,
|
|
40921
|
+
positions: track.positions,
|
|
40922
|
+
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
40923
|
+
};
|
|
40924
|
+
}
|
|
40755
40925
|
var GroupSealer = class {
|
|
40756
40926
|
deps;
|
|
40757
40927
|
constructor(deps) {
|
|
40758
40928
|
this.deps = deps;
|
|
40759
40929
|
}
|
|
40760
|
-
|
|
40761
|
-
|
|
40762
|
-
return this.sealSince(now - SEAL_LOOKBACK_MS, now);
|
|
40763
|
-
}
|
|
40764
|
-
async backfill() {
|
|
40765
|
-
const now = this.deps.now();
|
|
40766
|
-
return this.sealSince(now - BACKFILL_LOOKBACK_MS, now);
|
|
40930
|
+
get processEnabled() {
|
|
40931
|
+
return this.deps.enabled ?? false;
|
|
40767
40932
|
}
|
|
40768
|
-
async
|
|
40933
|
+
async flushIdle(buffer, now = this.deps.now()) {
|
|
40934
|
+
if (!this.processEnabled) return 0;
|
|
40769
40935
|
let written = 0;
|
|
40770
|
-
|
|
40936
|
+
for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
|
|
40937
|
+
buffer.drop([group.id]);
|
|
40938
|
+
written += 1;
|
|
40939
|
+
}
|
|
40940
|
+
return written;
|
|
40941
|
+
}
|
|
40942
|
+
/**
|
|
40943
|
+
* First activity on camera D after a crash: replay 2 minutes of that camera
|
|
40944
|
+
* into the live buffer, persist whoever is already idle, keep the rest in RAM.
|
|
40945
|
+
*/
|
|
40946
|
+
async recoverCamera(deviceId, buffer) {
|
|
40947
|
+
if (!this.processEnabled) return 0;
|
|
40948
|
+
if (buffer.isPrimed(deviceId)) return 0;
|
|
40949
|
+
const now = this.deps.now();
|
|
40771
40950
|
try {
|
|
40772
|
-
|
|
40951
|
+
replayInto(await this.deps.trackStore.queryHistorical({
|
|
40952
|
+
deviceId,
|
|
40953
|
+
since: now - GROUP_CRASH_LOOKBACK_MS,
|
|
40954
|
+
until: now,
|
|
40955
|
+
limit: QUERY_LIMIT,
|
|
40956
|
+
projection: "full"
|
|
40957
|
+
}), buffer);
|
|
40773
40958
|
} catch (err) {
|
|
40774
|
-
this.deps.logger.warn("group sealer:
|
|
40775
|
-
|
|
40959
|
+
this.deps.logger.warn("group sealer: lookback failed", {
|
|
40960
|
+
tags: { deviceId },
|
|
40961
|
+
meta: { error: String(err) }
|
|
40962
|
+
});
|
|
40963
|
+
} finally {
|
|
40964
|
+
buffer.markPrimed(deviceId);
|
|
40776
40965
|
}
|
|
40777
|
-
|
|
40778
|
-
|
|
40966
|
+
return this.flushIdle(buffer, now);
|
|
40967
|
+
}
|
|
40968
|
+
/**
|
|
40969
|
+
* Seal ONE camera over a caller-owned window via live replay.
|
|
40970
|
+
*
|
|
40971
|
+
* This is how a digest recovers groups without a boot scan: the producer
|
|
40972
|
+
* already knows the window. Walking every camera for 48h at initialize()
|
|
40973
|
+
* is what wedged hub-main.
|
|
40974
|
+
*/
|
|
40975
|
+
async sealWindow(deviceId, since, until) {
|
|
40976
|
+
if (!this.processEnabled) return 0;
|
|
40977
|
+
try {
|
|
40978
|
+
const closed = await this.deps.trackStore.queryHistorical({
|
|
40979
|
+
deviceId,
|
|
40980
|
+
since,
|
|
40981
|
+
until,
|
|
40982
|
+
limit: QUERY_LIMIT,
|
|
40983
|
+
projection: "full"
|
|
40984
|
+
});
|
|
40985
|
+
if (closed.length === 0) return 0;
|
|
40986
|
+
const already = new Set((await this.deps.groupStore.membersOfTracks(closed.map((t) => t.trackId))).map((m) => m.trackId));
|
|
40987
|
+
const pending = closed.filter((t) => !already.has(t.trackId));
|
|
40988
|
+
if (pending.length === 0) return 0;
|
|
40989
|
+
const assigner = replayLive(pending);
|
|
40990
|
+
const now = this.deps.now();
|
|
40991
|
+
const byId = new Map(pending.map((t) => [t.trackId, t]));
|
|
40992
|
+
let written = 0;
|
|
40993
|
+
for (const group of assigner.idleGroups(now, GROUP_SESSION_IDLE_MS)) {
|
|
40994
|
+
const members = group.memberTrackIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
40995
|
+
if (members.length === 0) continue;
|
|
40996
|
+
try {
|
|
40997
|
+
await this.persistPartition(members, group.openedAt);
|
|
40998
|
+
written += 1;
|
|
40999
|
+
} catch (err) {
|
|
41000
|
+
this.deps.logger.warn("group sealer: persist failed", {
|
|
41001
|
+
tags: { deviceId },
|
|
41002
|
+
meta: {
|
|
41003
|
+
members: members.map((m) => m.trackId),
|
|
41004
|
+
error: String(err)
|
|
41005
|
+
}
|
|
41006
|
+
});
|
|
41007
|
+
}
|
|
41008
|
+
}
|
|
41009
|
+
return written;
|
|
40779
41010
|
} catch (err) {
|
|
40780
41011
|
this.deps.logger.warn("group sealer: device failed", {
|
|
40781
41012
|
tags: { deviceId },
|
|
40782
41013
|
meta: { error: String(err) }
|
|
40783
41014
|
});
|
|
41015
|
+
return 0;
|
|
40784
41016
|
}
|
|
40785
|
-
return written;
|
|
40786
41017
|
}
|
|
40787
|
-
async
|
|
40788
|
-
const
|
|
40789
|
-
|
|
40790
|
-
|
|
40791
|
-
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
40795
|
-
|
|
40796
|
-
|
|
40797
|
-
|
|
40798
|
-
|
|
40799
|
-
const active = this.deps.trackStore.getActive(deviceId).map((t) => ({
|
|
40800
|
-
deviceId,
|
|
40801
|
-
firstSeen: t.firstSeen,
|
|
40802
|
-
lastSeen: t.lastSeen
|
|
40803
|
-
}));
|
|
40804
|
-
const partitions = partitionsForSealedSessions(pending.map(toInstant), active, now, GROUP_SESSION_IDLE_MS);
|
|
40805
|
-
if (partitions.length === 0) {
|
|
40806
|
-
if (pending.filter((t) => t.closedReason !== "stationary-promotion" && t.className !== "audio").length === 0 && pending.length > 0) this.deps.logger.info("group session produced no groups", {
|
|
41018
|
+
async persistLiveGroup(group, buffer) {
|
|
41019
|
+
const deviceId = group.deviceId;
|
|
41020
|
+
const snaps = group.memberTrackIds.map((id) => buffer.snapshot(id)).filter((t) => t !== void 0);
|
|
41021
|
+
if (snaps.length === 0) return true;
|
|
41022
|
+
const already = new Set((await this.deps.groupStore.membersOfTracks(snaps.map((t) => t.trackId))).map((m) => m.trackId));
|
|
41023
|
+
const pending = snaps.filter((t) => !already.has(t.trackId)).map(toSealer);
|
|
41024
|
+
if (pending.length === 0) return true;
|
|
41025
|
+
try {
|
|
41026
|
+
await this.persistPartition(pending, group.openedAt);
|
|
41027
|
+
return true;
|
|
41028
|
+
} catch (err) {
|
|
41029
|
+
this.deps.logger.warn("group sealer: persist failed", {
|
|
40807
41030
|
tags: { deviceId },
|
|
40808
41031
|
meta: {
|
|
40809
|
-
|
|
40810
|
-
|
|
41032
|
+
members: pending.map((m) => m.trackId),
|
|
41033
|
+
error: String(err)
|
|
40811
41034
|
}
|
|
40812
41035
|
});
|
|
40813
|
-
return
|
|
40814
|
-
}
|
|
40815
|
-
const byId = new Map(pending.map((t) => [t.trackId, t]));
|
|
40816
|
-
let written = 0;
|
|
40817
|
-
for (const part of partitions) {
|
|
40818
|
-
const members = part.memberTrackIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
40819
|
-
if (members.length === 0) continue;
|
|
40820
|
-
if (members.every((m) => already.has(m.trackId))) continue;
|
|
40821
|
-
try {
|
|
40822
|
-
await this.persistPartition(members);
|
|
40823
|
-
written += 1;
|
|
40824
|
-
} catch (err) {
|
|
40825
|
-
this.deps.logger.warn("group sealer: persist failed", {
|
|
40826
|
-
tags: { deviceId },
|
|
40827
|
-
meta: {
|
|
40828
|
-
members: members.map((m) => m.trackId),
|
|
40829
|
-
error: String(err)
|
|
40830
|
-
}
|
|
40831
|
-
});
|
|
40832
|
-
}
|
|
41036
|
+
return false;
|
|
40833
41037
|
}
|
|
40834
|
-
return written;
|
|
40835
41038
|
}
|
|
40836
|
-
async persistPartition(members) {
|
|
41039
|
+
async persistPartition(members, openedAt) {
|
|
40837
41040
|
const deviceId = members[0].deviceId;
|
|
40838
41041
|
const frame = this.deps.frameDims(deviceId) ?? DEFAULT_FRAME;
|
|
40839
41042
|
const instantMembers = members.map(toInstant);
|
|
40840
41043
|
const instant = chooseGroupInstant(instantMembers, frame);
|
|
40841
|
-
const openedAt = Math.min(...members.map((m) => m.firstSeen));
|
|
40842
41044
|
const closedAt = Math.max(...members.map((m) => m.lastSeen));
|
|
40843
41045
|
const timestamp = instant?.timestamp ?? closedAt;
|
|
40844
41046
|
const mediaInfos = await Promise.all(members.map(async (m) => ({
|
|
@@ -40874,29 +41076,66 @@ var GroupSealer = class {
|
|
|
40874
41076
|
nativeKey
|
|
40875
41077
|
});
|
|
40876
41078
|
if (members.length < 2 || instant === null) return;
|
|
40877
|
-
const source = await this.pickCleanPixels(mediaInfos, timestamp);
|
|
40878
|
-
if (source === null)
|
|
41079
|
+
const source = await this.pickCleanPixels(mediaInfos, timestamp, deviceId);
|
|
41080
|
+
if (source === null) {
|
|
41081
|
+
this.deps.logger.info("group sealer: no clean pixels for crop", {
|
|
41082
|
+
tags: { deviceId },
|
|
41083
|
+
meta: {
|
|
41084
|
+
groupId: draft.id,
|
|
41085
|
+
members: members.map((m) => m.trackId)
|
|
41086
|
+
}
|
|
41087
|
+
});
|
|
41088
|
+
return;
|
|
41089
|
+
}
|
|
40879
41090
|
try {
|
|
40880
|
-
const raw = await (0, sharp.default)(source.jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
40881
|
-
if (raw.info.channels
|
|
40882
|
-
|
|
40883
|
-
|
|
40884
|
-
|
|
40885
|
-
|
|
40886
|
-
|
|
40887
|
-
|
|
40888
|
-
|
|
40889
|
-
|
|
41091
|
+
const raw = await (0, sharp.default)(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
|
|
41092
|
+
if (raw.info.channels === 3) {
|
|
41093
|
+
const union = {
|
|
41094
|
+
x: instant.ux * raw.info.width,
|
|
41095
|
+
y: instant.uy * raw.info.height,
|
|
41096
|
+
w: instant.uw * raw.info.width,
|
|
41097
|
+
h: instant.uh * raw.info.height
|
|
41098
|
+
};
|
|
41099
|
+
const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
|
|
41100
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
41101
|
+
deviceId,
|
|
41102
|
+
ownerKind: "group",
|
|
41103
|
+
ownerId: draft.id,
|
|
41104
|
+
kind: "thumbnail",
|
|
41105
|
+
timestamp,
|
|
41106
|
+
data: crop
|
|
41107
|
+
});
|
|
41108
|
+
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
41109
|
+
}
|
|
41110
|
+
} catch (err) {
|
|
41111
|
+
this.deps.logger.warn("group sealer: crop failed", {
|
|
41112
|
+
tags: { deviceId },
|
|
41113
|
+
meta: {
|
|
41114
|
+
groupId: draft.id,
|
|
41115
|
+
error: String(err)
|
|
41116
|
+
}
|
|
41117
|
+
});
|
|
41118
|
+
}
|
|
41119
|
+
try {
|
|
41120
|
+
const composed = await composeGroupFrame({
|
|
41121
|
+
jpeg: source.jpeg,
|
|
41122
|
+
boxes: memberBoxesAt(instantMembers, timestamp, frame),
|
|
41123
|
+
at: timestamp,
|
|
41124
|
+
logger: this.deps.logger,
|
|
41125
|
+
deviceId,
|
|
41126
|
+
groupKey: draft.id
|
|
41127
|
+
});
|
|
41128
|
+
if (composed === null) return;
|
|
41129
|
+
await this.deps.mediaStore.put({
|
|
40890
41130
|
deviceId,
|
|
40891
41131
|
ownerKind: "group",
|
|
40892
41132
|
ownerId: draft.id,
|
|
40893
|
-
kind: "
|
|
41133
|
+
kind: "fullFrameBoxed",
|
|
40894
41134
|
timestamp,
|
|
40895
|
-
data:
|
|
41135
|
+
data: composed.bytes
|
|
40896
41136
|
});
|
|
40897
|
-
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
40898
41137
|
} catch (err) {
|
|
40899
|
-
this.deps.logger.warn("group sealer:
|
|
41138
|
+
this.deps.logger.warn("group sealer: boxed frame failed", {
|
|
40900
41139
|
tags: { deviceId },
|
|
40901
41140
|
meta: {
|
|
40902
41141
|
groupId: draft.id,
|
|
@@ -40905,27 +41144,53 @@ var GroupSealer = class {
|
|
|
40905
41144
|
});
|
|
40906
41145
|
}
|
|
40907
41146
|
}
|
|
40908
|
-
async pickCleanPixels(mediaInfos, instantMs) {
|
|
40909
|
-
const
|
|
40910
|
-
|
|
40911
|
-
|
|
40912
|
-
|
|
40913
|
-
|
|
40914
|
-
|
|
40915
|
-
|
|
40916
|
-
|
|
40917
|
-
|
|
40918
|
-
rank
|
|
40919
|
-
});
|
|
40920
|
-
}
|
|
40921
|
-
const chosen = candidates.toSorted((a, b) => a.rank - b.rank || a.dt - b.dt)[0];
|
|
40922
|
-
if (chosen === void 0) return null;
|
|
41147
|
+
async pickCleanPixels(mediaInfos, instantMs, deviceId) {
|
|
41148
|
+
const chosen = pickCleanPixelRow(mediaInfos.flatMap((info) => info.rows), instantMs);
|
|
41149
|
+
if (chosen === null) return null;
|
|
41150
|
+
if (chosen.usedFallback) this.deps.logger.info("group sealer: crop pixels not within 2s of instant, using nearest keyFrame", {
|
|
41151
|
+
tags: { deviceId },
|
|
41152
|
+
meta: {
|
|
41153
|
+
instantMs,
|
|
41154
|
+
mediaKey: chosen.key
|
|
41155
|
+
}
|
|
41156
|
+
});
|
|
40923
41157
|
const file = await this.deps.mediaStore.getByKey(chosen.key);
|
|
40924
41158
|
if (file === null) return null;
|
|
40925
41159
|
return { jpeg: Buffer.from(file.base64, "base64") };
|
|
40926
41160
|
}
|
|
40927
41161
|
};
|
|
40928
41162
|
//#endregion
|
|
41163
|
+
//#region src/pipeline-analytics/group/group-cursor.ts
|
|
41164
|
+
function encodeGroupCursor(cursor) {
|
|
41165
|
+
return Buffer.from(JSON.stringify({
|
|
41166
|
+
c: cursor.closedAt,
|
|
41167
|
+
i: cursor.groupId
|
|
41168
|
+
}), "utf8").toString("base64url");
|
|
41169
|
+
}
|
|
41170
|
+
function decodeGroupCursor(raw) {
|
|
41171
|
+
let parsed;
|
|
41172
|
+
try {
|
|
41173
|
+
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
41174
|
+
} catch {
|
|
41175
|
+
throw new Error("listGroups: malformed cursor");
|
|
41176
|
+
}
|
|
41177
|
+
if (parsed === null || typeof parsed !== "object" || !("c" in parsed) || !("i" in parsed) || typeof parsed.c !== "number" || !Number.isFinite(parsed.c) || typeof parsed.i !== "string" || parsed.i.length === 0) throw new Error("listGroups: malformed cursor");
|
|
41178
|
+
return {
|
|
41179
|
+
closedAt: parsed.c,
|
|
41180
|
+
groupId: parsed.i
|
|
41181
|
+
};
|
|
41182
|
+
}
|
|
41183
|
+
function compareGroupDesc(a, b) {
|
|
41184
|
+
if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
|
|
41185
|
+
if (a.id === b.id) return 0;
|
|
41186
|
+
return a.id < b.id ? 1 : -1;
|
|
41187
|
+
}
|
|
41188
|
+
/** True when `row` sits strictly AFTER the cursor in DESC order. */
|
|
41189
|
+
function isAfterGroupCursor(row, cursor) {
|
|
41190
|
+
if (row.closedAt < cursor.closedAt) return true;
|
|
41191
|
+
return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
|
|
41192
|
+
}
|
|
41193
|
+
//#endregion
|
|
40929
41194
|
//#region src/pipeline-analytics/group/group-store.ts
|
|
40930
41195
|
/**
|
|
40931
41196
|
* The group ENTITY store — a co-moving partition of one session, with one
|
|
@@ -40947,8 +41212,8 @@ var GroupSealer = class {
|
|
|
40947
41212
|
*/
|
|
40948
41213
|
/**
|
|
40949
41214
|
* @durable class=ledger owner=pipeline-analytics
|
|
40950
|
-
* write="one row per co-moving group at
|
|
40951
|
-
* Membership is
|
|
41215
|
+
* write="one row per co-moving group, sealed at idle — never a live row.
|
|
41216
|
+
* Membership is the RAM members at persist, not a session partition. mediaKey is
|
|
40952
41217
|
* the group's own crop; nativeKey is a POINTER to the anchor member's
|
|
40953
41218
|
* keyFrame, nulled if that member is deleted alone. anchorTrackId and
|
|
40954
41219
|
* nativeKey are the only fields rewritten after birth."
|
|
@@ -41187,6 +41452,24 @@ var TrackGroupStore = class {
|
|
|
41187
41452
|
data: r.data
|
|
41188
41453
|
}));
|
|
41189
41454
|
}
|
|
41455
|
+
/**
|
|
41456
|
+
* Membership for a page of groups in ONE query. `listGroupRecords` used to
|
|
41457
|
+
* call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
|
|
41458
|
+
* ~20 s, and the viewer feed times out into a retry loop.
|
|
41459
|
+
*/
|
|
41460
|
+
async listMembersForGroups(groupIds) {
|
|
41461
|
+
if (groupIds.length === 0) return [];
|
|
41462
|
+
return (await this.store.query.query({
|
|
41463
|
+
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
41464
|
+
filter: {
|
|
41465
|
+
whereIn: { groupId: [...groupIds] },
|
|
41466
|
+
limit: 1e4
|
|
41467
|
+
}
|
|
41468
|
+
})).map((r) => this.normalizeMember({
|
|
41469
|
+
id: r.id,
|
|
41470
|
+
data: r.data
|
|
41471
|
+
}));
|
|
41472
|
+
}
|
|
41190
41473
|
async membersOfTracks(trackIds) {
|
|
41191
41474
|
const out = [];
|
|
41192
41475
|
for (const trackId of trackIds) {
|
|
@@ -41202,20 +41485,29 @@ var TrackGroupStore = class {
|
|
|
41202
41485
|
return out;
|
|
41203
41486
|
}
|
|
41204
41487
|
/**
|
|
41205
|
-
* Newest-closed page for the
|
|
41488
|
+
* Newest-closed page for the Groups feed.
|
|
41206
41489
|
*
|
|
41207
41490
|
* Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
|
|
41208
41491
|
* same as {@link listOverlapping}. Pagination is applied AFTER the window
|
|
41209
41492
|
* so a day cursor is honest rather than a raw-row offset that skips live
|
|
41210
|
-
* groups on a busy camera.
|
|
41493
|
+
* groups on a busy camera. `deviceIds: []` is an empty page (same as
|
|
41494
|
+
* `listRecentTracks`); omit the field for every camera. `deviceId` stays
|
|
41495
|
+
* as the single-camera alias the custom-action bridge still sends.
|
|
41211
41496
|
*/
|
|
41212
41497
|
async listRecent(input) {
|
|
41213
41498
|
const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
|
|
41214
41499
|
const offset = Math.max(input.offset ?? 0, 0);
|
|
41215
|
-
const
|
|
41500
|
+
const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
|
|
41501
|
+
if (deviceIds !== void 0 && deviceIds.length === 0) return {
|
|
41502
|
+
groups: [],
|
|
41503
|
+
hasMore: false,
|
|
41504
|
+
nextCursor: null
|
|
41505
|
+
};
|
|
41506
|
+
const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
|
|
41507
|
+
const filtered = (await this.store.query.query({
|
|
41216
41508
|
collection: TRACK_GROUPS_COLLECTION,
|
|
41217
41509
|
filter: {
|
|
41218
|
-
...
|
|
41510
|
+
...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
|
|
41219
41511
|
orderBy: {
|
|
41220
41512
|
field: "closedAt",
|
|
41221
41513
|
direction: "desc"
|
|
@@ -41228,11 +41520,20 @@ var TrackGroupStore = class {
|
|
|
41228
41520
|
})).filter((g) => {
|
|
41229
41521
|
if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
|
|
41230
41522
|
if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
|
|
41523
|
+
if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
|
|
41231
41524
|
return true;
|
|
41232
|
-
}).
|
|
41525
|
+
}).toSorted(compareGroupDesc);
|
|
41526
|
+
const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
|
|
41527
|
+
const groups = slice.slice(0, limit);
|
|
41528
|
+
const hasMore = slice.length > limit;
|
|
41529
|
+
const last = groups[groups.length - 1];
|
|
41233
41530
|
return {
|
|
41234
|
-
groups
|
|
41235
|
-
hasMore
|
|
41531
|
+
groups,
|
|
41532
|
+
hasMore,
|
|
41533
|
+
nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
|
|
41534
|
+
closedAt: last.closedAt,
|
|
41535
|
+
groupId: last.id
|
|
41536
|
+
}) : null
|
|
41236
41537
|
};
|
|
41237
41538
|
}
|
|
41238
41539
|
/** Groups whose life overlaps `[since, until]` on one camera. */
|
|
@@ -42564,7 +42865,7 @@ function evaluateStationaryPromotion(input) {
|
|
|
42564
42865
|
};
|
|
42565
42866
|
}
|
|
42566
42867
|
var DEFAULT_PACKAGE_STILLNESS_CONFIG = {
|
|
42567
|
-
|
|
42868
|
+
minHits: 3,
|
|
42568
42869
|
roundGapMs: 5e3,
|
|
42569
42870
|
ttlMs: 5 * 6e4,
|
|
42570
42871
|
assocIou: .3
|
|
@@ -42597,8 +42898,9 @@ var PackageStillnessTracker = class {
|
|
|
42597
42898
|
const prev = next[index];
|
|
42598
42899
|
if (prev === void 0) continue;
|
|
42599
42900
|
consumed.add(prev.id);
|
|
42600
|
-
const updated = confirm(prev, input, s
|
|
42601
|
-
|
|
42901
|
+
const updated = confirm(prev, input, s);
|
|
42902
|
+
const minHits = input.minHits ?? this.config.minHits;
|
|
42903
|
+
if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
|
|
42602
42904
|
next[index] = updated;
|
|
42603
42905
|
continue;
|
|
42604
42906
|
}
|
|
@@ -42622,7 +42924,7 @@ var PackageStillnessTracker = class {
|
|
|
42622
42924
|
id: c.id,
|
|
42623
42925
|
className: c.className,
|
|
42624
42926
|
bbox: { ...c.bbox },
|
|
42625
|
-
|
|
42927
|
+
hits: c.hits,
|
|
42626
42928
|
firstSeenAt: c.firstSeenAt,
|
|
42627
42929
|
lastSeenAt: c.lastSeenAt,
|
|
42628
42930
|
activityBackedAtOpen: c.activityBackedAtOpen
|
|
@@ -42680,7 +42982,7 @@ var PackageStillnessTracker = class {
|
|
|
42680
42982
|
frameHeight: input.frameHeight,
|
|
42681
42983
|
firstSeenAt: input.timestamp,
|
|
42682
42984
|
lastSeenAt: input.timestamp,
|
|
42683
|
-
|
|
42985
|
+
hits: 1,
|
|
42684
42986
|
promotedAt: null,
|
|
42685
42987
|
activityBackedAtOpen,
|
|
42686
42988
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
@@ -42690,18 +42992,16 @@ var PackageStillnessTracker = class {
|
|
|
42690
42992
|
};
|
|
42691
42993
|
}
|
|
42692
42994
|
};
|
|
42693
|
-
/** Re-confirm a candidate with this frame's box.
|
|
42694
|
-
*
|
|
42695
|
-
|
|
42696
|
-
function confirm(prev, input, s, roundGapMs) {
|
|
42697
|
-
const newRound = input.timestamp - prev.lastSeenAt >= roundGapMs;
|
|
42995
|
+
/** Re-confirm a candidate with this frame's box. Every sighting is a hit —
|
|
42996
|
+
* a settle burst of three frames in one second is three confirmations. */
|
|
42997
|
+
function confirm(prev, input, s) {
|
|
42698
42998
|
return {
|
|
42699
42999
|
...prev,
|
|
42700
43000
|
bbox: { ...s.bbox },
|
|
42701
43001
|
frameWidth: input.frameWidth,
|
|
42702
43002
|
frameHeight: input.frameHeight,
|
|
42703
43003
|
lastSeenAt: input.timestamp,
|
|
42704
|
-
|
|
43004
|
+
hits: prev.hits + 1,
|
|
42705
43005
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
42706
43006
|
...s.label !== void 0 ? { label: s.label } : {},
|
|
42707
43007
|
...s.confidence !== void 0 ? { confidence: s.confidence } : {},
|
|
@@ -42726,6 +43026,31 @@ function toEntry(c) {
|
|
|
42726
43026
|
};
|
|
42727
43027
|
}
|
|
42728
43028
|
//#endregion
|
|
43029
|
+
//#region src/pipeline-analytics/pipeline/package/package-zone-activity.ts
|
|
43030
|
+
/**
|
|
43031
|
+
* Whether a live track is motion inside a configured package drop-off zone.
|
|
43032
|
+
*
|
|
43033
|
+
* The stillness activity gate used to stamp ANY non-package track on the
|
|
43034
|
+
* camera. A walker at the far gate then "delivered" the streetlight wedge
|
|
43035
|
+
* on the doormat. The operator's prerequisite is motion in the drop-off
|
|
43036
|
+
* zone the package rule points at — the same zones `PackageDropDetector`
|
|
43037
|
+
* uses for the centroid gate.
|
|
43038
|
+
*/
|
|
43039
|
+
function isDelivererInPackageZone(trackZones, packageZoneIds) {
|
|
43040
|
+
if (packageZoneIds.size === 0 || trackZones.length === 0) return false;
|
|
43041
|
+
for (const z of trackZones) if (packageZoneIds.has(z)) return true;
|
|
43042
|
+
return false;
|
|
43043
|
+
}
|
|
43044
|
+
/** Zone ids referenced by enabled `package`-stage rules. Disabled rules do not count. */
|
|
43045
|
+
function collectPackageZoneIds(rules) {
|
|
43046
|
+
const ids = /* @__PURE__ */ new Set();
|
|
43047
|
+
for (const r of rules) {
|
|
43048
|
+
if (r.enabled === false) continue;
|
|
43049
|
+
for (const z of r.zoneIds) ids.add(z);
|
|
43050
|
+
}
|
|
43051
|
+
return ids;
|
|
43052
|
+
}
|
|
43053
|
+
//#endregion
|
|
42729
43054
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-zones.ts
|
|
42730
43055
|
/**
|
|
42731
43056
|
* Zone ids whose 0–1 polygon contains the entry's normalised bbox centroid.
|
|
@@ -49350,10 +49675,21 @@ var TrackStore = class {
|
|
|
49350
49675
|
* in-memory dedup (the query cap has no DISTINCT); best-effort.
|
|
49351
49676
|
*/
|
|
49352
49677
|
async listDeviceIds() {
|
|
49678
|
+
return this.collectDeviceIds(0);
|
|
49679
|
+
}
|
|
49680
|
+
/**
|
|
49681
|
+
* Distinct deviceIds with a persisted track whose `lastSeen` is at least
|
|
49682
|
+
* `since`. The group sealer's 10-minute window. Walking from epoch 0 is how
|
|
49683
|
+
* `sealRecent` wedged hub-main every TTL sweep.
|
|
49684
|
+
*/
|
|
49685
|
+
async listDeviceIdsSince(since) {
|
|
49686
|
+
return this.collectDeviceIds(Math.max(0, Math.floor(since)));
|
|
49687
|
+
}
|
|
49688
|
+
async collectDeviceIds(fromLastSeen) {
|
|
49353
49689
|
const seenDevices = /* @__PURE__ */ new Set();
|
|
49354
49690
|
const seenIds = /* @__PURE__ */ new Set();
|
|
49355
49691
|
const PAGE = 500;
|
|
49356
|
-
let cursor =
|
|
49692
|
+
let cursor = fromLastSeen;
|
|
49357
49693
|
try {
|
|
49358
49694
|
for (;;) {
|
|
49359
49695
|
const rows = await this.store.query.query({
|
|
@@ -49618,6 +49954,28 @@ var TrackStore = class {
|
|
|
49618
49954
|
return this.rowToTrack(row.id, row.data);
|
|
49619
49955
|
}
|
|
49620
49956
|
/**
|
|
49957
|
+
* Batch read of persisted tracks. `listGroupRecords` cannot call
|
|
49958
|
+
* {@link getPersistedByTrackId} per member — that is one settings-store RPC
|
|
49959
|
+
* per row and a 60-group page takes ~20 s, which the viewer feed times out.
|
|
49960
|
+
*/
|
|
49961
|
+
async getPersistedByTrackIds(trackIds) {
|
|
49962
|
+
if (trackIds.length === 0) return [];
|
|
49963
|
+
const PAGE = 500;
|
|
49964
|
+
const out = [];
|
|
49965
|
+
for (let i = 0; i < trackIds.length; i += PAGE) {
|
|
49966
|
+
const chunk = trackIds.slice(i, i + PAGE);
|
|
49967
|
+
const rows = await this.store.query.query({
|
|
49968
|
+
collection: TRACKS_COLLECTION,
|
|
49969
|
+
filter: {
|
|
49970
|
+
whereIn: { id: [...chunk] },
|
|
49971
|
+
limit: chunk.length
|
|
49972
|
+
}
|
|
49973
|
+
});
|
|
49974
|
+
for (const row of rows) out.push(this.rowToTrack(row.id, row.data));
|
|
49975
|
+
}
|
|
49976
|
+
return out;
|
|
49977
|
+
}
|
|
49978
|
+
/**
|
|
49621
49979
|
* Persist a SYNTHETIC track (a sensor/control event projected into the
|
|
49622
49980
|
* unified track store). Unlike `persistCompleted` there is no live in-RAM
|
|
49623
49981
|
* state and no trajectory envelope (synthetic tracks carry no positions) —
|
|
@@ -54690,11 +55048,22 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54690
55048
|
description: "Also emit a package-picked-up event when the delivered package leaves.",
|
|
54691
55049
|
default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
|
|
54692
55050
|
},
|
|
55051
|
+
{
|
|
55052
|
+
type: "slider",
|
|
55053
|
+
key: "packageDropMinFrames",
|
|
55054
|
+
label: "Confirmation frames",
|
|
55055
|
+
description: "How many frames the box must be seen on before it counts as a delivery. One is a flicker; two can still be a passer-by; three is the default bar against false positives. Independent of dwell.",
|
|
55056
|
+
min: 1,
|
|
55057
|
+
max: 10,
|
|
55058
|
+
step: 1,
|
|
55059
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropMinFrames,
|
|
55060
|
+
showValue: true
|
|
55061
|
+
},
|
|
54693
55062
|
{
|
|
54694
55063
|
type: "slider",
|
|
54695
55064
|
key: "packageDropDwellSec",
|
|
54696
55065
|
label: "Minimum dwell",
|
|
54697
|
-
description: "
|
|
55066
|
+
description: "Extra time the package must stay parked after the confirmation frames. 0 (default) means N frames is enough — a courier who drops and leaves in a couple of seconds still delivers. Raise to reject a bag briefly set down.",
|
|
54698
55067
|
min: 0,
|
|
54699
55068
|
max: 300,
|
|
54700
55069
|
step: 5,
|
|
@@ -54728,7 +55097,7 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54728
55097
|
type: "slider",
|
|
54729
55098
|
key: "packageDropActivityWindowSec",
|
|
54730
55099
|
label: "Deliverer window",
|
|
54731
|
-
description: "A delivery only counts if a person or vehicle was
|
|
55100
|
+
description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Motion elsewhere on the camera does not count. 0 disables the requirement.",
|
|
54732
55101
|
min: 0,
|
|
54733
55102
|
max: 1800,
|
|
54734
55103
|
step: 30,
|
|
@@ -56740,6 +57109,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
56740
57109
|
plateStore = null;
|
|
56741
57110
|
groupStore = null;
|
|
56742
57111
|
groupSealer = null;
|
|
57112
|
+
groupLive = null;
|
|
57113
|
+
/** Serialise first-activity lookback per camera, then observe in order. */
|
|
57114
|
+
groupObserveTail = /* @__PURE__ */ new Map();
|
|
56743
57115
|
plateRecognizer = null;
|
|
56744
57116
|
objectEmbeddingStore = null;
|
|
56745
57117
|
/** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
|
|
@@ -57150,11 +57522,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
57150
57522
|
*/
|
|
57151
57523
|
packageStillness = new PackageStillnessTracker({ newId: () => (0, node_crypto.randomUUID)() });
|
|
57152
57524
|
/**
|
|
57153
|
-
* Frame-clock timestamp of the last NON-package track seen
|
|
57154
|
-
*
|
|
57155
|
-
* deliverer").
|
|
57156
|
-
*
|
|
57157
|
-
*
|
|
57525
|
+
* Frame-clock timestamp of the last NON-package track seen IN A PACKAGE
|
|
57526
|
+
* DROP-OFF ZONE per device — the activity mirror behind the package
|
|
57527
|
+
* activity gate ("a parcel needs a deliverer"). A walker at the far gate
|
|
57528
|
+
* does not stamp this. Deliberately not {@link lastTrackActivityMs}: that
|
|
57529
|
+
* map is stamped by ANY live track, including the package-class FP itself,
|
|
57530
|
+
* which would let the streetlight wedge vouch for its own delivery.
|
|
57158
57531
|
*/
|
|
57159
57532
|
lastNonPackageActivityAt = /* @__PURE__ */ new Map();
|
|
57160
57533
|
/** Last time the oversize-sighting drop was logged per device — one line
|
|
@@ -58064,6 +58437,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58064
58437
|
media: mediaStore
|
|
58065
58438
|
});
|
|
58066
58439
|
this.groupStore = groupStore;
|
|
58440
|
+
this.groupLive = new GroupLiveBuffer();
|
|
58067
58441
|
this.groupSealer = new GroupSealer({
|
|
58068
58442
|
trackStore,
|
|
58069
58443
|
groupStore,
|
|
@@ -58078,9 +58452,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58078
58452
|
},
|
|
58079
58453
|
now: () => Date.now()
|
|
58080
58454
|
});
|
|
58081
|
-
this.groupSealer.backfill().catch((err) => {
|
|
58082
|
-
logger.warn("group sealer backfill failed", { meta: { error: require_dist.errMsg(err) } });
|
|
58083
|
-
});
|
|
58084
58455
|
return {
|
|
58085
58456
|
trackStore,
|
|
58086
58457
|
mediaStore,
|
|
@@ -58523,12 +58894,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58523
58894
|
buildSummaryPorts(stores) {
|
|
58524
58895
|
return {
|
|
58525
58896
|
listTracks: async ({ deviceId, sinceMs, untilMs, limit }) => {
|
|
58526
|
-
const groups = await stores.groupStore.listOverlapping({
|
|
58527
|
-
deviceId,
|
|
58528
|
-
sinceMs,
|
|
58529
|
-
untilMs,
|
|
58530
|
-
limit
|
|
58531
|
-
});
|
|
58532
58897
|
const windowTracks = await stores.trackStore.queryHistorical({
|
|
58533
58898
|
deviceId,
|
|
58534
58899
|
since: sinceMs,
|
|
@@ -58536,51 +58901,42 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58536
58901
|
projection: "slim",
|
|
58537
58902
|
limit: Math.max(limit, 2e3)
|
|
58538
58903
|
});
|
|
58539
|
-
const trackById = new Map(windowTracks.map((t) => [t.trackId, t]));
|
|
58540
58904
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
58541
|
-
|
|
58542
|
-
|
|
58543
|
-
|
|
58544
|
-
|
|
58545
|
-
|
|
58546
|
-
|
|
58547
|
-
|
|
58548
|
-
|
|
58549
|
-
|
|
58550
|
-
|
|
58551
|
-
|
|
58552
|
-
|
|
58553
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
58554
|
-
deviceId,
|
|
58555
|
-
className: primary.className,
|
|
58556
|
-
firstSeen: group.openedAt,
|
|
58557
|
-
lastSeen: group.closedAt,
|
|
58558
|
-
importance,
|
|
58559
|
-
zones,
|
|
58560
|
-
classes,
|
|
58561
|
-
...tracks.every((t) => t.closedReason === "stationary-promotion") ? { stationary: true } : {},
|
|
58562
|
-
confirmed: tracks.some((t) => t.observations !== void 0 && t.observations >= minHits)
|
|
58563
|
-
});
|
|
58564
|
-
}
|
|
58565
|
-
return out;
|
|
58905
|
+
return windowTracks.map((t) => ({
|
|
58906
|
+
trackId: t.trackId,
|
|
58907
|
+
deviceId,
|
|
58908
|
+
className: t.className,
|
|
58909
|
+
firstSeen: t.firstSeen,
|
|
58910
|
+
lastSeen: t.lastSeen,
|
|
58911
|
+
zones: [...t.zonesVisited],
|
|
58912
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
58913
|
+
...t.classes !== void 0 && t.classes.length > 0 ? { classes: t.classes } : {},
|
|
58914
|
+
...t.closedReason === "stationary-promotion" ? { stationary: true } : {},
|
|
58915
|
+
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
58916
|
+
}));
|
|
58566
58917
|
},
|
|
58567
58918
|
listMediaInfo: async (id) => {
|
|
58568
58919
|
const group = await stores.groupStore.get(id);
|
|
58569
|
-
if (group
|
|
58570
|
-
|
|
58571
|
-
|
|
58572
|
-
|
|
58573
|
-
|
|
58574
|
-
|
|
58575
|
-
|
|
58576
|
-
|
|
58577
|
-
|
|
58920
|
+
if (group) {
|
|
58921
|
+
if (group.mediaKey || group.nativeKey) return digestMediaRows({
|
|
58922
|
+
mediaKey: group.mediaKey,
|
|
58923
|
+
nativeKey: group.nativeKey,
|
|
58924
|
+
timestamp: group.timestamp,
|
|
58925
|
+
memberRows: []
|
|
58926
|
+
});
|
|
58927
|
+
const members = await stores.groupStore.listMembers(group.id);
|
|
58928
|
+
const memberRows = (await Promise.all(members.map((m) => stores.mediaStore.listInfoByOwner("track", m.trackId)))).flatMap((rows) => rows.map((r) => ({
|
|
58578
58929
|
key: r.key,
|
|
58579
58930
|
kind: r.kind,
|
|
58580
58931
|
timestamp: r.timestamp
|
|
58581
|
-
}));
|
|
58932
|
+
})));
|
|
58933
|
+
return digestMediaRows({
|
|
58934
|
+
mediaKey: group.mediaKey,
|
|
58935
|
+
nativeKey: group.nativeKey,
|
|
58936
|
+
timestamp: group.timestamp,
|
|
58937
|
+
memberRows
|
|
58938
|
+
});
|
|
58582
58939
|
}
|
|
58583
|
-
if (group) return [];
|
|
58584
58940
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
58585
58941
|
key: r.key,
|
|
58586
58942
|
kind: r.kind,
|
|
@@ -59826,6 +60182,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
59826
60182
|
state: t.state
|
|
59827
60183
|
});
|
|
59828
60184
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
60185
|
+
this.enqueueLiveGroup(upserted, result.timestamp);
|
|
59829
60186
|
}
|
|
59830
60187
|
if (result.rawTrackedDetections.length > 0) {
|
|
59831
60188
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
@@ -60039,6 +60396,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60039
60396
|
deviceId,
|
|
60040
60397
|
tracked: result.tracked,
|
|
60041
60398
|
packageClasses,
|
|
60399
|
+
packageZoneIds: collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []),
|
|
60042
60400
|
settings: packageSettings,
|
|
60043
60401
|
frameWidth: result.frameWidth,
|
|
60044
60402
|
frameHeight: result.frameHeight,
|
|
@@ -61749,8 +62107,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61749
62107
|
async sweepExpiredTracks() {
|
|
61750
62108
|
if (this.shuttingDown || !this.trackStore) return;
|
|
61751
62109
|
await this.trackCloser.sweep();
|
|
61752
|
-
this.groupSealer?.
|
|
62110
|
+
if (this.isPostProcessingNode && this.groupLive) this.groupSealer?.flushIdle(this.groupLive);
|
|
61753
62111
|
}
|
|
62112
|
+
/** Hub-only: assign this sighting in RAM. First activity on a camera
|
|
62113
|
+
* replays 2 minutes of that camera (crash recover), then observe is sync. */
|
|
62114
|
+
enqueueLiveGroup(track, now) {}
|
|
61754
62115
|
/** Unified retention (Phase 1): earliest retained footage per device — the
|
|
61755
62116
|
* follow-recordings cutoff source. Cached 10 min: availability answers in
|
|
61756
62117
|
* ~70 ms but the sweep asks once per device per pass. `null` = no footage
|
|
@@ -62196,7 +62557,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62196
62557
|
runPackageStillness(input) {
|
|
62197
62558
|
if (this.packageDropDetector === null) return;
|
|
62198
62559
|
const tags = { deviceId: input.deviceId };
|
|
62199
|
-
for (const t of input.tracked)
|
|
62560
|
+
for (const t of input.tracked) {
|
|
62561
|
+
if (input.packageClasses.has(t.className)) continue;
|
|
62562
|
+
if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
|
|
62200
62563
|
this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
|
|
62201
62564
|
break;
|
|
62202
62565
|
}
|
|
@@ -62243,6 +62606,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62243
62606
|
frameWidth: input.frameWidth,
|
|
62244
62607
|
frameHeight: input.frameHeight,
|
|
62245
62608
|
timestamp: input.timestamp,
|
|
62609
|
+
minHits: input.settings.packageDropMinFrames,
|
|
62246
62610
|
minDwellMs: input.settings.packageDropDwellSec * 1e3,
|
|
62247
62611
|
activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
|
|
62248
62612
|
...lastActivityAt !== void 0 ? { lastActivityAt } : {}
|
|
@@ -62346,9 +62710,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62346
62710
|
for (const ex of outcome.expired) {
|
|
62347
62711
|
const observedDwellMs = ex.lastSeenAt - ex.firstSeenAt;
|
|
62348
62712
|
const requiredDwellMs = input.settings.packageDropDwellSec * 1e3;
|
|
62713
|
+
const requiredHits = input.settings.packageDropMinFrames;
|
|
62349
62714
|
const reasons = [];
|
|
62350
62715
|
if (!ex.activityBackedAtOpen) reasons.push("no-activity-before-open");
|
|
62351
|
-
if (ex.
|
|
62716
|
+
if (ex.hits < requiredHits) reasons.push("hits");
|
|
62352
62717
|
if (observedDwellMs < requiredDwellMs) reasons.push("dwell");
|
|
62353
62718
|
this.ctx.logger.info("package candidate expired without promotion", {
|
|
62354
62719
|
tags,
|
|
@@ -62356,8 +62721,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62356
62721
|
candidateId: ex.id,
|
|
62357
62722
|
className: ex.className,
|
|
62358
62723
|
bbox: ex.bbox,
|
|
62359
|
-
|
|
62360
|
-
|
|
62724
|
+
hits: ex.hits,
|
|
62725
|
+
minHits: requiredHits,
|
|
62361
62726
|
observedDwellMs,
|
|
62362
62727
|
requiredDwellMs,
|
|
62363
62728
|
activityBackedAtOpen: ex.activityBackedAtOpen,
|
|
@@ -62394,6 +62759,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62394
62759
|
};
|
|
62395
62760
|
if (plausible) this.stationaryRegistry?.promote(entry);
|
|
62396
62761
|
processor.dropTrack(track.trackId);
|
|
62762
|
+
const peek = this.trackStore?.peekActive(track.trackId);
|
|
62763
|
+
if (peek) this.enqueueLiveGroup(toGroupableTrack({
|
|
62764
|
+
trackId: track.trackId,
|
|
62765
|
+
deviceId,
|
|
62766
|
+
className: track.className,
|
|
62767
|
+
firstSeen: peek.firstSeen,
|
|
62768
|
+
lastSeen: timestamp,
|
|
62769
|
+
positions: peek.positions,
|
|
62770
|
+
closedReason: "stationary-promotion"
|
|
62771
|
+
}), timestamp);
|
|
62397
62772
|
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
62398
62773
|
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
62399
62774
|
tags: { deviceId },
|
|
@@ -62609,6 +62984,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
62609
62984
|
async listRecentTracks(input) {
|
|
62610
62985
|
return this.queryFacade.listRecentTracks(input);
|
|
62611
62986
|
}
|
|
62987
|
+
async listGroups(input) {
|
|
62988
|
+
return {
|
|
62989
|
+
groups: [],
|
|
62990
|
+
nextCursor: null
|
|
62991
|
+
};
|
|
62992
|
+
}
|
|
62993
|
+
async getGroup(input) {
|
|
62994
|
+
return null;
|
|
62995
|
+
}
|
|
62612
62996
|
/**
|
|
62613
62997
|
* Every event kind the device can produce: built-ins (motion + audio),
|
|
62614
62998
|
* detection classes actually observed on the device, and sensor kinds
|
|
@@ -63753,33 +64137,38 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63753
64137
|
buildFaceRescoreActionHandlers() {
|
|
63754
64138
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
63755
64139
|
}
|
|
63756
|
-
|
|
63757
|
-
const
|
|
63758
|
-
|
|
63759
|
-
|
|
63760
|
-
|
|
63761
|
-
|
|
63762
|
-
|
|
63763
|
-
|
|
63764
|
-
|
|
63765
|
-
|
|
63766
|
-
|
|
63767
|
-
|
|
63768
|
-
|
|
64140
|
+
groupQueryPorts() {
|
|
64141
|
+
const groupStore = this.groupStore;
|
|
64142
|
+
if (groupStore === null) return null;
|
|
64143
|
+
return {
|
|
64144
|
+
groupStore,
|
|
64145
|
+
loadTracks: async (trackIds) => {
|
|
64146
|
+
const map = /* @__PURE__ */ new Map();
|
|
64147
|
+
const tracks = this.trackStore;
|
|
64148
|
+
if (!tracks || trackIds.length === 0) return map;
|
|
64149
|
+
const missing = [];
|
|
64150
|
+
for (const id of trackIds) {
|
|
64151
|
+
const active = tracks.getActiveByTrack(id);
|
|
64152
|
+
if (active !== null) map.set(id, trackToLite(active));
|
|
64153
|
+
else missing.push(id);
|
|
64154
|
+
}
|
|
64155
|
+
if (missing.length === 0) return map;
|
|
64156
|
+
for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
|
|
64157
|
+
return map;
|
|
64158
|
+
},
|
|
64159
|
+
eventMediaBaseUrl: () => this.eventMediaBaseUrl
|
|
63769
64160
|
};
|
|
64161
|
+
}
|
|
64162
|
+
buildGroupActionHandlers() {
|
|
63770
64163
|
return {
|
|
63771
64164
|
"analytics.listGroups": async (input) => {
|
|
63772
|
-
|
|
63773
|
-
if (ready === null) return {
|
|
64165
|
+
return {
|
|
63774
64166
|
groups: [],
|
|
63775
64167
|
hasMore: false
|
|
63776
64168
|
};
|
|
63777
|
-
return listGroupRecords(ready, input);
|
|
63778
64169
|
},
|
|
63779
64170
|
"analytics.getGroup": async (input) => {
|
|
63780
|
-
|
|
63781
|
-
if (ready === null) return { group: null };
|
|
63782
|
-
return { group: await getGroupDetail(ready, input.groupId) };
|
|
64171
|
+
return { group: null };
|
|
63783
64172
|
}
|
|
63784
64173
|
};
|
|
63785
64174
|
}
|