@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-PMOtjzh2.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -15132,28 +15132,38 @@ function selectSummaryTiles(candidates, opts) {
|
|
|
15132
15132
|
};
|
|
15133
15133
|
}
|
|
15134
15134
|
/**
|
|
15135
|
+
* Ranked leftover after the quota pass — refill a mosaic that dropped
|
|
15136
|
+
* duplicate frames or missing crops without walking the same track twice.
|
|
15137
|
+
*/
|
|
15138
|
+
function remainingSummaryCandidates(kept, taken, rank) {
|
|
15139
|
+
const takenIds = new Set(taken.map((c) => c.trackId));
|
|
15140
|
+
return kept.filter((c) => !takenIds.has(c.trackId)).toSorted(byRankDesc(rank));
|
|
15141
|
+
}
|
|
15142
|
+
/**
|
|
15135
15143
|
* The media a tile is drawn from, best first.
|
|
15136
15144
|
*
|
|
15137
|
-
*
|
|
15138
|
-
*
|
|
15139
|
-
*
|
|
15140
|
-
*
|
|
15145
|
+
* First-level group crop only (`groupCrop`), then the group's native-frame
|
|
15146
|
+
* pointer, then a member's clean full frame. Member `thumbnail` / plate /
|
|
15147
|
+
* face crops stay off this ladder: a successful plate read overwrites the
|
|
15148
|
+
* vehicle first-level crop under the same `thumbnail` kind, and a digest
|
|
15149
|
+
* tile of a targa is the wrong picture. Boxed frames stay absent: a drawn
|
|
15150
|
+
* rectangle is a permanent lie in a digest tile. Duplicate driveways from
|
|
15151
|
+
* tracker ID churn are collapsed by `frameDedupeKey`, not by hiding the
|
|
15152
|
+
* full-frame fallback.
|
|
15141
15153
|
*/
|
|
15142
15154
|
var SUMMARY_MEDIA_LADDER = [
|
|
15143
15155
|
"groupCrop",
|
|
15144
|
-
"
|
|
15156
|
+
"nativeFrame",
|
|
15145
15157
|
"keyFrame",
|
|
15146
|
-
"
|
|
15147
|
-
"fullFrame"
|
|
15148
|
-
"thumbnailSmall",
|
|
15149
|
-
"thumbnail"
|
|
15158
|
+
"keyFrameSmall",
|
|
15159
|
+
"fullFrame"
|
|
15150
15160
|
];
|
|
15151
15161
|
/**
|
|
15152
15162
|
* Pick ONE media row for a track — the cheap question first (index rows only),
|
|
15153
15163
|
* so exactly one blob is ever read.
|
|
15154
15164
|
*
|
|
15155
|
-
* Within a rung the row CLOSEST to the track's peak is chosen;
|
|
15156
|
-
*
|
|
15165
|
+
* Within a rung the row CLOSEST to the track's peak is chosen; a member may
|
|
15166
|
+
* carry more than one full-frame still, so "the first one" would systematically
|
|
15157
15167
|
* pick the moment the subject entered rather than the moment it was best seen.
|
|
15158
15168
|
*/
|
|
15159
15169
|
function pickSummaryMedia(rows, aroundMs) {
|
|
@@ -15164,6 +15174,13 @@ function pickSummaryMedia(rows, aroundMs) {
|
|
|
15164
15174
|
}
|
|
15165
15175
|
return null;
|
|
15166
15176
|
}
|
|
15177
|
+
/**
|
|
15178
|
+
* Same camera, same wall-clock second → same JPEG. Tracker ID churn
|
|
15179
|
+
* otherwise fills a mosaic with eleven copies of one driveway still.
|
|
15180
|
+
*/
|
|
15181
|
+
function frameDedupeKey(deviceId, mediaTimestamp) {
|
|
15182
|
+
return `${deviceId}:${Math.floor(mediaTimestamp / 1e3)}`;
|
|
15183
|
+
}
|
|
15167
15184
|
//#endregion
|
|
15168
15185
|
//#region src/notification-center/summary/summary-window.ts
|
|
15169
15186
|
/**
|
|
@@ -15463,7 +15480,12 @@ var NcSummaryProducer = class {
|
|
|
15463
15480
|
droppedStationary: filtered.droppedStationary
|
|
15464
15481
|
};
|
|
15465
15482
|
}
|
|
15466
|
-
const loaded = await this.loadTiles(
|
|
15483
|
+
const loaded = await this.loadTiles({
|
|
15484
|
+
rule,
|
|
15485
|
+
preferred: selection.tiles,
|
|
15486
|
+
refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
|
|
15487
|
+
maxTiles: rule.maxTiles
|
|
15488
|
+
});
|
|
15467
15489
|
const detections = countDetections({ tracks: selection.tiles.map((t) => ({
|
|
15468
15490
|
firstSeen: t.firstSeen,
|
|
15469
15491
|
lastSeen: t.lastSeen,
|
|
@@ -15521,7 +15543,7 @@ var NcSummaryProducer = class {
|
|
|
15521
15543
|
tiles: loaded.length,
|
|
15522
15544
|
cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
15523
15545
|
camerasDropped: selection.camerasDropped,
|
|
15524
|
-
droppedNoMedia:
|
|
15546
|
+
droppedNoMedia: Math.max(0, rule.maxTiles - loaded.length),
|
|
15525
15547
|
mosaicBytes: mosaic.bytes,
|
|
15526
15548
|
mosaicPublished: mosaic.ref !== null,
|
|
15527
15549
|
enqueued,
|
|
@@ -15585,10 +15607,18 @@ var NcSummaryProducer = class {
|
|
|
15585
15607
|
}
|
|
15586
15608
|
return all;
|
|
15587
15609
|
}
|
|
15588
|
-
/** One blob per tile — the cheap question (index rows) always first.
|
|
15589
|
-
|
|
15610
|
+
/** One blob per tile — the cheap question (index rows) always first.
|
|
15611
|
+
* Duplicate camera-seconds are skipped so tracker ID churn cannot paint
|
|
15612
|
+
* the same driveway eleven times; leftover ranked candidates refill. */
|
|
15613
|
+
async loadTiles(input) {
|
|
15590
15614
|
const out = [];
|
|
15591
|
-
|
|
15615
|
+
const seenFrames = /* @__PURE__ */ new Set();
|
|
15616
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
15617
|
+
const queue = [...input.preferred, ...input.refill];
|
|
15618
|
+
for (const candidate of queue) {
|
|
15619
|
+
if (out.length >= input.maxTiles) break;
|
|
15620
|
+
if (seenIds.has(candidate.trackId)) continue;
|
|
15621
|
+
seenIds.add(candidate.trackId);
|
|
15592
15622
|
const tags = { deviceId: candidate.deviceId };
|
|
15593
15623
|
try {
|
|
15594
15624
|
const rows = await this.deps.listMediaInfo(candidate.trackId);
|
|
@@ -15597,25 +15627,38 @@ var NcSummaryProducer = class {
|
|
|
15597
15627
|
this.deps.logger.debug("summary tile dropped: the track carries no clean media", {
|
|
15598
15628
|
tags,
|
|
15599
15629
|
meta: {
|
|
15600
|
-
ruleId: rule.id,
|
|
15630
|
+
ruleId: input.rule.id,
|
|
15601
15631
|
trackId: candidate.trackId,
|
|
15602
15632
|
mediaRows: rows.length
|
|
15603
15633
|
}
|
|
15604
15634
|
});
|
|
15605
15635
|
continue;
|
|
15606
15636
|
}
|
|
15637
|
+
const frameKey = frameDedupeKey(candidate.deviceId, picked.timestamp);
|
|
15638
|
+
if (seenFrames.has(frameKey)) {
|
|
15639
|
+
this.deps.logger.debug("summary tile dropped: same frame as an earlier tile", {
|
|
15640
|
+
tags,
|
|
15641
|
+
meta: {
|
|
15642
|
+
ruleId: input.rule.id,
|
|
15643
|
+
trackId: candidate.trackId,
|
|
15644
|
+
frameKey
|
|
15645
|
+
}
|
|
15646
|
+
});
|
|
15647
|
+
continue;
|
|
15648
|
+
}
|
|
15607
15649
|
const jpeg = await this.deps.readMedia(picked.key);
|
|
15608
15650
|
if (jpeg === null || jpeg.byteLength === 0) {
|
|
15609
15651
|
this.deps.logger.debug("summary tile dropped: the media blob could not be read", {
|
|
15610
15652
|
tags,
|
|
15611
15653
|
meta: {
|
|
15612
|
-
ruleId: rule.id,
|
|
15654
|
+
ruleId: input.rule.id,
|
|
15613
15655
|
trackId: candidate.trackId,
|
|
15614
15656
|
mediaKey: picked.key
|
|
15615
15657
|
}
|
|
15616
15658
|
});
|
|
15617
15659
|
continue;
|
|
15618
15660
|
}
|
|
15661
|
+
seenFrames.add(frameKey);
|
|
15619
15662
|
out.push({
|
|
15620
15663
|
candidate,
|
|
15621
15664
|
jpeg
|
|
@@ -15624,7 +15667,7 @@ var NcSummaryProducer = class {
|
|
|
15624
15667
|
this.deps.logger.warn("summary tile dropped: reading its media threw", {
|
|
15625
15668
|
tags,
|
|
15626
15669
|
meta: {
|
|
15627
|
-
ruleId: rule.id,
|
|
15670
|
+
ruleId: input.rule.id,
|
|
15628
15671
|
trackId: candidate.trackId,
|
|
15629
15672
|
error: String(err)
|
|
15630
15673
|
}
|
|
@@ -39832,8 +39875,9 @@ var OverlayDetailStateStore = class {
|
|
|
39832
39875
|
*
|
|
39833
39876
|
* See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
|
|
39834
39877
|
* docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
|
|
39835
|
-
* (surface C).
|
|
39836
|
-
*
|
|
39878
|
+
* (surface C). Confirmation is N frames (default 3); dwell default is 0
|
|
39879
|
+
* (an extra bar). The 15 s dwell plus a 5 s round-gap missed a real
|
|
39880
|
+
* parcel on device 615 (2026-08-24 13:40).
|
|
39837
39881
|
*/
|
|
39838
39882
|
var PackageDropSettingsSchema = object({
|
|
39839
39883
|
/**
|
|
@@ -39854,13 +39898,20 @@ var PackageDropSettingsSchema = object({
|
|
|
39854
39898
|
*/
|
|
39855
39899
|
packageDropEnabled: boolean().default(true),
|
|
39856
39900
|
/**
|
|
39857
|
-
*
|
|
39858
|
-
*
|
|
39859
|
-
*
|
|
39860
|
-
*
|
|
39861
|
-
*
|
|
39901
|
+
* Frames the box must be re-seen on before it is a parked package.
|
|
39902
|
+
* Same bar as tracker `minHits`: one frame is a flicker, two can still
|
|
39903
|
+
* be a passer-by's bag, three is "it stayed". A settle burst of three
|
|
39904
|
+
* frames in ~1 s is enough; no second governor round after the courier
|
|
39905
|
+
* left. Device 615 2026-08-24 13:40 expired at 1 round / 1.5 s dwell.
|
|
39862
39906
|
*/
|
|
39863
|
-
|
|
39907
|
+
packageDropMinFrames: number().int().min(1).max(20).default(3),
|
|
39908
|
+
/**
|
|
39909
|
+
* Extra OBSERVED dwell (seconds, since first-seen) on top of N frames.
|
|
39910
|
+
* 0 (the default) means the frame count is enough — a courier who drops
|
|
39911
|
+
* and leaves in 2 s still delivers. Raise this to reject a bag briefly
|
|
39912
|
+
* set down and snatched back.
|
|
39913
|
+
*/
|
|
39914
|
+
packageDropDwellSec: number().int().min(0).max(3600).default(0),
|
|
39864
39915
|
/** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
|
|
39865
39916
|
packageDropMinBboxAreaFrac: number().min(0).max(1).default(.004),
|
|
39866
39917
|
/**
|
|
@@ -39878,13 +39929,15 @@ var PackageDropSettingsSchema = object({
|
|
|
39878
39929
|
packageDropPickupEnabled: boolean().default(true),
|
|
39879
39930
|
/**
|
|
39880
39931
|
* A parcel needs a deliverer: a stillness candidate only ever promotes to
|
|
39881
|
-
* `delivered` if a non-package track (person/vehicle/animal) was active
|
|
39882
|
-
*
|
|
39883
|
-
* departure only counts as `picked-up` with
|
|
39884
|
-
*
|
|
39885
|
-
*
|
|
39886
|
-
*
|
|
39887
|
-
*
|
|
39932
|
+
* `delivered` if a non-package track (person/vehicle/animal) was active IN
|
|
39933
|
+
* A CONFIGURED PACKAGE DROP-OFF ZONE within this many seconds BEFORE the
|
|
39934
|
+
* box first appeared — and a departure only counts as `picked-up` with
|
|
39935
|
+
* somebody in that zone around the disappearance. Whole-camera motion (a
|
|
39936
|
+
* walker at the far gate) does not count. 0 disables both gates. Device
|
|
39937
|
+
* 615's streetlight wedge cycled delivered→picked-up ~23×/night with
|
|
39938
|
+
* nobody in the drop-off zone. Default 300 s: wide enough to absorb the
|
|
39939
|
+
* motion-settle governor's cadence (the box is often first SEEN minutes
|
|
39940
|
+
* after the courier left).
|
|
39888
39941
|
*/
|
|
39889
39942
|
packageDropActivityWindowSec: number().int().min(0).max(3600).default(300),
|
|
39890
39943
|
/**
|
|
@@ -39908,6 +39961,7 @@ function resolvePackageDropSettings(raw) {
|
|
|
39908
39961
|
};
|
|
39909
39962
|
return {
|
|
39910
39963
|
packageDropEnabled: pick("packageDropEnabled"),
|
|
39964
|
+
packageDropMinFrames: pick("packageDropMinFrames"),
|
|
39911
39965
|
packageDropDwellSec: pick("packageDropDwellSec"),
|
|
39912
39966
|
packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
|
|
39913
39967
|
packageDropMaxBboxAreaFrac: pick("packageDropMaxBboxAreaFrac"),
|
|
@@ -40165,29 +40219,6 @@ var DeferredBirthRegistry = class {
|
|
|
40165
40219
|
this.byDevice.delete(deviceKey);
|
|
40166
40220
|
}
|
|
40167
40221
|
};
|
|
40168
|
-
var AnalyticsGroupRecordSchema = object({
|
|
40169
|
-
id: string(),
|
|
40170
|
-
deviceId: number().int(),
|
|
40171
|
-
openedAt: number().int(),
|
|
40172
|
-
closedAt: number().int(),
|
|
40173
|
-
timestamp: number().int(),
|
|
40174
|
-
memberCount: number().int(),
|
|
40175
|
-
memberTrackIds: array(string()).readonly(),
|
|
40176
|
-
className: string(),
|
|
40177
|
-
classes: array(string()).readonly(),
|
|
40178
|
-
/** Relative event-media path, or null when the group has no picture yet. */
|
|
40179
|
-
mediaUrl: string().nullable(),
|
|
40180
|
-
singleton: boolean()
|
|
40181
|
-
});
|
|
40182
|
-
var AnalyticsGroupMemberSchema = object({
|
|
40183
|
-
trackId: string(),
|
|
40184
|
-
deviceId: number().int(),
|
|
40185
|
-
className: string(),
|
|
40186
|
-
firstSeen: number().int(),
|
|
40187
|
-
lastSeen: number().int(),
|
|
40188
|
-
mediaUrl: string().nullable()
|
|
40189
|
-
});
|
|
40190
|
-
var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
|
|
40191
40222
|
var groupActions = defineCustomActions({
|
|
40192
40223
|
"analytics.listGroups": customAction(object({
|
|
40193
40224
|
deviceId: number().int().optional(),
|
|
@@ -40201,61 +40232,6 @@ var groupActions = defineCustomActions({
|
|
|
40201
40232
|
})),
|
|
40202
40233
|
"analytics.getGroup": customAction(object({ groupId: string().min(1) }), object({ group: AnalyticsGroupDetailSchema.nullable() }))
|
|
40203
40234
|
});
|
|
40204
|
-
//#endregion
|
|
40205
|
-
//#region src/pipeline-analytics/group/group-query.ts
|
|
40206
|
-
/**
|
|
40207
|
-
* Stamp a relative event-media URL.
|
|
40208
|
-
*
|
|
40209
|
-
* Media keys contain `:` and the data-plane serves them by key. Track ids are
|
|
40210
|
-
* bare and fall through to the existing event/track thumbnail resolver. Never
|
|
40211
|
-
* percent-encode: the handler does not URL-decode the path.
|
|
40212
|
-
*/
|
|
40213
|
-
function stampEventMediaUrl(baseUrl, id) {
|
|
40214
|
-
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
40215
|
-
if (id === null || id === void 0 || id.length === 0) return null;
|
|
40216
|
-
return `${baseUrl}/${id}`;
|
|
40217
|
-
}
|
|
40218
|
-
/** Union of every class on the members; `className` is the first, stable order. */
|
|
40219
|
-
function aggregateGroupClasses(tracks) {
|
|
40220
|
-
const seen = /* @__PURE__ */ new Set();
|
|
40221
|
-
const classes = [];
|
|
40222
|
-
for (const track of tracks) for (const name of [track.className, ...track.classes ?? []]) {
|
|
40223
|
-
if (name.length === 0 || seen.has(name)) continue;
|
|
40224
|
-
seen.add(name);
|
|
40225
|
-
classes.push(name);
|
|
40226
|
-
}
|
|
40227
|
-
return {
|
|
40228
|
-
className: classes[0] ?? "unknown",
|
|
40229
|
-
classes
|
|
40230
|
-
};
|
|
40231
|
-
}
|
|
40232
|
-
function toGroupRecord(input) {
|
|
40233
|
-
const { className, classes } = aggregateGroupClasses(input.tracks);
|
|
40234
|
-
const mediaId = input.group.mediaKey ?? (input.group.memberCount === 1 ? input.memberTrackIds[0] : null);
|
|
40235
|
-
return {
|
|
40236
|
-
id: input.group.id,
|
|
40237
|
-
deviceId: input.group.deviceId,
|
|
40238
|
-
openedAt: input.group.openedAt,
|
|
40239
|
-
closedAt: input.group.closedAt,
|
|
40240
|
-
timestamp: input.group.timestamp,
|
|
40241
|
-
memberCount: input.memberTrackIds.length > 0 ? input.memberTrackIds.length : input.group.memberCount,
|
|
40242
|
-
memberTrackIds: [...input.memberTrackIds],
|
|
40243
|
-
className,
|
|
40244
|
-
classes: [...classes],
|
|
40245
|
-
mediaUrl: stampEventMediaUrl(input.eventMediaBaseUrl, mediaId),
|
|
40246
|
-
singleton: input.memberTrackIds.length <= 1
|
|
40247
|
-
};
|
|
40248
|
-
}
|
|
40249
|
-
function toGroupMember(member, track, eventMediaBaseUrl) {
|
|
40250
|
-
return {
|
|
40251
|
-
trackId: member.trackId,
|
|
40252
|
-
deviceId: member.deviceId,
|
|
40253
|
-
className: track?.className ?? "unknown",
|
|
40254
|
-
firstSeen: track?.firstSeen ?? member.timestamp,
|
|
40255
|
-
lastSeen: track?.lastSeen ?? member.timestamp,
|
|
40256
|
-
mediaUrl: stampEventMediaUrl(eventMediaBaseUrl, member.trackId)
|
|
40257
|
-
};
|
|
40258
|
-
}
|
|
40259
40235
|
function trackToLite(track) {
|
|
40260
40236
|
return {
|
|
40261
40237
|
trackId: track.trackId,
|
|
@@ -40265,116 +40241,43 @@ function trackToLite(track) {
|
|
|
40265
40241
|
lastSeen: track.lastSeen
|
|
40266
40242
|
};
|
|
40267
40243
|
}
|
|
40268
|
-
|
|
40269
|
-
|
|
40270
|
-
|
|
40271
|
-
|
|
40272
|
-
|
|
40273
|
-
|
|
40274
|
-
|
|
40275
|
-
|
|
40276
|
-
|
|
40277
|
-
|
|
40278
|
-
|
|
40279
|
-
|
|
40280
|
-
|
|
40281
|
-
const
|
|
40282
|
-
|
|
40283
|
-
|
|
40284
|
-
|
|
40285
|
-
|
|
40286
|
-
if (lite !== null) lites.push(lite);
|
|
40287
|
-
rows.push(toGroupMember(member, lite, ports.eventMediaBaseUrl()));
|
|
40288
|
-
}
|
|
40289
|
-
return {
|
|
40290
|
-
...toGroupRecord({
|
|
40291
|
-
group,
|
|
40292
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
40293
|
-
tracks: lites,
|
|
40294
|
-
eventMediaBaseUrl: ports.eventMediaBaseUrl()
|
|
40295
|
-
}),
|
|
40296
|
-
members: rows
|
|
40297
|
-
};
|
|
40298
|
-
}
|
|
40299
|
-
async function buildGroupRecord(ports, group) {
|
|
40300
|
-
const members = await ports.groupStore.listMembers(group.id);
|
|
40301
|
-
const lites = [];
|
|
40302
|
-
for (const member of members) {
|
|
40303
|
-
const track = await ports.loadTrack(member.trackId);
|
|
40304
|
-
if (track !== null) lites.push(trackToLite(track));
|
|
40305
|
-
}
|
|
40306
|
-
return toGroupRecord({
|
|
40307
|
-
group,
|
|
40308
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
40309
|
-
tracks: lites,
|
|
40310
|
-
eventMediaBaseUrl: ports.eventMediaBaseUrl()
|
|
40244
|
+
/**
|
|
40245
|
+
* Gallery and digest read the same ladder: groupCrop, then the native-frame
|
|
40246
|
+
* pointer, then a member's clean full frame. Plate / face / member
|
|
40247
|
+
* thumbnails never reach the picker — a plate read overwrites the vehicle
|
|
40248
|
+
* first-level crop under the same `thumbnail` kind. A multi with no crop
|
|
40249
|
+
* still has a picture if the sealer stored `nativeKey`.
|
|
40250
|
+
*/
|
|
40251
|
+
var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
|
|
40252
|
+
"keyFrame",
|
|
40253
|
+
"keyFrameSmall",
|
|
40254
|
+
"fullFrame"
|
|
40255
|
+
]);
|
|
40256
|
+
function digestMediaRows(input) {
|
|
40257
|
+
const owned = [];
|
|
40258
|
+
if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
|
|
40259
|
+
key: input.mediaKey,
|
|
40260
|
+
kind: "groupCrop",
|
|
40261
|
+
timestamp: input.timestamp
|
|
40311
40262
|
});
|
|
40312
|
-
|
|
40313
|
-
|
|
40314
|
-
|
|
40315
|
-
|
|
40316
|
-
function groupWideCentralSquareLayout(unionBbox, frame) {
|
|
40317
|
-
return wideCentralSquareLayout(unionBbox, frame, { containSubject: true });
|
|
40318
|
-
}
|
|
40319
|
-
async function cropGroupFromRgb(rgb, frameWidth, frameHeight, unionBbox) {
|
|
40320
|
-
const layout = groupWideCentralSquareLayout(unionBbox, {
|
|
40321
|
-
W: frameWidth,
|
|
40322
|
-
H: frameHeight
|
|
40263
|
+
if (input.nativeKey !== null && input.nativeKey.length > 0) owned.push({
|
|
40264
|
+
key: input.nativeKey,
|
|
40265
|
+
kind: "nativeFrame",
|
|
40266
|
+
timestamp: input.timestamp
|
|
40323
40267
|
});
|
|
40324
|
-
|
|
40325
|
-
return
|
|
40268
|
+
if (owned.length > 0) return owned;
|
|
40269
|
+
return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
|
|
40326
40270
|
}
|
|
40327
40271
|
var MATCH_WINDOW_MS = 1e3;
|
|
40328
40272
|
var MOVE_EPS = .35;
|
|
40273
|
+
/** Live-assigner window: same predicate, recent positions only. */
|
|
40274
|
+
var LIVE_WINDOW_MS = 3e3;
|
|
40329
40275
|
/** Registry stationary is out; audio has no bbox and is never a member or a singleton. */
|
|
40330
40276
|
function isEligibleMember(track) {
|
|
40331
40277
|
if (track.closedReason === "stationary-promotion") return false;
|
|
40332
40278
|
if (track.className === "audio") return false;
|
|
40333
40279
|
return true;
|
|
40334
40280
|
}
|
|
40335
|
-
function temporallyOverlap(a, b, slackMs) {
|
|
40336
|
-
return a.firstSeen <= b.lastSeen + slackMs && b.firstSeen <= a.lastSeen + slackMs;
|
|
40337
|
-
}
|
|
40338
|
-
/** Connected components of temporal overlap (+ slack) per camera. */
|
|
40339
|
-
function sessionsOf(tracks) {
|
|
40340
|
-
const byDevice = /* @__PURE__ */ new Map();
|
|
40341
|
-
for (const t of tracks) {
|
|
40342
|
-
const list = byDevice.get(t.deviceId);
|
|
40343
|
-
if (list) list.push(t);
|
|
40344
|
-
else byDevice.set(t.deviceId, [t]);
|
|
40345
|
-
}
|
|
40346
|
-
const sessions = [];
|
|
40347
|
-
for (const deviceTracks of byDevice.values()) {
|
|
40348
|
-
const n = deviceTracks.length;
|
|
40349
|
-
const parent = deviceTracks.map((_, i) => i);
|
|
40350
|
-
const find = (i) => {
|
|
40351
|
-
let cur = i;
|
|
40352
|
-
while (parent[cur] !== cur) cur = parent[cur];
|
|
40353
|
-
let walk = i;
|
|
40354
|
-
while (parent[walk] !== cur) {
|
|
40355
|
-
const next = parent[walk];
|
|
40356
|
-
parent[walk] = cur;
|
|
40357
|
-
walk = next;
|
|
40358
|
-
}
|
|
40359
|
-
return cur;
|
|
40360
|
-
};
|
|
40361
|
-
const union = (i, j) => {
|
|
40362
|
-
const ri = find(i);
|
|
40363
|
-
const rj = find(j);
|
|
40364
|
-
if (ri !== rj) parent[ri] = rj;
|
|
40365
|
-
};
|
|
40366
|
-
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);
|
|
40367
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
40368
|
-
for (let i = 0; i < n; i++) {
|
|
40369
|
-
const root = find(i);
|
|
40370
|
-
const bucket = buckets.get(root);
|
|
40371
|
-
if (bucket) bucket.push(deviceTracks[i]);
|
|
40372
|
-
else buckets.set(root, [deviceTracks[i]]);
|
|
40373
|
-
}
|
|
40374
|
-
sessions.push(...buckets.values());
|
|
40375
|
-
}
|
|
40376
|
-
return sessions;
|
|
40377
|
-
}
|
|
40378
40281
|
function diag(bbox) {
|
|
40379
40282
|
return Math.hypot(bbox.w, bbox.h);
|
|
40380
40283
|
}
|
|
@@ -40396,9 +40299,21 @@ function meanDiag(positions) {
|
|
|
40396
40299
|
for (const p of positions) sum += diag(p.bbox);
|
|
40397
40300
|
return sum / positions.length;
|
|
40398
40301
|
}
|
|
40399
|
-
function
|
|
40400
|
-
|
|
40401
|
-
|
|
40302
|
+
function vecLen(v) {
|
|
40303
|
+
return Math.hypot(v.x, v.y);
|
|
40304
|
+
}
|
|
40305
|
+
function cosine(a, b) {
|
|
40306
|
+
const la = vecLen(a);
|
|
40307
|
+
const lb = vecLen(b);
|
|
40308
|
+
if (la === 0 || lb === 0) return 1;
|
|
40309
|
+
return (a.x * b.x + a.y * b.y) / (la * lb);
|
|
40310
|
+
}
|
|
40311
|
+
function positionsInWindow(track, start, end) {
|
|
40312
|
+
return track.positions.filter((p) => p.timestamp >= start && p.timestamp <= end);
|
|
40313
|
+
}
|
|
40314
|
+
function windowDisplacement(positions) {
|
|
40315
|
+
const first = positions[0];
|
|
40316
|
+
const last = positions[positions.length - 1];
|
|
40402
40317
|
if (!first || !last) return {
|
|
40403
40318
|
x: 0,
|
|
40404
40319
|
y: 0
|
|
@@ -40410,24 +40325,19 @@ function netDisplacement(track) {
|
|
|
40410
40325
|
y: b.y - a.y
|
|
40411
40326
|
};
|
|
40412
40327
|
}
|
|
40413
|
-
function
|
|
40414
|
-
|
|
40415
|
-
}
|
|
40416
|
-
function cosine(a, b) {
|
|
40417
|
-
const la = vecLen(a);
|
|
40418
|
-
const lb = vecLen(b);
|
|
40419
|
-
if (la === 0 || lb === 0) return 1;
|
|
40420
|
-
return (a.x * b.x + a.y * b.y) / (la * lb);
|
|
40421
|
-
}
|
|
40422
|
-
function relativeMotion(track) {
|
|
40423
|
-
const mean = meanDiag(track.positions);
|
|
40328
|
+
function windowRelativeMotion(positions) {
|
|
40329
|
+
const mean = meanDiag(positions);
|
|
40424
40330
|
if (mean <= 0) return 0;
|
|
40425
|
-
return vecLen(
|
|
40331
|
+
return vecLen(windowDisplacement(positions)) / mean;
|
|
40426
40332
|
}
|
|
40427
|
-
/**
|
|
40428
|
-
|
|
40429
|
-
|
|
40430
|
-
|
|
40333
|
+
/**
|
|
40334
|
+
* Same cohesion + crossing veto as {@link pairTogether}, over `[now − 3 s, now]`.
|
|
40335
|
+
* Displacement/cosine use the window, not the whole track.
|
|
40336
|
+
*/
|
|
40337
|
+
function pairTogetherNow(a, b, now) {
|
|
40338
|
+
const start = now - LIVE_WINDOW_MS;
|
|
40339
|
+
const overlapStart = Math.max(a.firstSeen, b.firstSeen, start);
|
|
40340
|
+
const overlapEnd = Math.min(a.lastSeen, b.lastSeen, now);
|
|
40431
40341
|
if (overlapEnd < overlapStart) return false;
|
|
40432
40342
|
let joint = 0;
|
|
40433
40343
|
let close = 0;
|
|
@@ -40444,55 +40354,245 @@ function pairTogether(a, b) {
|
|
|
40444
40354
|
}
|
|
40445
40355
|
if (joint === 0) return false;
|
|
40446
40356
|
if (close / joint < .5) return false;
|
|
40447
|
-
const
|
|
40448
|
-
const
|
|
40449
|
-
|
|
40357
|
+
const wa = positionsInWindow(a, start, now);
|
|
40358
|
+
const wb = positionsInWindow(b, start, now);
|
|
40359
|
+
const aMoves = windowRelativeMotion(wa) >= MOVE_EPS;
|
|
40360
|
+
const bMoves = windowRelativeMotion(wb) >= MOVE_EPS;
|
|
40361
|
+
if (aMoves && bMoves && cosine(windowDisplacement(wa), windowDisplacement(wb)) < .3) return false;
|
|
40450
40362
|
return true;
|
|
40451
40363
|
}
|
|
40452
|
-
|
|
40453
|
-
|
|
40454
|
-
|
|
40455
|
-
|
|
40456
|
-
|
|
40457
|
-
|
|
40458
|
-
|
|
40459
|
-
|
|
40460
|
-
|
|
40461
|
-
|
|
40462
|
-
|
|
40463
|
-
|
|
40464
|
-
|
|
40465
|
-
|
|
40466
|
-
|
|
40467
|
-
|
|
40468
|
-
|
|
40469
|
-
|
|
40470
|
-
|
|
40471
|
-
|
|
40472
|
-
|
|
40473
|
-
|
|
40474
|
-
|
|
40475
|
-
|
|
40476
|
-
const
|
|
40477
|
-
|
|
40478
|
-
|
|
40479
|
-
|
|
40480
|
-
|
|
40481
|
-
|
|
40482
|
-
|
|
40483
|
-
|
|
40484
|
-
|
|
40485
|
-
|
|
40486
|
-
|
|
40487
|
-
|
|
40488
|
-
|
|
40489
|
-
|
|
40490
|
-
|
|
40491
|
-
|
|
40492
|
-
|
|
40364
|
+
var GroupLiveAssigner = class {
|
|
40365
|
+
nextId;
|
|
40366
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
40367
|
+
groups = /* @__PURE__ */ new Map();
|
|
40368
|
+
membership = /* @__PURE__ */ new Map();
|
|
40369
|
+
/** `${trackId}>${groupId}` → first instant the join predicate held. */
|
|
40370
|
+
joinSince = /* @__PURE__ */ new Map();
|
|
40371
|
+
/** trackId → first instant they failed pair against every remaining member. */
|
|
40372
|
+
leaveSince = /* @__PURE__ */ new Map();
|
|
40373
|
+
constructor(nextId = () => randomUUID()) {
|
|
40374
|
+
this.nextId = nextId;
|
|
40375
|
+
}
|
|
40376
|
+
observe(track, now) {
|
|
40377
|
+
this.snapshots.set(track.trackId, track);
|
|
40378
|
+
if (!isEligibleMember(track)) {
|
|
40379
|
+
this.removeFromGroup(track.trackId);
|
|
40380
|
+
return;
|
|
40381
|
+
}
|
|
40382
|
+
if (!this.membership.has(track.trackId)) this.birthSingleton(track, now);
|
|
40383
|
+
this.considerLeave(track, now);
|
|
40384
|
+
const groupId = this.membership.get(track.trackId);
|
|
40385
|
+
const group = groupId !== void 0 ? this.groups.get(groupId) : void 0;
|
|
40386
|
+
if (group) for (const id of [...group.members]) {
|
|
40387
|
+
if (id === track.trackId) continue;
|
|
40388
|
+
const snap = this.snapshots.get(id);
|
|
40389
|
+
if (snap) this.considerLeave(snap, now);
|
|
40390
|
+
}
|
|
40391
|
+
this.considerJoin(track, now);
|
|
40392
|
+
}
|
|
40393
|
+
groupOf(trackId) {
|
|
40394
|
+
return this.membership.get(trackId);
|
|
40395
|
+
}
|
|
40396
|
+
snapshot(trackId) {
|
|
40397
|
+
return this.snapshots.get(trackId);
|
|
40398
|
+
}
|
|
40399
|
+
liveGroups() {
|
|
40400
|
+
return [...this.groups.values()].map((g) => this.toLive(g));
|
|
40401
|
+
}
|
|
40402
|
+
idleGroups(now, idleMs) {
|
|
40403
|
+
return this.liveGroups().filter((g) => now >= g.lastActiveAt + idleMs);
|
|
40404
|
+
}
|
|
40405
|
+
drop(ids) {
|
|
40406
|
+
for (const id of ids) {
|
|
40407
|
+
const g = this.groups.get(id);
|
|
40408
|
+
if (!g) continue;
|
|
40409
|
+
for (const member of g.members) this.membership.delete(member);
|
|
40410
|
+
this.groups.delete(id);
|
|
40411
|
+
}
|
|
40412
|
+
}
|
|
40413
|
+
birthSingleton(track, now) {
|
|
40414
|
+
const id = this.nextId();
|
|
40415
|
+
this.groups.set(id, {
|
|
40416
|
+
id,
|
|
40417
|
+
deviceId: track.deviceId,
|
|
40418
|
+
openedAt: now,
|
|
40419
|
+
members: new Set([track.trackId])
|
|
40493
40420
|
});
|
|
40421
|
+
this.membership.set(track.trackId, id);
|
|
40494
40422
|
}
|
|
40495
|
-
|
|
40423
|
+
considerLeave(track, now) {
|
|
40424
|
+
const groupId = this.membership.get(track.trackId);
|
|
40425
|
+
if (groupId === void 0) return;
|
|
40426
|
+
const group = this.groups.get(groupId);
|
|
40427
|
+
if (!group || group.members.size < 2) {
|
|
40428
|
+
this.leaveSince.delete(track.trackId);
|
|
40429
|
+
return;
|
|
40430
|
+
}
|
|
40431
|
+
if ([...group.members].filter((id) => id !== track.trackId).some((id) => {
|
|
40432
|
+
const other = this.snapshots.get(id);
|
|
40433
|
+
return other !== void 0 && pairTogetherNow(track, other, now);
|
|
40434
|
+
})) {
|
|
40435
|
+
this.leaveSince.delete(track.trackId);
|
|
40436
|
+
return;
|
|
40437
|
+
}
|
|
40438
|
+
const since = this.leaveSince.get(track.trackId) ?? now;
|
|
40439
|
+
this.leaveSince.set(track.trackId, since);
|
|
40440
|
+
if (now - since < 3e3) return;
|
|
40441
|
+
this.peel(track, now);
|
|
40442
|
+
}
|
|
40443
|
+
peel(track, now) {
|
|
40444
|
+
this.removeFromGroup(track.trackId);
|
|
40445
|
+
this.leaveSince.delete(track.trackId);
|
|
40446
|
+
this.birthSingleton(track, now);
|
|
40447
|
+
}
|
|
40448
|
+
considerJoin(track, now) {
|
|
40449
|
+
const mineId = this.membership.get(track.trackId);
|
|
40450
|
+
if (mineId === void 0) return;
|
|
40451
|
+
const mine = this.groups.get(mineId);
|
|
40452
|
+
if (!mine || mine.members.size !== 1) {
|
|
40453
|
+
this.clearJoinClocks(track.trackId);
|
|
40454
|
+
return;
|
|
40455
|
+
}
|
|
40456
|
+
let best;
|
|
40457
|
+
for (const group of this.groups.values()) {
|
|
40458
|
+
if (group.id === mineId || group.deviceId !== track.deviceId) continue;
|
|
40459
|
+
let gap = Number.POSITIVE_INFINITY;
|
|
40460
|
+
let together = false;
|
|
40461
|
+
for (const memberId of group.members) {
|
|
40462
|
+
const other = this.snapshots.get(memberId);
|
|
40463
|
+
if (!other) continue;
|
|
40464
|
+
if (pairTogetherNow(track, other, now)) {
|
|
40465
|
+
together = true;
|
|
40466
|
+
gap = Math.min(gap, centroidGap(track, other, now));
|
|
40467
|
+
}
|
|
40468
|
+
}
|
|
40469
|
+
if (!together) continue;
|
|
40470
|
+
if (best === void 0 || gap < best.gap) best = {
|
|
40471
|
+
groupId: group.id,
|
|
40472
|
+
gap
|
|
40473
|
+
};
|
|
40474
|
+
}
|
|
40475
|
+
if (best === void 0) {
|
|
40476
|
+
this.clearJoinClocks(track.trackId);
|
|
40477
|
+
return;
|
|
40478
|
+
}
|
|
40479
|
+
const key = `${track.trackId}>${best.groupId}`;
|
|
40480
|
+
for (const existing of [...this.joinSince.keys()]) if (existing.startsWith(`${track.trackId}>`) && existing !== key) this.joinSince.delete(existing);
|
|
40481
|
+
const since = this.joinSince.get(key) ?? now;
|
|
40482
|
+
this.joinSince.set(key, since);
|
|
40483
|
+
if (now - since < 1500) return;
|
|
40484
|
+
this.absorb(track.trackId, mineId, best.groupId);
|
|
40485
|
+
}
|
|
40486
|
+
absorb(trackId, fromId, intoId) {
|
|
40487
|
+
const into = this.groups.get(intoId);
|
|
40488
|
+
if (!into) return;
|
|
40489
|
+
into.members.add(trackId);
|
|
40490
|
+
this.membership.set(trackId, intoId);
|
|
40491
|
+
this.groups.delete(fromId);
|
|
40492
|
+
this.clearJoinClocks(trackId);
|
|
40493
|
+
this.leaveSince.delete(trackId);
|
|
40494
|
+
}
|
|
40495
|
+
removeFromGroup(trackId) {
|
|
40496
|
+
const groupId = this.membership.get(trackId);
|
|
40497
|
+
this.membership.delete(trackId);
|
|
40498
|
+
this.clearJoinClocks(trackId);
|
|
40499
|
+
this.leaveSince.delete(trackId);
|
|
40500
|
+
if (groupId === void 0) return;
|
|
40501
|
+
const group = this.groups.get(groupId);
|
|
40502
|
+
if (!group) return;
|
|
40503
|
+
group.members.delete(trackId);
|
|
40504
|
+
if (group.members.size === 0) this.groups.delete(groupId);
|
|
40505
|
+
}
|
|
40506
|
+
clearJoinClocks(trackId) {
|
|
40507
|
+
for (const key of [...this.joinSince.keys()]) if (key.startsWith(`${trackId}>`)) this.joinSince.delete(key);
|
|
40508
|
+
}
|
|
40509
|
+
toLive(group) {
|
|
40510
|
+
let lastActiveAt = group.openedAt;
|
|
40511
|
+
for (const id of group.members) {
|
|
40512
|
+
const snap = this.snapshots.get(id);
|
|
40513
|
+
if (snap && snap.lastSeen > lastActiveAt) lastActiveAt = snap.lastSeen;
|
|
40514
|
+
}
|
|
40515
|
+
return {
|
|
40516
|
+
id: group.id,
|
|
40517
|
+
deviceId: group.deviceId,
|
|
40518
|
+
memberTrackIds: [...group.members],
|
|
40519
|
+
openedAt: group.openedAt,
|
|
40520
|
+
lastActiveAt
|
|
40521
|
+
};
|
|
40522
|
+
}
|
|
40523
|
+
};
|
|
40524
|
+
function centroidGap(a, b, now) {
|
|
40525
|
+
const pa = a.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, a.positions[0]);
|
|
40526
|
+
const pb = b.positions.reduce((best, p) => Math.abs(p.timestamp - now) < Math.abs(best.timestamp - now) ? p : best, b.positions[0]);
|
|
40527
|
+
if (!pa || !pb) return Number.POSITIVE_INFINITY;
|
|
40528
|
+
const ca = bboxCentroid(pa.bbox);
|
|
40529
|
+
const cb = bboxCentroid(pb.bbox);
|
|
40530
|
+
return Math.hypot(ca.x - cb.x, ca.y - cb.y);
|
|
40531
|
+
}
|
|
40532
|
+
//#endregion
|
|
40533
|
+
//#region src/pipeline-analytics/group/group-session.ts
|
|
40534
|
+
/** Same idle as the notification burst (`NC_GROUP_DEFAULT_IDLE_SEC`). */
|
|
40535
|
+
var GROUP_SESSION_IDLE_MS = 3e4;
|
|
40536
|
+
//#endregion
|
|
40537
|
+
//#region src/pipeline-analytics/group/group-live-buffer.ts
|
|
40538
|
+
function toGroupableTrack(track) {
|
|
40539
|
+
return {
|
|
40540
|
+
trackId: track.trackId,
|
|
40541
|
+
deviceId: track.deviceId,
|
|
40542
|
+
className: track.className,
|
|
40543
|
+
firstSeen: track.firstSeen,
|
|
40544
|
+
lastSeen: track.lastSeen,
|
|
40545
|
+
positions: track.positions.map((p) => ({
|
|
40546
|
+
...p,
|
|
40547
|
+
bbox: { ...p.bbox }
|
|
40548
|
+
})),
|
|
40549
|
+
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
40550
|
+
};
|
|
40551
|
+
}
|
|
40552
|
+
var GroupLiveBuffer = class {
|
|
40553
|
+
assigner;
|
|
40554
|
+
primed = /* @__PURE__ */ new Set();
|
|
40555
|
+
constructor(nextId) {
|
|
40556
|
+
this.assigner = new GroupLiveAssigner(nextId);
|
|
40557
|
+
}
|
|
40558
|
+
observe(track, now) {
|
|
40559
|
+
this.assigner.observe(track, now);
|
|
40560
|
+
}
|
|
40561
|
+
groupOf(trackId) {
|
|
40562
|
+
return this.assigner.groupOf(trackId);
|
|
40563
|
+
}
|
|
40564
|
+
snapshot(trackId) {
|
|
40565
|
+
return this.assigner.snapshot(trackId);
|
|
40566
|
+
}
|
|
40567
|
+
liveGroups() {
|
|
40568
|
+
return this.assigner.liveGroups();
|
|
40569
|
+
}
|
|
40570
|
+
idleGroups(now, idleMs = GROUP_SESSION_IDLE_MS) {
|
|
40571
|
+
return this.assigner.idleGroups(now, idleMs);
|
|
40572
|
+
}
|
|
40573
|
+
drop(ids) {
|
|
40574
|
+
this.assigner.drop(ids);
|
|
40575
|
+
}
|
|
40576
|
+
isPrimed(deviceId) {
|
|
40577
|
+
return this.primed.has(deviceId);
|
|
40578
|
+
}
|
|
40579
|
+
markPrimed(deviceId) {
|
|
40580
|
+
this.primed.add(deviceId);
|
|
40581
|
+
}
|
|
40582
|
+
};
|
|
40583
|
+
//#endregion
|
|
40584
|
+
//#region src/pipeline-analytics/group/group-crop.ts
|
|
40585
|
+
/** Always containSubject: true. Never the 0.5 delivered threshold. */
|
|
40586
|
+
function groupWideCentralSquareLayout(unionBbox, frame) {
|
|
40587
|
+
return wideCentralSquareLayout(unionBbox, frame, { containSubject: true });
|
|
40588
|
+
}
|
|
40589
|
+
async function cropGroupFromRgb(rgb, frameWidth, frameHeight, unionBbox) {
|
|
40590
|
+
const layout = groupWideCentralSquareLayout(unionBbox, {
|
|
40591
|
+
W: frameWidth,
|
|
40592
|
+
H: frameHeight
|
|
40593
|
+
});
|
|
40594
|
+
const { crop: slab } = await extractCrop(rgb, frameWidth, frameHeight, layout.fetch);
|
|
40595
|
+
return composeWideCentralSquareThumbnail(slab, layout);
|
|
40496
40596
|
}
|
|
40497
40597
|
//#endregion
|
|
40498
40598
|
//#region src/pipeline-analytics/group/group-instant.ts
|
|
@@ -40509,6 +40609,7 @@ function clusterGroups(tracks) {
|
|
|
40509
40609
|
*
|
|
40510
40610
|
* Retrospective. No new capture path.
|
|
40511
40611
|
*/
|
|
40612
|
+
var GROUP_MEDIA_MATCH_MS = 2e3;
|
|
40512
40613
|
function nearest(positions, t, windowMs) {
|
|
40513
40614
|
let best;
|
|
40514
40615
|
let bestDt = windowMs + 1;
|
|
@@ -40617,55 +40718,85 @@ function chooseAnchorTrackId(members, ownsCleanFrame) {
|
|
|
40617
40718
|
return life !== 0 ? life : a.trackId.localeCompare(b.trackId);
|
|
40618
40719
|
})[0].trackId;
|
|
40619
40720
|
}
|
|
40620
|
-
//#endregion
|
|
40621
|
-
//#region src/pipeline-analytics/group/group-session.ts
|
|
40622
40721
|
/**
|
|
40623
|
-
*
|
|
40722
|
+
* The RGB source for a group crop is a full native frame. Thumbnails are
|
|
40723
|
+
* already single-subject crops and cannot substitute for union-bbox geometry.
|
|
40624
40724
|
*
|
|
40625
|
-
*
|
|
40626
|
-
*
|
|
40627
|
-
*
|
|
40628
|
-
*
|
|
40629
|
-
* Merge and split cannot occur: connected components of a closed session is a
|
|
40630
|
-
* partition. Say it out loud so a later live-group attempt does not reach
|
|
40631
|
-
* for them here.
|
|
40725
|
+
* Prefer a keyFrame within {@link GROUP_MEDIA_MATCH_MS} of the chosen instant;
|
|
40726
|
+
* if none, take the nearest keyFrame anyway — dropping the crop because the
|
|
40727
|
+
* best-shot was 3 s off the instant is how a recognised person left the
|
|
40728
|
+
* digest with no crop.
|
|
40632
40729
|
*/
|
|
40633
|
-
|
|
40634
|
-
|
|
40635
|
-
|
|
40636
|
-
|
|
40730
|
+
function pickCleanPixelRow(rows, instantMs) {
|
|
40731
|
+
const ranked = (maxDt) => {
|
|
40732
|
+
const candidates = [];
|
|
40733
|
+
for (const row of rows) {
|
|
40734
|
+
const rank = row.kind === "keyFrame" ? 0 : row.kind === "keyFrameSmall" ? 1 : -1;
|
|
40735
|
+
if (rank < 0) continue;
|
|
40736
|
+
const dt = Math.abs(row.timestamp - instantMs);
|
|
40737
|
+
if (maxDt !== null && dt > maxDt) continue;
|
|
40738
|
+
candidates.push({
|
|
40739
|
+
key: row.key,
|
|
40740
|
+
dt,
|
|
40741
|
+
rank
|
|
40742
|
+
});
|
|
40743
|
+
}
|
|
40744
|
+
return candidates.toSorted((a, b) => a.rank - b.rank || a.dt - b.dt)[0];
|
|
40745
|
+
};
|
|
40746
|
+
const near = ranked(GROUP_MEDIA_MATCH_MS);
|
|
40747
|
+
if (near !== void 0) return {
|
|
40748
|
+
key: near.key,
|
|
40749
|
+
usedFallback: false
|
|
40750
|
+
};
|
|
40751
|
+
const any = ranked(null);
|
|
40752
|
+
if (any === void 0) return null;
|
|
40753
|
+
return {
|
|
40754
|
+
key: any.key,
|
|
40755
|
+
usedFallback: true
|
|
40756
|
+
};
|
|
40637
40757
|
}
|
|
40638
|
-
|
|
40758
|
+
//#endregion
|
|
40759
|
+
//#region src/pipeline-analytics/group/group-live-replay.ts
|
|
40760
|
+
function sliceTrackAt(track, now) {
|
|
40761
|
+
const positions = track.positions.filter((p) => p.timestamp <= now);
|
|
40762
|
+
const last = positions[positions.length - 1];
|
|
40639
40763
|
return {
|
|
40640
|
-
|
|
40641
|
-
lastSeen:
|
|
40764
|
+
...track,
|
|
40765
|
+
lastSeen: last?.timestamp ?? track.firstSeen,
|
|
40766
|
+
positions
|
|
40642
40767
|
};
|
|
40643
40768
|
}
|
|
40644
|
-
|
|
40645
|
-
|
|
40646
|
-
|
|
40647
|
-
|
|
40648
|
-
|
|
40649
|
-
|
|
40650
|
-
|
|
40651
|
-
|
|
40652
|
-
|
|
40653
|
-
|
|
40654
|
-
|
|
40655
|
-
const ready = [];
|
|
40656
|
-
for (const [deviceId, tracks] of byDevice) {
|
|
40657
|
-
const deviceActive = active.filter((a) => a.deviceId === deviceId);
|
|
40658
|
-
for (const session of sessionsOf(tracks)) {
|
|
40659
|
-
const span = sessionSpan(session);
|
|
40660
|
-
if (now < span.lastSeen + idleMs) continue;
|
|
40661
|
-
if (deviceActive.some((a) => overlaps(span, a))) continue;
|
|
40662
|
-
ready.push(session);
|
|
40769
|
+
function replayInto(tracks, observer) {
|
|
40770
|
+
const events = [];
|
|
40771
|
+
const byId = /* @__PURE__ */ new Map();
|
|
40772
|
+
for (const track of tracks) {
|
|
40773
|
+
byId.set(track.trackId, track);
|
|
40774
|
+
if (track.positions.length === 0) {
|
|
40775
|
+
events.push({
|
|
40776
|
+
t: track.lastSeen,
|
|
40777
|
+
trackId: track.trackId
|
|
40778
|
+
});
|
|
40779
|
+
continue;
|
|
40663
40780
|
}
|
|
40781
|
+
for (const p of track.positions) events.push({
|
|
40782
|
+
t: p.timestamp,
|
|
40783
|
+
trackId: track.trackId
|
|
40784
|
+
});
|
|
40785
|
+
}
|
|
40786
|
+
events.sort((a, b) => a.t - b.t || a.trackId.localeCompare(b.trackId));
|
|
40787
|
+
let lastKey = "";
|
|
40788
|
+
for (const e of events) {
|
|
40789
|
+
const key = `${e.trackId}@${e.t}`;
|
|
40790
|
+
if (key === lastKey) continue;
|
|
40791
|
+
lastKey = key;
|
|
40792
|
+
const track = byId.get(e.trackId);
|
|
40793
|
+
if (!track) continue;
|
|
40794
|
+
observer.observe(sliceTrackAt(track, e.t), e.t);
|
|
40664
40795
|
}
|
|
40665
|
-
return ready;
|
|
40666
40796
|
}
|
|
40667
|
-
function
|
|
40668
|
-
|
|
40797
|
+
function replayLive(tracks, assigner = new GroupLiveAssigner()) {
|
|
40798
|
+
replayInto(tracks, assigner);
|
|
40799
|
+
return assigner;
|
|
40669
40800
|
}
|
|
40670
40801
|
//#endregion
|
|
40671
40802
|
//#region src/pipeline-analytics/group/group-sealer.ts
|
|
@@ -40673,9 +40804,9 @@ var DEFAULT_FRAME = {
|
|
|
40673
40804
|
width: 640,
|
|
40674
40805
|
height: 360
|
|
40675
40806
|
};
|
|
40676
|
-
var SEAL_LOOKBACK_MS = 10 * 6e4;
|
|
40677
|
-
var BACKFILL_LOOKBACK_MS = 2880 * 6e4;
|
|
40678
40807
|
var QUERY_LIMIT = 2e3;
|
|
40808
|
+
/** Crash recover: replay this camera only, on its next activity. */
|
|
40809
|
+
var GROUP_CRASH_LOOKBACK_MS = 2 * 6e4;
|
|
40679
40810
|
function toInstant(t) {
|
|
40680
40811
|
return {
|
|
40681
40812
|
trackId: t.trackId,
|
|
@@ -40689,93 +40820,164 @@ function toInstant(t) {
|
|
|
40689
40820
|
confidence: t.importance
|
|
40690
40821
|
};
|
|
40691
40822
|
}
|
|
40823
|
+
function memberBoxesAt(members, at, frame) {
|
|
40824
|
+
const w = Math.max(frame.width, 1);
|
|
40825
|
+
const h = Math.max(frame.height, 1);
|
|
40826
|
+
const boxes = [];
|
|
40827
|
+
for (const member of members) {
|
|
40828
|
+
let best = member.positions[0];
|
|
40829
|
+
let bestDt = Number.POSITIVE_INFINITY;
|
|
40830
|
+
for (const p of member.positions) {
|
|
40831
|
+
const dt = Math.abs(p.timestamp - at);
|
|
40832
|
+
if (dt <= 1e3 && dt < bestDt) {
|
|
40833
|
+
best = p;
|
|
40834
|
+
bestDt = dt;
|
|
40835
|
+
}
|
|
40836
|
+
}
|
|
40837
|
+
if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
|
|
40838
|
+
boxes.push({
|
|
40839
|
+
bbox: {
|
|
40840
|
+
x: best.bbox.x / w,
|
|
40841
|
+
y: best.bbox.y / h,
|
|
40842
|
+
w: best.bbox.w / w,
|
|
40843
|
+
h: best.bbox.h / h
|
|
40844
|
+
},
|
|
40845
|
+
label: member.className,
|
|
40846
|
+
observedAt: best.timestamp
|
|
40847
|
+
});
|
|
40848
|
+
}
|
|
40849
|
+
return boxes;
|
|
40850
|
+
}
|
|
40851
|
+
function toSealer(track) {
|
|
40852
|
+
return {
|
|
40853
|
+
trackId: track.trackId,
|
|
40854
|
+
deviceId: track.deviceId,
|
|
40855
|
+
className: track.className,
|
|
40856
|
+
firstSeen: track.firstSeen,
|
|
40857
|
+
lastSeen: track.lastSeen,
|
|
40858
|
+
positions: track.positions,
|
|
40859
|
+
...track.closedReason !== void 0 ? { closedReason: track.closedReason } : {}
|
|
40860
|
+
};
|
|
40861
|
+
}
|
|
40692
40862
|
var GroupSealer = class {
|
|
40693
40863
|
deps;
|
|
40694
40864
|
constructor(deps) {
|
|
40695
40865
|
this.deps = deps;
|
|
40696
40866
|
}
|
|
40697
|
-
|
|
40698
|
-
|
|
40699
|
-
return this.sealSince(now - SEAL_LOOKBACK_MS, now);
|
|
40700
|
-
}
|
|
40701
|
-
async backfill() {
|
|
40702
|
-
const now = this.deps.now();
|
|
40703
|
-
return this.sealSince(now - BACKFILL_LOOKBACK_MS, now);
|
|
40867
|
+
get processEnabled() {
|
|
40868
|
+
return this.deps.enabled ?? false;
|
|
40704
40869
|
}
|
|
40705
|
-
async
|
|
40870
|
+
async flushIdle(buffer, now = this.deps.now()) {
|
|
40871
|
+
if (!this.processEnabled) return 0;
|
|
40706
40872
|
let written = 0;
|
|
40707
|
-
|
|
40873
|
+
for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
|
|
40874
|
+
buffer.drop([group.id]);
|
|
40875
|
+
written += 1;
|
|
40876
|
+
}
|
|
40877
|
+
return written;
|
|
40878
|
+
}
|
|
40879
|
+
/**
|
|
40880
|
+
* First activity on camera D after a crash: replay 2 minutes of that camera
|
|
40881
|
+
* into the live buffer, persist whoever is already idle, keep the rest in RAM.
|
|
40882
|
+
*/
|
|
40883
|
+
async recoverCamera(deviceId, buffer) {
|
|
40884
|
+
if (!this.processEnabled) return 0;
|
|
40885
|
+
if (buffer.isPrimed(deviceId)) return 0;
|
|
40886
|
+
const now = this.deps.now();
|
|
40708
40887
|
try {
|
|
40709
|
-
|
|
40888
|
+
replayInto(await this.deps.trackStore.queryHistorical({
|
|
40889
|
+
deviceId,
|
|
40890
|
+
since: now - GROUP_CRASH_LOOKBACK_MS,
|
|
40891
|
+
until: now,
|
|
40892
|
+
limit: QUERY_LIMIT,
|
|
40893
|
+
projection: "full"
|
|
40894
|
+
}), buffer);
|
|
40710
40895
|
} catch (err) {
|
|
40711
|
-
this.deps.logger.warn("group sealer:
|
|
40712
|
-
|
|
40896
|
+
this.deps.logger.warn("group sealer: lookback failed", {
|
|
40897
|
+
tags: { deviceId },
|
|
40898
|
+
meta: { error: String(err) }
|
|
40899
|
+
});
|
|
40900
|
+
} finally {
|
|
40901
|
+
buffer.markPrimed(deviceId);
|
|
40713
40902
|
}
|
|
40714
|
-
|
|
40715
|
-
|
|
40903
|
+
return this.flushIdle(buffer, now);
|
|
40904
|
+
}
|
|
40905
|
+
/**
|
|
40906
|
+
* Seal ONE camera over a caller-owned window via live replay.
|
|
40907
|
+
*
|
|
40908
|
+
* This is how a digest recovers groups without a boot scan: the producer
|
|
40909
|
+
* already knows the window. Walking every camera for 48h at initialize()
|
|
40910
|
+
* is what wedged hub-main.
|
|
40911
|
+
*/
|
|
40912
|
+
async sealWindow(deviceId, since, until) {
|
|
40913
|
+
if (!this.processEnabled) return 0;
|
|
40914
|
+
try {
|
|
40915
|
+
const closed = await this.deps.trackStore.queryHistorical({
|
|
40916
|
+
deviceId,
|
|
40917
|
+
since,
|
|
40918
|
+
until,
|
|
40919
|
+
limit: QUERY_LIMIT,
|
|
40920
|
+
projection: "full"
|
|
40921
|
+
});
|
|
40922
|
+
if (closed.length === 0) return 0;
|
|
40923
|
+
const already = new Set((await this.deps.groupStore.membersOfTracks(closed.map((t) => t.trackId))).map((m) => m.trackId));
|
|
40924
|
+
const pending = closed.filter((t) => !already.has(t.trackId));
|
|
40925
|
+
if (pending.length === 0) return 0;
|
|
40926
|
+
const assigner = replayLive(pending);
|
|
40927
|
+
const now = this.deps.now();
|
|
40928
|
+
const byId = new Map(pending.map((t) => [t.trackId, t]));
|
|
40929
|
+
let written = 0;
|
|
40930
|
+
for (const group of assigner.idleGroups(now, GROUP_SESSION_IDLE_MS)) {
|
|
40931
|
+
const members = group.memberTrackIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
40932
|
+
if (members.length === 0) continue;
|
|
40933
|
+
try {
|
|
40934
|
+
await this.persistPartition(members, group.openedAt);
|
|
40935
|
+
written += 1;
|
|
40936
|
+
} catch (err) {
|
|
40937
|
+
this.deps.logger.warn("group sealer: persist failed", {
|
|
40938
|
+
tags: { deviceId },
|
|
40939
|
+
meta: {
|
|
40940
|
+
members: members.map((m) => m.trackId),
|
|
40941
|
+
error: String(err)
|
|
40942
|
+
}
|
|
40943
|
+
});
|
|
40944
|
+
}
|
|
40945
|
+
}
|
|
40946
|
+
return written;
|
|
40716
40947
|
} catch (err) {
|
|
40717
40948
|
this.deps.logger.warn("group sealer: device failed", {
|
|
40718
40949
|
tags: { deviceId },
|
|
40719
40950
|
meta: { error: String(err) }
|
|
40720
40951
|
});
|
|
40952
|
+
return 0;
|
|
40721
40953
|
}
|
|
40722
|
-
return written;
|
|
40723
40954
|
}
|
|
40724
|
-
async
|
|
40725
|
-
const
|
|
40726
|
-
|
|
40727
|
-
|
|
40728
|
-
|
|
40729
|
-
|
|
40730
|
-
|
|
40731
|
-
|
|
40732
|
-
|
|
40733
|
-
|
|
40734
|
-
|
|
40735
|
-
|
|
40736
|
-
const active = this.deps.trackStore.getActive(deviceId).map((t) => ({
|
|
40737
|
-
deviceId,
|
|
40738
|
-
firstSeen: t.firstSeen,
|
|
40739
|
-
lastSeen: t.lastSeen
|
|
40740
|
-
}));
|
|
40741
|
-
const partitions = partitionsForSealedSessions(pending.map(toInstant), active, now, GROUP_SESSION_IDLE_MS);
|
|
40742
|
-
if (partitions.length === 0) {
|
|
40743
|
-
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", {
|
|
40955
|
+
async persistLiveGroup(group, buffer) {
|
|
40956
|
+
const deviceId = group.deviceId;
|
|
40957
|
+
const snaps = group.memberTrackIds.map((id) => buffer.snapshot(id)).filter((t) => t !== void 0);
|
|
40958
|
+
if (snaps.length === 0) return true;
|
|
40959
|
+
const already = new Set((await this.deps.groupStore.membersOfTracks(snaps.map((t) => t.trackId))).map((m) => m.trackId));
|
|
40960
|
+
const pending = snaps.filter((t) => !already.has(t.trackId)).map(toSealer);
|
|
40961
|
+
if (pending.length === 0) return true;
|
|
40962
|
+
try {
|
|
40963
|
+
await this.persistPartition(pending, group.openedAt);
|
|
40964
|
+
return true;
|
|
40965
|
+
} catch (err) {
|
|
40966
|
+
this.deps.logger.warn("group sealer: persist failed", {
|
|
40744
40967
|
tags: { deviceId },
|
|
40745
40968
|
meta: {
|
|
40746
|
-
|
|
40747
|
-
|
|
40969
|
+
members: pending.map((m) => m.trackId),
|
|
40970
|
+
error: String(err)
|
|
40748
40971
|
}
|
|
40749
40972
|
});
|
|
40750
|
-
return
|
|
40751
|
-
}
|
|
40752
|
-
const byId = new Map(pending.map((t) => [t.trackId, t]));
|
|
40753
|
-
let written = 0;
|
|
40754
|
-
for (const part of partitions) {
|
|
40755
|
-
const members = part.memberTrackIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
|
|
40756
|
-
if (members.length === 0) continue;
|
|
40757
|
-
if (members.every((m) => already.has(m.trackId))) continue;
|
|
40758
|
-
try {
|
|
40759
|
-
await this.persistPartition(members);
|
|
40760
|
-
written += 1;
|
|
40761
|
-
} catch (err) {
|
|
40762
|
-
this.deps.logger.warn("group sealer: persist failed", {
|
|
40763
|
-
tags: { deviceId },
|
|
40764
|
-
meta: {
|
|
40765
|
-
members: members.map((m) => m.trackId),
|
|
40766
|
-
error: String(err)
|
|
40767
|
-
}
|
|
40768
|
-
});
|
|
40769
|
-
}
|
|
40973
|
+
return false;
|
|
40770
40974
|
}
|
|
40771
|
-
return written;
|
|
40772
40975
|
}
|
|
40773
|
-
async persistPartition(members) {
|
|
40976
|
+
async persistPartition(members, openedAt) {
|
|
40774
40977
|
const deviceId = members[0].deviceId;
|
|
40775
40978
|
const frame = this.deps.frameDims(deviceId) ?? DEFAULT_FRAME;
|
|
40776
40979
|
const instantMembers = members.map(toInstant);
|
|
40777
40980
|
const instant = chooseGroupInstant(instantMembers, frame);
|
|
40778
|
-
const openedAt = Math.min(...members.map((m) => m.firstSeen));
|
|
40779
40981
|
const closedAt = Math.max(...members.map((m) => m.lastSeen));
|
|
40780
40982
|
const timestamp = instant?.timestamp ?? closedAt;
|
|
40781
40983
|
const mediaInfos = await Promise.all(members.map(async (m) => ({
|
|
@@ -40811,29 +41013,66 @@ var GroupSealer = class {
|
|
|
40811
41013
|
nativeKey
|
|
40812
41014
|
});
|
|
40813
41015
|
if (members.length < 2 || instant === null) return;
|
|
40814
|
-
const source = await this.pickCleanPixels(mediaInfos, timestamp);
|
|
40815
|
-
if (source === null)
|
|
41016
|
+
const source = await this.pickCleanPixels(mediaInfos, timestamp, deviceId);
|
|
41017
|
+
if (source === null) {
|
|
41018
|
+
this.deps.logger.info("group sealer: no clean pixels for crop", {
|
|
41019
|
+
tags: { deviceId },
|
|
41020
|
+
meta: {
|
|
41021
|
+
groupId: draft.id,
|
|
41022
|
+
members: members.map((m) => m.trackId)
|
|
41023
|
+
}
|
|
41024
|
+
});
|
|
41025
|
+
return;
|
|
41026
|
+
}
|
|
40816
41027
|
try {
|
|
40817
|
-
const raw = await sharp(source.jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
40818
|
-
if (raw.info.channels
|
|
40819
|
-
|
|
40820
|
-
|
|
40821
|
-
|
|
40822
|
-
|
|
40823
|
-
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
41028
|
+
const raw = await sharp(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
|
|
41029
|
+
if (raw.info.channels === 3) {
|
|
41030
|
+
const union = {
|
|
41031
|
+
x: instant.ux * raw.info.width,
|
|
41032
|
+
y: instant.uy * raw.info.height,
|
|
41033
|
+
w: instant.uw * raw.info.width,
|
|
41034
|
+
h: instant.uh * raw.info.height
|
|
41035
|
+
};
|
|
41036
|
+
const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
|
|
41037
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
41038
|
+
deviceId,
|
|
41039
|
+
ownerKind: "group",
|
|
41040
|
+
ownerId: draft.id,
|
|
41041
|
+
kind: "thumbnail",
|
|
41042
|
+
timestamp,
|
|
41043
|
+
data: crop
|
|
41044
|
+
});
|
|
41045
|
+
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
41046
|
+
}
|
|
41047
|
+
} catch (err) {
|
|
41048
|
+
this.deps.logger.warn("group sealer: crop failed", {
|
|
41049
|
+
tags: { deviceId },
|
|
41050
|
+
meta: {
|
|
41051
|
+
groupId: draft.id,
|
|
41052
|
+
error: String(err)
|
|
41053
|
+
}
|
|
41054
|
+
});
|
|
41055
|
+
}
|
|
41056
|
+
try {
|
|
41057
|
+
const composed = await composeGroupFrame({
|
|
41058
|
+
jpeg: source.jpeg,
|
|
41059
|
+
boxes: memberBoxesAt(instantMembers, timestamp, frame),
|
|
41060
|
+
at: timestamp,
|
|
41061
|
+
logger: this.deps.logger,
|
|
41062
|
+
deviceId,
|
|
41063
|
+
groupKey: draft.id
|
|
41064
|
+
});
|
|
41065
|
+
if (composed === null) return;
|
|
41066
|
+
await this.deps.mediaStore.put({
|
|
40827
41067
|
deviceId,
|
|
40828
41068
|
ownerKind: "group",
|
|
40829
41069
|
ownerId: draft.id,
|
|
40830
|
-
kind: "
|
|
41070
|
+
kind: "fullFrameBoxed",
|
|
40831
41071
|
timestamp,
|
|
40832
|
-
data:
|
|
41072
|
+
data: composed.bytes
|
|
40833
41073
|
});
|
|
40834
|
-
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
40835
41074
|
} catch (err) {
|
|
40836
|
-
this.deps.logger.warn("group sealer:
|
|
41075
|
+
this.deps.logger.warn("group sealer: boxed frame failed", {
|
|
40837
41076
|
tags: { deviceId },
|
|
40838
41077
|
meta: {
|
|
40839
41078
|
groupId: draft.id,
|
|
@@ -40842,27 +41081,53 @@ var GroupSealer = class {
|
|
|
40842
41081
|
});
|
|
40843
41082
|
}
|
|
40844
41083
|
}
|
|
40845
|
-
async pickCleanPixels(mediaInfos, instantMs) {
|
|
40846
|
-
const
|
|
40847
|
-
|
|
40848
|
-
|
|
40849
|
-
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
40853
|
-
|
|
40854
|
-
|
|
40855
|
-
rank
|
|
40856
|
-
});
|
|
40857
|
-
}
|
|
40858
|
-
const chosen = candidates.toSorted((a, b) => a.rank - b.rank || a.dt - b.dt)[0];
|
|
40859
|
-
if (chosen === void 0) return null;
|
|
41084
|
+
async pickCleanPixels(mediaInfos, instantMs, deviceId) {
|
|
41085
|
+
const chosen = pickCleanPixelRow(mediaInfos.flatMap((info) => info.rows), instantMs);
|
|
41086
|
+
if (chosen === null) return null;
|
|
41087
|
+
if (chosen.usedFallback) this.deps.logger.info("group sealer: crop pixels not within 2s of instant, using nearest keyFrame", {
|
|
41088
|
+
tags: { deviceId },
|
|
41089
|
+
meta: {
|
|
41090
|
+
instantMs,
|
|
41091
|
+
mediaKey: chosen.key
|
|
41092
|
+
}
|
|
41093
|
+
});
|
|
40860
41094
|
const file = await this.deps.mediaStore.getByKey(chosen.key);
|
|
40861
41095
|
if (file === null) return null;
|
|
40862
41096
|
return { jpeg: Buffer.from(file.base64, "base64") };
|
|
40863
41097
|
}
|
|
40864
41098
|
};
|
|
40865
41099
|
//#endregion
|
|
41100
|
+
//#region src/pipeline-analytics/group/group-cursor.ts
|
|
41101
|
+
function encodeGroupCursor(cursor) {
|
|
41102
|
+
return Buffer.from(JSON.stringify({
|
|
41103
|
+
c: cursor.closedAt,
|
|
41104
|
+
i: cursor.groupId
|
|
41105
|
+
}), "utf8").toString("base64url");
|
|
41106
|
+
}
|
|
41107
|
+
function decodeGroupCursor(raw) {
|
|
41108
|
+
let parsed;
|
|
41109
|
+
try {
|
|
41110
|
+
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
41111
|
+
} catch {
|
|
41112
|
+
throw new Error("listGroups: malformed cursor");
|
|
41113
|
+
}
|
|
41114
|
+
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");
|
|
41115
|
+
return {
|
|
41116
|
+
closedAt: parsed.c,
|
|
41117
|
+
groupId: parsed.i
|
|
41118
|
+
};
|
|
41119
|
+
}
|
|
41120
|
+
function compareGroupDesc(a, b) {
|
|
41121
|
+
if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
|
|
41122
|
+
if (a.id === b.id) return 0;
|
|
41123
|
+
return a.id < b.id ? 1 : -1;
|
|
41124
|
+
}
|
|
41125
|
+
/** True when `row` sits strictly AFTER the cursor in DESC order. */
|
|
41126
|
+
function isAfterGroupCursor(row, cursor) {
|
|
41127
|
+
if (row.closedAt < cursor.closedAt) return true;
|
|
41128
|
+
return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
|
|
41129
|
+
}
|
|
41130
|
+
//#endregion
|
|
40866
41131
|
//#region src/pipeline-analytics/group/group-store.ts
|
|
40867
41132
|
/**
|
|
40868
41133
|
* The group ENTITY store — a co-moving partition of one session, with one
|
|
@@ -40884,8 +41149,8 @@ var GroupSealer = class {
|
|
|
40884
41149
|
*/
|
|
40885
41150
|
/**
|
|
40886
41151
|
* @durable class=ledger owner=pipeline-analytics
|
|
40887
|
-
* write="one row per co-moving group at
|
|
40888
|
-
* Membership is
|
|
41152
|
+
* write="one row per co-moving group, sealed at idle — never a live row.
|
|
41153
|
+
* Membership is the RAM members at persist, not a session partition. mediaKey is
|
|
40889
41154
|
* the group's own crop; nativeKey is a POINTER to the anchor member's
|
|
40890
41155
|
* keyFrame, nulled if that member is deleted alone. anchorTrackId and
|
|
40891
41156
|
* nativeKey are the only fields rewritten after birth."
|
|
@@ -41124,6 +41389,24 @@ var TrackGroupStore = class {
|
|
|
41124
41389
|
data: r.data
|
|
41125
41390
|
}));
|
|
41126
41391
|
}
|
|
41392
|
+
/**
|
|
41393
|
+
* Membership for a page of groups in ONE query. `listGroupRecords` used to
|
|
41394
|
+
* call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
|
|
41395
|
+
* ~20 s, and the viewer feed times out into a retry loop.
|
|
41396
|
+
*/
|
|
41397
|
+
async listMembersForGroups(groupIds) {
|
|
41398
|
+
if (groupIds.length === 0) return [];
|
|
41399
|
+
return (await this.store.query.query({
|
|
41400
|
+
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
41401
|
+
filter: {
|
|
41402
|
+
whereIn: { groupId: [...groupIds] },
|
|
41403
|
+
limit: 1e4
|
|
41404
|
+
}
|
|
41405
|
+
})).map((r) => this.normalizeMember({
|
|
41406
|
+
id: r.id,
|
|
41407
|
+
data: r.data
|
|
41408
|
+
}));
|
|
41409
|
+
}
|
|
41127
41410
|
async membersOfTracks(trackIds) {
|
|
41128
41411
|
const out = [];
|
|
41129
41412
|
for (const trackId of trackIds) {
|
|
@@ -41139,20 +41422,29 @@ var TrackGroupStore = class {
|
|
|
41139
41422
|
return out;
|
|
41140
41423
|
}
|
|
41141
41424
|
/**
|
|
41142
|
-
* Newest-closed page for the
|
|
41425
|
+
* Newest-closed page for the Groups feed.
|
|
41143
41426
|
*
|
|
41144
41427
|
* Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
|
|
41145
41428
|
* same as {@link listOverlapping}. Pagination is applied AFTER the window
|
|
41146
41429
|
* so a day cursor is honest rather than a raw-row offset that skips live
|
|
41147
|
-
* groups on a busy camera.
|
|
41430
|
+
* groups on a busy camera. `deviceIds: []` is an empty page (same as
|
|
41431
|
+
* `listRecentTracks`); omit the field for every camera. `deviceId` stays
|
|
41432
|
+
* as the single-camera alias the custom-action bridge still sends.
|
|
41148
41433
|
*/
|
|
41149
41434
|
async listRecent(input) {
|
|
41150
41435
|
const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
|
|
41151
41436
|
const offset = Math.max(input.offset ?? 0, 0);
|
|
41152
|
-
const
|
|
41437
|
+
const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
|
|
41438
|
+
if (deviceIds !== void 0 && deviceIds.length === 0) return {
|
|
41439
|
+
groups: [],
|
|
41440
|
+
hasMore: false,
|
|
41441
|
+
nextCursor: null
|
|
41442
|
+
};
|
|
41443
|
+
const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
|
|
41444
|
+
const filtered = (await this.store.query.query({
|
|
41153
41445
|
collection: TRACK_GROUPS_COLLECTION,
|
|
41154
41446
|
filter: {
|
|
41155
|
-
...
|
|
41447
|
+
...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
|
|
41156
41448
|
orderBy: {
|
|
41157
41449
|
field: "closedAt",
|
|
41158
41450
|
direction: "desc"
|
|
@@ -41165,11 +41457,20 @@ var TrackGroupStore = class {
|
|
|
41165
41457
|
})).filter((g) => {
|
|
41166
41458
|
if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
|
|
41167
41459
|
if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
|
|
41460
|
+
if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
|
|
41168
41461
|
return true;
|
|
41169
|
-
}).
|
|
41462
|
+
}).toSorted(compareGroupDesc);
|
|
41463
|
+
const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
|
|
41464
|
+
const groups = slice.slice(0, limit);
|
|
41465
|
+
const hasMore = slice.length > limit;
|
|
41466
|
+
const last = groups[groups.length - 1];
|
|
41170
41467
|
return {
|
|
41171
|
-
groups
|
|
41172
|
-
hasMore
|
|
41468
|
+
groups,
|
|
41469
|
+
hasMore,
|
|
41470
|
+
nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
|
|
41471
|
+
closedAt: last.closedAt,
|
|
41472
|
+
groupId: last.id
|
|
41473
|
+
}) : null
|
|
41173
41474
|
};
|
|
41174
41475
|
}
|
|
41175
41476
|
/** Groups whose life overlaps `[since, until]` on one camera. */
|
|
@@ -42501,7 +42802,7 @@ function evaluateStationaryPromotion(input) {
|
|
|
42501
42802
|
};
|
|
42502
42803
|
}
|
|
42503
42804
|
var DEFAULT_PACKAGE_STILLNESS_CONFIG = {
|
|
42504
|
-
|
|
42805
|
+
minHits: 3,
|
|
42505
42806
|
roundGapMs: 5e3,
|
|
42506
42807
|
ttlMs: 5 * 6e4,
|
|
42507
42808
|
assocIou: .3
|
|
@@ -42534,8 +42835,9 @@ var PackageStillnessTracker = class {
|
|
|
42534
42835
|
const prev = next[index];
|
|
42535
42836
|
if (prev === void 0) continue;
|
|
42536
42837
|
consumed.add(prev.id);
|
|
42537
|
-
const updated = confirm(prev, input, s
|
|
42538
|
-
|
|
42838
|
+
const updated = confirm(prev, input, s);
|
|
42839
|
+
const minHits = input.minHits ?? this.config.minHits;
|
|
42840
|
+
if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
|
|
42539
42841
|
next[index] = updated;
|
|
42540
42842
|
continue;
|
|
42541
42843
|
}
|
|
@@ -42559,7 +42861,7 @@ var PackageStillnessTracker = class {
|
|
|
42559
42861
|
id: c.id,
|
|
42560
42862
|
className: c.className,
|
|
42561
42863
|
bbox: { ...c.bbox },
|
|
42562
|
-
|
|
42864
|
+
hits: c.hits,
|
|
42563
42865
|
firstSeenAt: c.firstSeenAt,
|
|
42564
42866
|
lastSeenAt: c.lastSeenAt,
|
|
42565
42867
|
activityBackedAtOpen: c.activityBackedAtOpen
|
|
@@ -42617,7 +42919,7 @@ var PackageStillnessTracker = class {
|
|
|
42617
42919
|
frameHeight: input.frameHeight,
|
|
42618
42920
|
firstSeenAt: input.timestamp,
|
|
42619
42921
|
lastSeenAt: input.timestamp,
|
|
42620
|
-
|
|
42922
|
+
hits: 1,
|
|
42621
42923
|
promotedAt: null,
|
|
42622
42924
|
activityBackedAtOpen,
|
|
42623
42925
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
@@ -42627,18 +42929,16 @@ var PackageStillnessTracker = class {
|
|
|
42627
42929
|
};
|
|
42628
42930
|
}
|
|
42629
42931
|
};
|
|
42630
|
-
/** Re-confirm a candidate with this frame's box.
|
|
42631
|
-
*
|
|
42632
|
-
|
|
42633
|
-
function confirm(prev, input, s, roundGapMs) {
|
|
42634
|
-
const newRound = input.timestamp - prev.lastSeenAt >= roundGapMs;
|
|
42932
|
+
/** Re-confirm a candidate with this frame's box. Every sighting is a hit —
|
|
42933
|
+
* a settle burst of three frames in one second is three confirmations. */
|
|
42934
|
+
function confirm(prev, input, s) {
|
|
42635
42935
|
return {
|
|
42636
42936
|
...prev,
|
|
42637
42937
|
bbox: { ...s.bbox },
|
|
42638
42938
|
frameWidth: input.frameWidth,
|
|
42639
42939
|
frameHeight: input.frameHeight,
|
|
42640
42940
|
lastSeenAt: input.timestamp,
|
|
42641
|
-
|
|
42941
|
+
hits: prev.hits + 1,
|
|
42642
42942
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
42643
42943
|
...s.label !== void 0 ? { label: s.label } : {},
|
|
42644
42944
|
...s.confidence !== void 0 ? { confidence: s.confidence } : {},
|
|
@@ -42663,6 +42963,31 @@ function toEntry(c) {
|
|
|
42663
42963
|
};
|
|
42664
42964
|
}
|
|
42665
42965
|
//#endregion
|
|
42966
|
+
//#region src/pipeline-analytics/pipeline/package/package-zone-activity.ts
|
|
42967
|
+
/**
|
|
42968
|
+
* Whether a live track is motion inside a configured package drop-off zone.
|
|
42969
|
+
*
|
|
42970
|
+
* The stillness activity gate used to stamp ANY non-package track on the
|
|
42971
|
+
* camera. A walker at the far gate then "delivered" the streetlight wedge
|
|
42972
|
+
* on the doormat. The operator's prerequisite is motion in the drop-off
|
|
42973
|
+
* zone the package rule points at — the same zones `PackageDropDetector`
|
|
42974
|
+
* uses for the centroid gate.
|
|
42975
|
+
*/
|
|
42976
|
+
function isDelivererInPackageZone(trackZones, packageZoneIds) {
|
|
42977
|
+
if (packageZoneIds.size === 0 || trackZones.length === 0) return false;
|
|
42978
|
+
for (const z of trackZones) if (packageZoneIds.has(z)) return true;
|
|
42979
|
+
return false;
|
|
42980
|
+
}
|
|
42981
|
+
/** Zone ids referenced by enabled `package`-stage rules. Disabled rules do not count. */
|
|
42982
|
+
function collectPackageZoneIds(rules) {
|
|
42983
|
+
const ids = /* @__PURE__ */ new Set();
|
|
42984
|
+
for (const r of rules) {
|
|
42985
|
+
if (r.enabled === false) continue;
|
|
42986
|
+
for (const z of r.zoneIds) ids.add(z);
|
|
42987
|
+
}
|
|
42988
|
+
return ids;
|
|
42989
|
+
}
|
|
42990
|
+
//#endregion
|
|
42666
42991
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-zones.ts
|
|
42667
42992
|
/**
|
|
42668
42993
|
* Zone ids whose 0–1 polygon contains the entry's normalised bbox centroid.
|
|
@@ -49287,10 +49612,21 @@ var TrackStore = class {
|
|
|
49287
49612
|
* in-memory dedup (the query cap has no DISTINCT); best-effort.
|
|
49288
49613
|
*/
|
|
49289
49614
|
async listDeviceIds() {
|
|
49615
|
+
return this.collectDeviceIds(0);
|
|
49616
|
+
}
|
|
49617
|
+
/**
|
|
49618
|
+
* Distinct deviceIds with a persisted track whose `lastSeen` is at least
|
|
49619
|
+
* `since`. The group sealer's 10-minute window. Walking from epoch 0 is how
|
|
49620
|
+
* `sealRecent` wedged hub-main every TTL sweep.
|
|
49621
|
+
*/
|
|
49622
|
+
async listDeviceIdsSince(since) {
|
|
49623
|
+
return this.collectDeviceIds(Math.max(0, Math.floor(since)));
|
|
49624
|
+
}
|
|
49625
|
+
async collectDeviceIds(fromLastSeen) {
|
|
49290
49626
|
const seenDevices = /* @__PURE__ */ new Set();
|
|
49291
49627
|
const seenIds = /* @__PURE__ */ new Set();
|
|
49292
49628
|
const PAGE = 500;
|
|
49293
|
-
let cursor =
|
|
49629
|
+
let cursor = fromLastSeen;
|
|
49294
49630
|
try {
|
|
49295
49631
|
for (;;) {
|
|
49296
49632
|
const rows = await this.store.query.query({
|
|
@@ -49555,6 +49891,28 @@ var TrackStore = class {
|
|
|
49555
49891
|
return this.rowToTrack(row.id, row.data);
|
|
49556
49892
|
}
|
|
49557
49893
|
/**
|
|
49894
|
+
* Batch read of persisted tracks. `listGroupRecords` cannot call
|
|
49895
|
+
* {@link getPersistedByTrackId} per member — that is one settings-store RPC
|
|
49896
|
+
* per row and a 60-group page takes ~20 s, which the viewer feed times out.
|
|
49897
|
+
*/
|
|
49898
|
+
async getPersistedByTrackIds(trackIds) {
|
|
49899
|
+
if (trackIds.length === 0) return [];
|
|
49900
|
+
const PAGE = 500;
|
|
49901
|
+
const out = [];
|
|
49902
|
+
for (let i = 0; i < trackIds.length; i += PAGE) {
|
|
49903
|
+
const chunk = trackIds.slice(i, i + PAGE);
|
|
49904
|
+
const rows = await this.store.query.query({
|
|
49905
|
+
collection: TRACKS_COLLECTION,
|
|
49906
|
+
filter: {
|
|
49907
|
+
whereIn: { id: [...chunk] },
|
|
49908
|
+
limit: chunk.length
|
|
49909
|
+
}
|
|
49910
|
+
});
|
|
49911
|
+
for (const row of rows) out.push(this.rowToTrack(row.id, row.data));
|
|
49912
|
+
}
|
|
49913
|
+
return out;
|
|
49914
|
+
}
|
|
49915
|
+
/**
|
|
49558
49916
|
* Persist a SYNTHETIC track (a sensor/control event projected into the
|
|
49559
49917
|
* unified track store). Unlike `persistCompleted` there is no live in-RAM
|
|
49560
49918
|
* state and no trajectory envelope (synthetic tracks carry no positions) —
|
|
@@ -54627,11 +54985,22 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54627
54985
|
description: "Also emit a package-picked-up event when the delivered package leaves.",
|
|
54628
54986
|
default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
|
|
54629
54987
|
},
|
|
54988
|
+
{
|
|
54989
|
+
type: "slider",
|
|
54990
|
+
key: "packageDropMinFrames",
|
|
54991
|
+
label: "Confirmation frames",
|
|
54992
|
+
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.",
|
|
54993
|
+
min: 1,
|
|
54994
|
+
max: 10,
|
|
54995
|
+
step: 1,
|
|
54996
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropMinFrames,
|
|
54997
|
+
showValue: true
|
|
54998
|
+
},
|
|
54630
54999
|
{
|
|
54631
55000
|
type: "slider",
|
|
54632
55001
|
key: "packageDropDwellSec",
|
|
54633
55002
|
label: "Minimum dwell",
|
|
54634
|
-
description: "
|
|
55003
|
+
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.",
|
|
54635
55004
|
min: 0,
|
|
54636
55005
|
max: 300,
|
|
54637
55006
|
step: 5,
|
|
@@ -54665,7 +55034,7 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54665
55034
|
type: "slider",
|
|
54666
55035
|
key: "packageDropActivityWindowSec",
|
|
54667
55036
|
label: "Deliverer window",
|
|
54668
|
-
description: "A delivery only counts if a person or vehicle was
|
|
55037
|
+
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.",
|
|
54669
55038
|
min: 0,
|
|
54670
55039
|
max: 1800,
|
|
54671
55040
|
step: 30,
|
|
@@ -56677,6 +57046,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
56677
57046
|
plateStore = null;
|
|
56678
57047
|
groupStore = null;
|
|
56679
57048
|
groupSealer = null;
|
|
57049
|
+
groupLive = null;
|
|
57050
|
+
/** Serialise first-activity lookback per camera, then observe in order. */
|
|
57051
|
+
groupObserveTail = /* @__PURE__ */ new Map();
|
|
56680
57052
|
plateRecognizer = null;
|
|
56681
57053
|
objectEmbeddingStore = null;
|
|
56682
57054
|
/** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
|
|
@@ -57087,11 +57459,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
57087
57459
|
*/
|
|
57088
57460
|
packageStillness = new PackageStillnessTracker({ newId: () => randomUUID() });
|
|
57089
57461
|
/**
|
|
57090
|
-
* Frame-clock timestamp of the last NON-package track seen
|
|
57091
|
-
*
|
|
57092
|
-
* deliverer").
|
|
57093
|
-
*
|
|
57094
|
-
*
|
|
57462
|
+
* Frame-clock timestamp of the last NON-package track seen IN A PACKAGE
|
|
57463
|
+
* DROP-OFF ZONE per device — the activity mirror behind the package
|
|
57464
|
+
* activity gate ("a parcel needs a deliverer"). A walker at the far gate
|
|
57465
|
+
* does not stamp this. Deliberately not {@link lastTrackActivityMs}: that
|
|
57466
|
+
* map is stamped by ANY live track, including the package-class FP itself,
|
|
57467
|
+
* which would let the streetlight wedge vouch for its own delivery.
|
|
57095
57468
|
*/
|
|
57096
57469
|
lastNonPackageActivityAt = /* @__PURE__ */ new Map();
|
|
57097
57470
|
/** Last time the oversize-sighting drop was logged per device — one line
|
|
@@ -58001,6 +58374,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58001
58374
|
media: mediaStore
|
|
58002
58375
|
});
|
|
58003
58376
|
this.groupStore = groupStore;
|
|
58377
|
+
this.groupLive = new GroupLiveBuffer();
|
|
58004
58378
|
this.groupSealer = new GroupSealer({
|
|
58005
58379
|
trackStore,
|
|
58006
58380
|
groupStore,
|
|
@@ -58015,9 +58389,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58015
58389
|
},
|
|
58016
58390
|
now: () => Date.now()
|
|
58017
58391
|
});
|
|
58018
|
-
this.groupSealer.backfill().catch((err) => {
|
|
58019
|
-
logger.warn("group sealer backfill failed", { meta: { error: errMsg(err) } });
|
|
58020
|
-
});
|
|
58021
58392
|
return {
|
|
58022
58393
|
trackStore,
|
|
58023
58394
|
mediaStore,
|
|
@@ -58460,12 +58831,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58460
58831
|
buildSummaryPorts(stores) {
|
|
58461
58832
|
return {
|
|
58462
58833
|
listTracks: async ({ deviceId, sinceMs, untilMs, limit }) => {
|
|
58463
|
-
const groups = await stores.groupStore.listOverlapping({
|
|
58464
|
-
deviceId,
|
|
58465
|
-
sinceMs,
|
|
58466
|
-
untilMs,
|
|
58467
|
-
limit
|
|
58468
|
-
});
|
|
58469
58834
|
const windowTracks = await stores.trackStore.queryHistorical({
|
|
58470
58835
|
deviceId,
|
|
58471
58836
|
since: sinceMs,
|
|
@@ -58473,51 +58838,42 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58473
58838
|
projection: "slim",
|
|
58474
58839
|
limit: Math.max(limit, 2e3)
|
|
58475
58840
|
});
|
|
58476
|
-
const trackById = new Map(windowTracks.map((t) => [t.trackId, t]));
|
|
58477
58841
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
58478
|
-
|
|
58479
|
-
|
|
58480
|
-
|
|
58481
|
-
|
|
58482
|
-
|
|
58483
|
-
|
|
58484
|
-
|
|
58485
|
-
|
|
58486
|
-
|
|
58487
|
-
|
|
58488
|
-
|
|
58489
|
-
|
|
58490
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
58491
|
-
deviceId,
|
|
58492
|
-
className: primary.className,
|
|
58493
|
-
firstSeen: group.openedAt,
|
|
58494
|
-
lastSeen: group.closedAt,
|
|
58495
|
-
importance,
|
|
58496
|
-
zones,
|
|
58497
|
-
classes,
|
|
58498
|
-
...tracks.every((t) => t.closedReason === "stationary-promotion") ? { stationary: true } : {},
|
|
58499
|
-
confirmed: tracks.some((t) => t.observations !== void 0 && t.observations >= minHits)
|
|
58500
|
-
});
|
|
58501
|
-
}
|
|
58502
|
-
return out;
|
|
58842
|
+
return windowTracks.map((t) => ({
|
|
58843
|
+
trackId: t.trackId,
|
|
58844
|
+
deviceId,
|
|
58845
|
+
className: t.className,
|
|
58846
|
+
firstSeen: t.firstSeen,
|
|
58847
|
+
lastSeen: t.lastSeen,
|
|
58848
|
+
zones: [...t.zonesVisited],
|
|
58849
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
58850
|
+
...t.classes !== void 0 && t.classes.length > 0 ? { classes: t.classes } : {},
|
|
58851
|
+
...t.closedReason === "stationary-promotion" ? { stationary: true } : {},
|
|
58852
|
+
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
58853
|
+
}));
|
|
58503
58854
|
},
|
|
58504
58855
|
listMediaInfo: async (id) => {
|
|
58505
58856
|
const group = await stores.groupStore.get(id);
|
|
58506
|
-
if (group
|
|
58507
|
-
|
|
58508
|
-
|
|
58509
|
-
|
|
58510
|
-
|
|
58511
|
-
|
|
58512
|
-
|
|
58513
|
-
|
|
58514
|
-
|
|
58857
|
+
if (group) {
|
|
58858
|
+
if (group.mediaKey || group.nativeKey) return digestMediaRows({
|
|
58859
|
+
mediaKey: group.mediaKey,
|
|
58860
|
+
nativeKey: group.nativeKey,
|
|
58861
|
+
timestamp: group.timestamp,
|
|
58862
|
+
memberRows: []
|
|
58863
|
+
});
|
|
58864
|
+
const members = await stores.groupStore.listMembers(group.id);
|
|
58865
|
+
const memberRows = (await Promise.all(members.map((m) => stores.mediaStore.listInfoByOwner("track", m.trackId)))).flatMap((rows) => rows.map((r) => ({
|
|
58515
58866
|
key: r.key,
|
|
58516
58867
|
kind: r.kind,
|
|
58517
58868
|
timestamp: r.timestamp
|
|
58518
|
-
}));
|
|
58869
|
+
})));
|
|
58870
|
+
return digestMediaRows({
|
|
58871
|
+
mediaKey: group.mediaKey,
|
|
58872
|
+
nativeKey: group.nativeKey,
|
|
58873
|
+
timestamp: group.timestamp,
|
|
58874
|
+
memberRows
|
|
58875
|
+
});
|
|
58519
58876
|
}
|
|
58520
|
-
if (group) return [];
|
|
58521
58877
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
58522
58878
|
key: r.key,
|
|
58523
58879
|
kind: r.kind,
|
|
@@ -59763,6 +60119,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
59763
60119
|
state: t.state
|
|
59764
60120
|
});
|
|
59765
60121
|
positionsCountById.set(t.trackId, upserted.positions.length);
|
|
60122
|
+
this.enqueueLiveGroup(upserted, result.timestamp);
|
|
59766
60123
|
}
|
|
59767
60124
|
if (result.rawTrackedDetections.length > 0) {
|
|
59768
60125
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
@@ -59976,6 +60333,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
59976
60333
|
deviceId,
|
|
59977
60334
|
tracked: result.tracked,
|
|
59978
60335
|
packageClasses,
|
|
60336
|
+
packageZoneIds: collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []),
|
|
59979
60337
|
settings: packageSettings,
|
|
59980
60338
|
frameWidth: result.frameWidth,
|
|
59981
60339
|
frameHeight: result.frameHeight,
|
|
@@ -61686,8 +62044,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61686
62044
|
async sweepExpiredTracks() {
|
|
61687
62045
|
if (this.shuttingDown || !this.trackStore) return;
|
|
61688
62046
|
await this.trackCloser.sweep();
|
|
61689
|
-
this.groupSealer?.
|
|
62047
|
+
if (this.isPostProcessingNode && this.groupLive) this.groupSealer?.flushIdle(this.groupLive);
|
|
61690
62048
|
}
|
|
62049
|
+
/** Hub-only: assign this sighting in RAM. First activity on a camera
|
|
62050
|
+
* replays 2 minutes of that camera (crash recover), then observe is sync. */
|
|
62051
|
+
enqueueLiveGroup(track, now) {}
|
|
61691
62052
|
/** Unified retention (Phase 1): earliest retained footage per device — the
|
|
61692
62053
|
* follow-recordings cutoff source. Cached 10 min: availability answers in
|
|
61693
62054
|
* ~70 ms but the sweep asks once per device per pass. `null` = no footage
|
|
@@ -62133,7 +62494,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62133
62494
|
runPackageStillness(input) {
|
|
62134
62495
|
if (this.packageDropDetector === null) return;
|
|
62135
62496
|
const tags = { deviceId: input.deviceId };
|
|
62136
|
-
for (const t of input.tracked)
|
|
62497
|
+
for (const t of input.tracked) {
|
|
62498
|
+
if (input.packageClasses.has(t.className)) continue;
|
|
62499
|
+
if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
|
|
62137
62500
|
this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
|
|
62138
62501
|
break;
|
|
62139
62502
|
}
|
|
@@ -62180,6 +62543,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62180
62543
|
frameWidth: input.frameWidth,
|
|
62181
62544
|
frameHeight: input.frameHeight,
|
|
62182
62545
|
timestamp: input.timestamp,
|
|
62546
|
+
minHits: input.settings.packageDropMinFrames,
|
|
62183
62547
|
minDwellMs: input.settings.packageDropDwellSec * 1e3,
|
|
62184
62548
|
activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
|
|
62185
62549
|
...lastActivityAt !== void 0 ? { lastActivityAt } : {}
|
|
@@ -62283,9 +62647,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62283
62647
|
for (const ex of outcome.expired) {
|
|
62284
62648
|
const observedDwellMs = ex.lastSeenAt - ex.firstSeenAt;
|
|
62285
62649
|
const requiredDwellMs = input.settings.packageDropDwellSec * 1e3;
|
|
62650
|
+
const requiredHits = input.settings.packageDropMinFrames;
|
|
62286
62651
|
const reasons = [];
|
|
62287
62652
|
if (!ex.activityBackedAtOpen) reasons.push("no-activity-before-open");
|
|
62288
|
-
if (ex.
|
|
62653
|
+
if (ex.hits < requiredHits) reasons.push("hits");
|
|
62289
62654
|
if (observedDwellMs < requiredDwellMs) reasons.push("dwell");
|
|
62290
62655
|
this.ctx.logger.info("package candidate expired without promotion", {
|
|
62291
62656
|
tags,
|
|
@@ -62293,8 +62658,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62293
62658
|
candidateId: ex.id,
|
|
62294
62659
|
className: ex.className,
|
|
62295
62660
|
bbox: ex.bbox,
|
|
62296
|
-
|
|
62297
|
-
|
|
62661
|
+
hits: ex.hits,
|
|
62662
|
+
minHits: requiredHits,
|
|
62298
62663
|
observedDwellMs,
|
|
62299
62664
|
requiredDwellMs,
|
|
62300
62665
|
activityBackedAtOpen: ex.activityBackedAtOpen,
|
|
@@ -62331,6 +62696,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62331
62696
|
};
|
|
62332
62697
|
if (plausible) this.stationaryRegistry?.promote(entry);
|
|
62333
62698
|
processor.dropTrack(track.trackId);
|
|
62699
|
+
const peek = this.trackStore?.peekActive(track.trackId);
|
|
62700
|
+
if (peek) this.enqueueLiveGroup(toGroupableTrack({
|
|
62701
|
+
trackId: track.trackId,
|
|
62702
|
+
deviceId,
|
|
62703
|
+
className: track.className,
|
|
62704
|
+
firstSeen: peek.firstSeen,
|
|
62705
|
+
lastSeen: timestamp,
|
|
62706
|
+
positions: peek.positions,
|
|
62707
|
+
closedReason: "stationary-promotion"
|
|
62708
|
+
}), timestamp);
|
|
62334
62709
|
this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
|
|
62335
62710
|
this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
|
|
62336
62711
|
tags: { deviceId },
|
|
@@ -62546,6 +62921,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62546
62921
|
async listRecentTracks(input) {
|
|
62547
62922
|
return this.queryFacade.listRecentTracks(input);
|
|
62548
62923
|
}
|
|
62924
|
+
async listGroups(input) {
|
|
62925
|
+
return {
|
|
62926
|
+
groups: [],
|
|
62927
|
+
nextCursor: null
|
|
62928
|
+
};
|
|
62929
|
+
}
|
|
62930
|
+
async getGroup(input) {
|
|
62931
|
+
return null;
|
|
62932
|
+
}
|
|
62549
62933
|
/**
|
|
62550
62934
|
* Every event kind the device can produce: built-ins (motion + audio),
|
|
62551
62935
|
* detection classes actually observed on the device, and sensor kinds
|
|
@@ -63690,33 +64074,38 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
63690
64074
|
buildFaceRescoreActionHandlers() {
|
|
63691
64075
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
63692
64076
|
}
|
|
63693
|
-
|
|
63694
|
-
const
|
|
63695
|
-
|
|
63696
|
-
|
|
63697
|
-
|
|
63698
|
-
|
|
63699
|
-
|
|
63700
|
-
|
|
63701
|
-
|
|
63702
|
-
|
|
63703
|
-
|
|
63704
|
-
|
|
63705
|
-
|
|
64077
|
+
groupQueryPorts() {
|
|
64078
|
+
const groupStore = this.groupStore;
|
|
64079
|
+
if (groupStore === null) return null;
|
|
64080
|
+
return {
|
|
64081
|
+
groupStore,
|
|
64082
|
+
loadTracks: async (trackIds) => {
|
|
64083
|
+
const map = /* @__PURE__ */ new Map();
|
|
64084
|
+
const tracks = this.trackStore;
|
|
64085
|
+
if (!tracks || trackIds.length === 0) return map;
|
|
64086
|
+
const missing = [];
|
|
64087
|
+
for (const id of trackIds) {
|
|
64088
|
+
const active = tracks.getActiveByTrack(id);
|
|
64089
|
+
if (active !== null) map.set(id, trackToLite(active));
|
|
64090
|
+
else missing.push(id);
|
|
64091
|
+
}
|
|
64092
|
+
if (missing.length === 0) return map;
|
|
64093
|
+
for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
|
|
64094
|
+
return map;
|
|
64095
|
+
},
|
|
64096
|
+
eventMediaBaseUrl: () => this.eventMediaBaseUrl
|
|
63706
64097
|
};
|
|
64098
|
+
}
|
|
64099
|
+
buildGroupActionHandlers() {
|
|
63707
64100
|
return {
|
|
63708
64101
|
"analytics.listGroups": async (input) => {
|
|
63709
|
-
|
|
63710
|
-
if (ready === null) return {
|
|
64102
|
+
return {
|
|
63711
64103
|
groups: [],
|
|
63712
64104
|
hasMore: false
|
|
63713
64105
|
};
|
|
63714
|
-
return listGroupRecords(ready, input);
|
|
63715
64106
|
},
|
|
63716
64107
|
"analytics.getGroup": async (input) => {
|
|
63717
|
-
|
|
63718
|
-
if (ready === null) return { group: null };
|
|
63719
|
-
return { group: await getGroupDetail(ready, input.groupId) };
|
|
64108
|
+
return { group: null };
|
|
63720
64109
|
}
|
|
63721
64110
|
};
|
|
63722
64111
|
}
|