@camstack/addon-post-analysis 1.2.116 → 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 +333 -269
- package/dist/pipeline-analytics/index.mjs +331 -267
- 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";
|
|
@@ -15142,26 +15142,28 @@ function remainingSummaryCandidates(kept, taken, rank) {
|
|
|
15142
15142
|
/**
|
|
15143
15143
|
* The media a tile is drawn from, best first.
|
|
15144
15144
|
*
|
|
15145
|
-
*
|
|
15146
|
-
*
|
|
15147
|
-
*
|
|
15148
|
-
*
|
|
15149
|
-
*
|
|
15150
|
-
*
|
|
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.
|
|
15151
15153
|
*/
|
|
15152
15154
|
var SUMMARY_MEDIA_LADDER = [
|
|
15153
15155
|
"groupCrop",
|
|
15154
15156
|
"nativeFrame",
|
|
15155
|
-
"
|
|
15156
|
-
"
|
|
15157
|
-
"
|
|
15157
|
+
"keyFrame",
|
|
15158
|
+
"keyFrameSmall",
|
|
15159
|
+
"fullFrame"
|
|
15158
15160
|
];
|
|
15159
15161
|
/**
|
|
15160
15162
|
* Pick ONE media row for a track — the cheap question first (index rows only),
|
|
15161
15163
|
* so exactly one blob is ever read.
|
|
15162
15164
|
*
|
|
15163
|
-
* Within a rung the row CLOSEST to the track's peak is chosen;
|
|
15164
|
-
*
|
|
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
|
|
15165
15167
|
* pick the moment the subject entered rather than the moment it was best seen.
|
|
15166
15168
|
*/
|
|
15167
15169
|
function pickSummaryMedia(rows, aroundMs) {
|
|
@@ -39873,8 +39875,9 @@ var OverlayDetailStateStore = class {
|
|
|
39873
39875
|
*
|
|
39874
39876
|
* See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
|
|
39875
39877
|
* docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
|
|
39876
|
-
* (surface C).
|
|
39877
|
-
*
|
|
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).
|
|
39878
39881
|
*/
|
|
39879
39882
|
var PackageDropSettingsSchema = object({
|
|
39880
39883
|
/**
|
|
@@ -39895,13 +39898,20 @@ var PackageDropSettingsSchema = object({
|
|
|
39895
39898
|
*/
|
|
39896
39899
|
packageDropEnabled: boolean().default(true),
|
|
39897
39900
|
/**
|
|
39898
|
-
*
|
|
39899
|
-
*
|
|
39900
|
-
*
|
|
39901
|
-
*
|
|
39902
|
-
*
|
|
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.
|
|
39906
|
+
*/
|
|
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.
|
|
39903
39913
|
*/
|
|
39904
|
-
packageDropDwellSec: number().int().min(0).max(3600).default(
|
|
39914
|
+
packageDropDwellSec: number().int().min(0).max(3600).default(0),
|
|
39905
39915
|
/** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
|
|
39906
39916
|
packageDropMinBboxAreaFrac: number().min(0).max(1).default(.004),
|
|
39907
39917
|
/**
|
|
@@ -39919,13 +39929,15 @@ var PackageDropSettingsSchema = object({
|
|
|
39919
39929
|
packageDropPickupEnabled: boolean().default(true),
|
|
39920
39930
|
/**
|
|
39921
39931
|
* A parcel needs a deliverer: a stillness candidate only ever promotes to
|
|
39922
|
-
* `delivered` if a non-package track (person/vehicle/animal) was active
|
|
39923
|
-
*
|
|
39924
|
-
* departure only counts as `picked-up` with
|
|
39925
|
-
*
|
|
39926
|
-
*
|
|
39927
|
-
*
|
|
39928
|
-
*
|
|
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).
|
|
39929
39941
|
*/
|
|
39930
39942
|
packageDropActivityWindowSec: number().int().min(0).max(3600).default(300),
|
|
39931
39943
|
/**
|
|
@@ -39949,6 +39961,7 @@ function resolvePackageDropSettings(raw) {
|
|
|
39949
39961
|
};
|
|
39950
39962
|
return {
|
|
39951
39963
|
packageDropEnabled: pick("packageDropEnabled"),
|
|
39964
|
+
packageDropMinFrames: pick("packageDropMinFrames"),
|
|
39952
39965
|
packageDropDwellSec: pick("packageDropDwellSec"),
|
|
39953
39966
|
packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
|
|
39954
39967
|
packageDropMaxBboxAreaFrac: pick("packageDropMaxBboxAreaFrac"),
|
|
@@ -40206,29 +40219,6 @@ var DeferredBirthRegistry = class {
|
|
|
40206
40219
|
this.byDevice.delete(deviceKey);
|
|
40207
40220
|
}
|
|
40208
40221
|
};
|
|
40209
|
-
var AnalyticsGroupRecordSchema = object({
|
|
40210
|
-
id: string(),
|
|
40211
|
-
deviceId: number().int(),
|
|
40212
|
-
openedAt: number().int(),
|
|
40213
|
-
closedAt: number().int(),
|
|
40214
|
-
timestamp: number().int(),
|
|
40215
|
-
memberCount: number().int(),
|
|
40216
|
-
memberTrackIds: array(string()).readonly(),
|
|
40217
|
-
className: string(),
|
|
40218
|
-
classes: array(string()).readonly(),
|
|
40219
|
-
/** Relative event-media path, or null when the group has no picture yet. */
|
|
40220
|
-
mediaUrl: string().nullable(),
|
|
40221
|
-
singleton: boolean()
|
|
40222
|
-
});
|
|
40223
|
-
var AnalyticsGroupMemberSchema = object({
|
|
40224
|
-
trackId: string(),
|
|
40225
|
-
deviceId: number().int(),
|
|
40226
|
-
className: string(),
|
|
40227
|
-
firstSeen: number().int(),
|
|
40228
|
-
lastSeen: number().int(),
|
|
40229
|
-
mediaUrl: string().nullable()
|
|
40230
|
-
});
|
|
40231
|
-
var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
|
|
40232
40222
|
var groupActions = defineCustomActions({
|
|
40233
40223
|
"analytics.listGroups": customAction(object({
|
|
40234
40224
|
deviceId: number().int().optional(),
|
|
@@ -40242,61 +40232,6 @@ var groupActions = defineCustomActions({
|
|
|
40242
40232
|
})),
|
|
40243
40233
|
"analytics.getGroup": customAction(object({ groupId: string().min(1) }), object({ group: AnalyticsGroupDetailSchema.nullable() }))
|
|
40244
40234
|
});
|
|
40245
|
-
//#endregion
|
|
40246
|
-
//#region src/pipeline-analytics/group/group-query.ts
|
|
40247
|
-
/**
|
|
40248
|
-
* Stamp a relative event-media URL.
|
|
40249
|
-
*
|
|
40250
|
-
* Media keys contain `:` and the data-plane serves them by key. Track ids are
|
|
40251
|
-
* bare and fall through to the existing event/track thumbnail resolver. Never
|
|
40252
|
-
* percent-encode: the handler does not URL-decode the path.
|
|
40253
|
-
*/
|
|
40254
|
-
function stampEventMediaUrl(baseUrl, id) {
|
|
40255
|
-
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
40256
|
-
if (id === null || id === void 0 || id.length === 0) return null;
|
|
40257
|
-
return `${baseUrl}/${id}`;
|
|
40258
|
-
}
|
|
40259
|
-
/** Union of every class on the members; `className` is the first, stable order. */
|
|
40260
|
-
function aggregateGroupClasses(tracks) {
|
|
40261
|
-
const seen = /* @__PURE__ */ new Set();
|
|
40262
|
-
const classes = [];
|
|
40263
|
-
for (const track of tracks) for (const name of [track.className, ...track.classes ?? []]) {
|
|
40264
|
-
if (name.length === 0 || seen.has(name)) continue;
|
|
40265
|
-
seen.add(name);
|
|
40266
|
-
classes.push(name);
|
|
40267
|
-
}
|
|
40268
|
-
return {
|
|
40269
|
-
className: classes[0] ?? "unknown",
|
|
40270
|
-
classes
|
|
40271
|
-
};
|
|
40272
|
-
}
|
|
40273
|
-
function toGroupRecord(input) {
|
|
40274
|
-
const { className, classes } = aggregateGroupClasses(input.tracks);
|
|
40275
|
-
const mediaId = input.group.mediaKey ?? input.group.nativeKey ?? (input.group.memberCount === 1 ? input.memberTrackIds[0] : null);
|
|
40276
|
-
return {
|
|
40277
|
-
id: input.group.id,
|
|
40278
|
-
deviceId: input.group.deviceId,
|
|
40279
|
-
openedAt: input.group.openedAt,
|
|
40280
|
-
closedAt: input.group.closedAt,
|
|
40281
|
-
timestamp: input.group.timestamp,
|
|
40282
|
-
memberCount: input.memberTrackIds.length > 0 ? input.memberTrackIds.length : input.group.memberCount,
|
|
40283
|
-
memberTrackIds: [...input.memberTrackIds],
|
|
40284
|
-
className,
|
|
40285
|
-
classes: [...classes],
|
|
40286
|
-
mediaUrl: stampEventMediaUrl(input.eventMediaBaseUrl, mediaId),
|
|
40287
|
-
singleton: input.memberTrackIds.length <= 1
|
|
40288
|
-
};
|
|
40289
|
-
}
|
|
40290
|
-
function toGroupMember(member, track, eventMediaBaseUrl) {
|
|
40291
|
-
return {
|
|
40292
|
-
trackId: member.trackId,
|
|
40293
|
-
deviceId: member.deviceId,
|
|
40294
|
-
className: track?.className ?? "unknown",
|
|
40295
|
-
firstSeen: track?.firstSeen ?? member.timestamp,
|
|
40296
|
-
lastSeen: track?.lastSeen ?? member.timestamp,
|
|
40297
|
-
mediaUrl: stampEventMediaUrl(eventMediaBaseUrl, member.trackId)
|
|
40298
|
-
};
|
|
40299
|
-
}
|
|
40300
40235
|
function trackToLite(track) {
|
|
40301
40236
|
return {
|
|
40302
40237
|
trackId: track.trackId,
|
|
@@ -40306,56 +40241,18 @@ function trackToLite(track) {
|
|
|
40306
40241
|
lastSeen: track.lastSeen
|
|
40307
40242
|
};
|
|
40308
40243
|
}
|
|
40309
|
-
async function listGroupRecords(ports, input) {
|
|
40310
|
-
const page = await ports.groupStore.listRecent(input);
|
|
40311
|
-
const groups = [];
|
|
40312
|
-
for (const group of page.groups) groups.push(await buildGroupRecord(ports, group));
|
|
40313
|
-
return {
|
|
40314
|
-
groups,
|
|
40315
|
-
hasMore: page.hasMore
|
|
40316
|
-
};
|
|
40317
|
-
}
|
|
40318
|
-
async function getGroupDetail(ports, groupId) {
|
|
40319
|
-
const group = await ports.groupStore.get(groupId);
|
|
40320
|
-
if (group === null) return null;
|
|
40321
|
-
const members = await ports.groupStore.listMembers(groupId);
|
|
40322
|
-
const lites = [];
|
|
40323
|
-
const rows = [];
|
|
40324
|
-
for (const member of members) {
|
|
40325
|
-
const track = await ports.loadTrack(member.trackId);
|
|
40326
|
-
const lite = track === null ? null : trackToLite(track);
|
|
40327
|
-
if (lite !== null) lites.push(lite);
|
|
40328
|
-
rows.push(toGroupMember(member, lite, ports.eventMediaBaseUrl()));
|
|
40329
|
-
}
|
|
40330
|
-
return {
|
|
40331
|
-
...toGroupRecord({
|
|
40332
|
-
group,
|
|
40333
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
40334
|
-
tracks: lites,
|
|
40335
|
-
eventMediaBaseUrl: ports.eventMediaBaseUrl()
|
|
40336
|
-
}),
|
|
40337
|
-
members: rows
|
|
40338
|
-
};
|
|
40339
|
-
}
|
|
40340
|
-
async function buildGroupRecord(ports, group) {
|
|
40341
|
-
const members = await ports.groupStore.listMembers(group.id);
|
|
40342
|
-
const lites = [];
|
|
40343
|
-
for (const member of members) {
|
|
40344
|
-
const track = await ports.loadTrack(member.trackId);
|
|
40345
|
-
if (track !== null) lites.push(trackToLite(track));
|
|
40346
|
-
}
|
|
40347
|
-
return toGroupRecord({
|
|
40348
|
-
group,
|
|
40349
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
40350
|
-
tracks: lites,
|
|
40351
|
-
eventMediaBaseUrl: ports.eventMediaBaseUrl()
|
|
40352
|
-
});
|
|
40353
|
-
}
|
|
40354
40244
|
/**
|
|
40355
40245
|
* Gallery and digest read the same ladder: groupCrop, then the native-frame
|
|
40356
|
-
* pointer, then member
|
|
40357
|
-
*
|
|
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`.
|
|
40358
40250
|
*/
|
|
40251
|
+
var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
|
|
40252
|
+
"keyFrame",
|
|
40253
|
+
"keyFrameSmall",
|
|
40254
|
+
"fullFrame"
|
|
40255
|
+
]);
|
|
40359
40256
|
function digestMediaRows(input) {
|
|
40360
40257
|
const owned = [];
|
|
40361
40258
|
if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
|
|
@@ -40369,7 +40266,7 @@ function digestMediaRows(input) {
|
|
|
40369
40266
|
timestamp: input.timestamp
|
|
40370
40267
|
});
|
|
40371
40268
|
if (owned.length > 0) return owned;
|
|
40372
|
-
return input.memberRows;
|
|
40269
|
+
return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
|
|
40373
40270
|
}
|
|
40374
40271
|
var MATCH_WINDOW_MS = 1e3;
|
|
40375
40272
|
var MOVE_EPS = .35;
|
|
@@ -40923,6 +40820,34 @@ function toInstant(t) {
|
|
|
40923
40820
|
confidence: t.importance
|
|
40924
40821
|
};
|
|
40925
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
|
+
}
|
|
40926
40851
|
function toSealer(track) {
|
|
40927
40852
|
return {
|
|
40928
40853
|
trackId: track.trackId,
|
|
@@ -40939,7 +40864,11 @@ var GroupSealer = class {
|
|
|
40939
40864
|
constructor(deps) {
|
|
40940
40865
|
this.deps = deps;
|
|
40941
40866
|
}
|
|
40867
|
+
get processEnabled() {
|
|
40868
|
+
return this.deps.enabled ?? false;
|
|
40869
|
+
}
|
|
40942
40870
|
async flushIdle(buffer, now = this.deps.now()) {
|
|
40871
|
+
if (!this.processEnabled) return 0;
|
|
40943
40872
|
let written = 0;
|
|
40944
40873
|
for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
|
|
40945
40874
|
buffer.drop([group.id]);
|
|
@@ -40952,6 +40881,7 @@ var GroupSealer = class {
|
|
|
40952
40881
|
* into the live buffer, persist whoever is already idle, keep the rest in RAM.
|
|
40953
40882
|
*/
|
|
40954
40883
|
async recoverCamera(deviceId, buffer) {
|
|
40884
|
+
if (!this.processEnabled) return 0;
|
|
40955
40885
|
if (buffer.isPrimed(deviceId)) return 0;
|
|
40956
40886
|
const now = this.deps.now();
|
|
40957
40887
|
try {
|
|
@@ -40980,6 +40910,7 @@ var GroupSealer = class {
|
|
|
40980
40910
|
* is what wedged hub-main.
|
|
40981
40911
|
*/
|
|
40982
40912
|
async sealWindow(deviceId, since, until) {
|
|
40913
|
+
if (!this.processEnabled) return 0;
|
|
40983
40914
|
try {
|
|
40984
40915
|
const closed = await this.deps.trackStore.queryHistorical({
|
|
40985
40916
|
deviceId,
|
|
@@ -41094,26 +41025,54 @@ var GroupSealer = class {
|
|
|
41094
41025
|
return;
|
|
41095
41026
|
}
|
|
41096
41027
|
try {
|
|
41097
|
-
const raw = await sharp(source.jpeg).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
41098
|
-
if (raw.info.channels
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
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({
|
|
41107
41067
|
deviceId,
|
|
41108
41068
|
ownerKind: "group",
|
|
41109
41069
|
ownerId: draft.id,
|
|
41110
|
-
kind: "
|
|
41070
|
+
kind: "fullFrameBoxed",
|
|
41111
41071
|
timestamp,
|
|
41112
|
-
data:
|
|
41072
|
+
data: composed.bytes
|
|
41113
41073
|
});
|
|
41114
|
-
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
41115
41074
|
} catch (err) {
|
|
41116
|
-
this.deps.logger.warn("group sealer:
|
|
41075
|
+
this.deps.logger.warn("group sealer: boxed frame failed", {
|
|
41117
41076
|
tags: { deviceId },
|
|
41118
41077
|
meta: {
|
|
41119
41078
|
groupId: draft.id,
|
|
@@ -41138,6 +41097,37 @@ var GroupSealer = class {
|
|
|
41138
41097
|
}
|
|
41139
41098
|
};
|
|
41140
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
|
|
41141
41131
|
//#region src/pipeline-analytics/group/group-store.ts
|
|
41142
41132
|
/**
|
|
41143
41133
|
* The group ENTITY store — a co-moving partition of one session, with one
|
|
@@ -41399,6 +41389,24 @@ var TrackGroupStore = class {
|
|
|
41399
41389
|
data: r.data
|
|
41400
41390
|
}));
|
|
41401
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
|
+
}
|
|
41402
41410
|
async membersOfTracks(trackIds) {
|
|
41403
41411
|
const out = [];
|
|
41404
41412
|
for (const trackId of trackIds) {
|
|
@@ -41414,20 +41422,29 @@ var TrackGroupStore = class {
|
|
|
41414
41422
|
return out;
|
|
41415
41423
|
}
|
|
41416
41424
|
/**
|
|
41417
|
-
* Newest-closed page for the
|
|
41425
|
+
* Newest-closed page for the Groups feed.
|
|
41418
41426
|
*
|
|
41419
41427
|
* Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
|
|
41420
41428
|
* same as {@link listOverlapping}. Pagination is applied AFTER the window
|
|
41421
41429
|
* so a day cursor is honest rather than a raw-row offset that skips live
|
|
41422
|
-
* 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.
|
|
41423
41433
|
*/
|
|
41424
41434
|
async listRecent(input) {
|
|
41425
41435
|
const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
|
|
41426
41436
|
const offset = Math.max(input.offset ?? 0, 0);
|
|
41427
|
-
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({
|
|
41428
41445
|
collection: TRACK_GROUPS_COLLECTION,
|
|
41429
41446
|
filter: {
|
|
41430
|
-
...
|
|
41447
|
+
...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
|
|
41431
41448
|
orderBy: {
|
|
41432
41449
|
field: "closedAt",
|
|
41433
41450
|
direction: "desc"
|
|
@@ -41440,11 +41457,20 @@ var TrackGroupStore = class {
|
|
|
41440
41457
|
})).filter((g) => {
|
|
41441
41458
|
if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
|
|
41442
41459
|
if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
|
|
41460
|
+
if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
|
|
41443
41461
|
return true;
|
|
41444
|
-
}).
|
|
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];
|
|
41445
41467
|
return {
|
|
41446
|
-
groups
|
|
41447
|
-
hasMore
|
|
41468
|
+
groups,
|
|
41469
|
+
hasMore,
|
|
41470
|
+
nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
|
|
41471
|
+
closedAt: last.closedAt,
|
|
41472
|
+
groupId: last.id
|
|
41473
|
+
}) : null
|
|
41448
41474
|
};
|
|
41449
41475
|
}
|
|
41450
41476
|
/** Groups whose life overlaps `[since, until]` on one camera. */
|
|
@@ -42776,7 +42802,7 @@ function evaluateStationaryPromotion(input) {
|
|
|
42776
42802
|
};
|
|
42777
42803
|
}
|
|
42778
42804
|
var DEFAULT_PACKAGE_STILLNESS_CONFIG = {
|
|
42779
|
-
|
|
42805
|
+
minHits: 3,
|
|
42780
42806
|
roundGapMs: 5e3,
|
|
42781
42807
|
ttlMs: 5 * 6e4,
|
|
42782
42808
|
assocIou: .3
|
|
@@ -42809,8 +42835,9 @@ var PackageStillnessTracker = class {
|
|
|
42809
42835
|
const prev = next[index];
|
|
42810
42836
|
if (prev === void 0) continue;
|
|
42811
42837
|
consumed.add(prev.id);
|
|
42812
|
-
const updated = confirm(prev, input, s
|
|
42813
|
-
|
|
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)) {
|
|
42814
42841
|
next[index] = updated;
|
|
42815
42842
|
continue;
|
|
42816
42843
|
}
|
|
@@ -42834,7 +42861,7 @@ var PackageStillnessTracker = class {
|
|
|
42834
42861
|
id: c.id,
|
|
42835
42862
|
className: c.className,
|
|
42836
42863
|
bbox: { ...c.bbox },
|
|
42837
|
-
|
|
42864
|
+
hits: c.hits,
|
|
42838
42865
|
firstSeenAt: c.firstSeenAt,
|
|
42839
42866
|
lastSeenAt: c.lastSeenAt,
|
|
42840
42867
|
activityBackedAtOpen: c.activityBackedAtOpen
|
|
@@ -42892,7 +42919,7 @@ var PackageStillnessTracker = class {
|
|
|
42892
42919
|
frameHeight: input.frameHeight,
|
|
42893
42920
|
firstSeenAt: input.timestamp,
|
|
42894
42921
|
lastSeenAt: input.timestamp,
|
|
42895
|
-
|
|
42922
|
+
hits: 1,
|
|
42896
42923
|
promotedAt: null,
|
|
42897
42924
|
activityBackedAtOpen,
|
|
42898
42925
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
@@ -42902,18 +42929,16 @@ var PackageStillnessTracker = class {
|
|
|
42902
42929
|
};
|
|
42903
42930
|
}
|
|
42904
42931
|
};
|
|
42905
|
-
/** Re-confirm a candidate with this frame's box.
|
|
42906
|
-
*
|
|
42907
|
-
|
|
42908
|
-
function confirm(prev, input, s, roundGapMs) {
|
|
42909
|
-
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) {
|
|
42910
42935
|
return {
|
|
42911
42936
|
...prev,
|
|
42912
42937
|
bbox: { ...s.bbox },
|
|
42913
42938
|
frameWidth: input.frameWidth,
|
|
42914
42939
|
frameHeight: input.frameHeight,
|
|
42915
42940
|
lastSeenAt: input.timestamp,
|
|
42916
|
-
|
|
42941
|
+
hits: prev.hits + 1,
|
|
42917
42942
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
42918
42943
|
...s.label !== void 0 ? { label: s.label } : {},
|
|
42919
42944
|
...s.confidence !== void 0 ? { confidence: s.confidence } : {},
|
|
@@ -42938,6 +42963,31 @@ function toEntry(c) {
|
|
|
42938
42963
|
};
|
|
42939
42964
|
}
|
|
42940
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
|
|
42941
42991
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-zones.ts
|
|
42942
42992
|
/**
|
|
42943
42993
|
* Zone ids whose 0–1 polygon contains the entry's normalised bbox centroid.
|
|
@@ -49841,6 +49891,28 @@ var TrackStore = class {
|
|
|
49841
49891
|
return this.rowToTrack(row.id, row.data);
|
|
49842
49892
|
}
|
|
49843
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
|
+
/**
|
|
49844
49916
|
* Persist a SYNTHETIC track (a sensor/control event projected into the
|
|
49845
49917
|
* unified track store). Unlike `persistCompleted` there is no live in-RAM
|
|
49846
49918
|
* state and no trajectory envelope (synthetic tracks carry no positions) —
|
|
@@ -54913,11 +54985,22 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54913
54985
|
description: "Also emit a package-picked-up event when the delivered package leaves.",
|
|
54914
54986
|
default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
|
|
54915
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
|
+
},
|
|
54916
54999
|
{
|
|
54917
55000
|
type: "slider",
|
|
54918
55001
|
key: "packageDropDwellSec",
|
|
54919
55002
|
label: "Minimum dwell",
|
|
54920
|
-
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.",
|
|
54921
55004
|
min: 0,
|
|
54922
55005
|
max: 300,
|
|
54923
55006
|
step: 5,
|
|
@@ -54951,7 +55034,7 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54951
55034
|
type: "slider",
|
|
54952
55035
|
key: "packageDropActivityWindowSec",
|
|
54953
55036
|
label: "Deliverer window",
|
|
54954
|
-
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.",
|
|
54955
55038
|
min: 0,
|
|
54956
55039
|
max: 1800,
|
|
54957
55040
|
step: 30,
|
|
@@ -57376,11 +57459,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
57376
57459
|
*/
|
|
57377
57460
|
packageStillness = new PackageStillnessTracker({ newId: () => randomUUID() });
|
|
57378
57461
|
/**
|
|
57379
|
-
* Frame-clock timestamp of the last NON-package track seen
|
|
57380
|
-
*
|
|
57381
|
-
* deliverer").
|
|
57382
|
-
*
|
|
57383
|
-
*
|
|
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.
|
|
57384
57468
|
*/
|
|
57385
57469
|
lastNonPackageActivityAt = /* @__PURE__ */ new Map();
|
|
57386
57470
|
/** Last time the oversize-sighting drop was logged per device — one line
|
|
@@ -58747,13 +58831,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58747
58831
|
buildSummaryPorts(stores) {
|
|
58748
58832
|
return {
|
|
58749
58833
|
listTracks: async ({ deviceId, sinceMs, untilMs, limit }) => {
|
|
58750
|
-
await this.groupSealer?.sealWindow(deviceId, sinceMs, untilMs);
|
|
58751
|
-
const groups = await stores.groupStore.listOverlapping({
|
|
58752
|
-
deviceId,
|
|
58753
|
-
sinceMs,
|
|
58754
|
-
untilMs,
|
|
58755
|
-
limit
|
|
58756
|
-
});
|
|
58757
58834
|
const windowTracks = await stores.trackStore.queryHistorical({
|
|
58758
58835
|
deviceId,
|
|
58759
58836
|
since: sinceMs,
|
|
@@ -58761,33 +58838,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58761
58838
|
projection: "slim",
|
|
58762
58839
|
limit: Math.max(limit, 2e3)
|
|
58763
58840
|
});
|
|
58764
|
-
const trackById = new Map(windowTracks.map((t) => [t.trackId, t]));
|
|
58765
58841
|
const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
|
|
58766
|
-
|
|
58767
|
-
|
|
58768
|
-
|
|
58769
|
-
|
|
58770
|
-
|
|
58771
|
-
|
|
58772
|
-
|
|
58773
|
-
|
|
58774
|
-
|
|
58775
|
-
|
|
58776
|
-
|
|
58777
|
-
|
|
58778
|
-
memberTrackIds: members.map((m) => m.trackId),
|
|
58779
|
-
deviceId,
|
|
58780
|
-
className: primary.className,
|
|
58781
|
-
firstSeen: group.openedAt,
|
|
58782
|
-
lastSeen: group.closedAt,
|
|
58783
|
-
importance,
|
|
58784
|
-
zones,
|
|
58785
|
-
classes,
|
|
58786
|
-
...tracks.every((t) => t.closedReason === "stationary-promotion") ? { stationary: true } : {},
|
|
58787
|
-
confirmed: tracks.some((t) => t.observations !== void 0 && t.observations >= minHits)
|
|
58788
|
-
});
|
|
58789
|
-
}
|
|
58790
|
-
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
|
+
}));
|
|
58791
58854
|
},
|
|
58792
58855
|
listMediaInfo: async (id) => {
|
|
58793
58856
|
const group = await stores.groupStore.get(id);
|
|
@@ -60270,6 +60333,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60270
60333
|
deviceId,
|
|
60271
60334
|
tracked: result.tracked,
|
|
60272
60335
|
packageClasses,
|
|
60336
|
+
packageZoneIds: collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []),
|
|
60273
60337
|
settings: packageSettings,
|
|
60274
60338
|
frameWidth: result.frameWidth,
|
|
60275
60339
|
frameHeight: result.frameHeight,
|
|
@@ -61984,25 +62048,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61984
62048
|
}
|
|
61985
62049
|
/** Hub-only: assign this sighting in RAM. First activity on a camera
|
|
61986
62050
|
* replays 2 minutes of that camera (crash recover), then observe is sync. */
|
|
61987
|
-
enqueueLiveGroup(track, now) {
|
|
61988
|
-
if (!this.isPostProcessingNode || !this.groupLive) return;
|
|
61989
|
-
const deviceId = track.deviceId;
|
|
61990
|
-
const snapshot = toGroupableTrack(track);
|
|
61991
|
-
if (this.groupLive.isPrimed(deviceId)) {
|
|
61992
|
-
this.groupLive.observe(snapshot, now);
|
|
61993
|
-
return;
|
|
61994
|
-
}
|
|
61995
|
-
const next = (this.groupObserveTail.get(deviceId) ?? Promise.resolve()).then(async () => {
|
|
61996
|
-
if (this.groupLive && this.groupSealer && !this.groupLive.isPrimed(deviceId)) await this.groupSealer.recoverCamera(deviceId, this.groupLive);
|
|
61997
|
-
this.groupLive?.observe(snapshot, now);
|
|
61998
|
-
}).catch((err) => {
|
|
61999
|
-
this.ctx.logger.warn("group live observe failed", {
|
|
62000
|
-
tags: { deviceId },
|
|
62001
|
-
meta: { error: String(err) }
|
|
62002
|
-
});
|
|
62003
|
-
});
|
|
62004
|
-
this.groupObserveTail.set(deviceId, next);
|
|
62005
|
-
}
|
|
62051
|
+
enqueueLiveGroup(track, now) {}
|
|
62006
62052
|
/** Unified retention (Phase 1): earliest retained footage per device — the
|
|
62007
62053
|
* follow-recordings cutoff source. Cached 10 min: availability answers in
|
|
62008
62054
|
* ~70 ms but the sweep asks once per device per pass. `null` = no footage
|
|
@@ -62448,7 +62494,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62448
62494
|
runPackageStillness(input) {
|
|
62449
62495
|
if (this.packageDropDetector === null) return;
|
|
62450
62496
|
const tags = { deviceId: input.deviceId };
|
|
62451
|
-
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;
|
|
62452
62500
|
this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
|
|
62453
62501
|
break;
|
|
62454
62502
|
}
|
|
@@ -62495,6 +62543,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62495
62543
|
frameWidth: input.frameWidth,
|
|
62496
62544
|
frameHeight: input.frameHeight,
|
|
62497
62545
|
timestamp: input.timestamp,
|
|
62546
|
+
minHits: input.settings.packageDropMinFrames,
|
|
62498
62547
|
minDwellMs: input.settings.packageDropDwellSec * 1e3,
|
|
62499
62548
|
activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
|
|
62500
62549
|
...lastActivityAt !== void 0 ? { lastActivityAt } : {}
|
|
@@ -62598,9 +62647,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62598
62647
|
for (const ex of outcome.expired) {
|
|
62599
62648
|
const observedDwellMs = ex.lastSeenAt - ex.firstSeenAt;
|
|
62600
62649
|
const requiredDwellMs = input.settings.packageDropDwellSec * 1e3;
|
|
62650
|
+
const requiredHits = input.settings.packageDropMinFrames;
|
|
62601
62651
|
const reasons = [];
|
|
62602
62652
|
if (!ex.activityBackedAtOpen) reasons.push("no-activity-before-open");
|
|
62603
|
-
if (ex.
|
|
62653
|
+
if (ex.hits < requiredHits) reasons.push("hits");
|
|
62604
62654
|
if (observedDwellMs < requiredDwellMs) reasons.push("dwell");
|
|
62605
62655
|
this.ctx.logger.info("package candidate expired without promotion", {
|
|
62606
62656
|
tags,
|
|
@@ -62608,8 +62658,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62608
62658
|
candidateId: ex.id,
|
|
62609
62659
|
className: ex.className,
|
|
62610
62660
|
bbox: ex.bbox,
|
|
62611
|
-
|
|
62612
|
-
|
|
62661
|
+
hits: ex.hits,
|
|
62662
|
+
minHits: requiredHits,
|
|
62613
62663
|
observedDwellMs,
|
|
62614
62664
|
requiredDwellMs,
|
|
62615
62665
|
activityBackedAtOpen: ex.activityBackedAtOpen,
|
|
@@ -62871,6 +62921,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62871
62921
|
async listRecentTracks(input) {
|
|
62872
62922
|
return this.queryFacade.listRecentTracks(input);
|
|
62873
62923
|
}
|
|
62924
|
+
async listGroups(input) {
|
|
62925
|
+
return {
|
|
62926
|
+
groups: [],
|
|
62927
|
+
nextCursor: null
|
|
62928
|
+
};
|
|
62929
|
+
}
|
|
62930
|
+
async getGroup(input) {
|
|
62931
|
+
return null;
|
|
62932
|
+
}
|
|
62874
62933
|
/**
|
|
62875
62934
|
* Every event kind the device can produce: built-ins (motion + audio),
|
|
62876
62935
|
* detection classes actually observed on the device, and sensor kinds
|
|
@@ -64015,33 +64074,38 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
64015
64074
|
buildFaceRescoreActionHandlers() {
|
|
64016
64075
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
64017
64076
|
}
|
|
64018
|
-
|
|
64019
|
-
const
|
|
64020
|
-
|
|
64021
|
-
|
|
64022
|
-
|
|
64023
|
-
|
|
64024
|
-
|
|
64025
|
-
|
|
64026
|
-
|
|
64027
|
-
|
|
64028
|
-
|
|
64029
|
-
|
|
64030
|
-
|
|
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
|
|
64031
64097
|
};
|
|
64098
|
+
}
|
|
64099
|
+
buildGroupActionHandlers() {
|
|
64032
64100
|
return {
|
|
64033
64101
|
"analytics.listGroups": async (input) => {
|
|
64034
|
-
|
|
64035
|
-
if (ready === null) return {
|
|
64102
|
+
return {
|
|
64036
64103
|
groups: [],
|
|
64037
64104
|
hasMore: false
|
|
64038
64105
|
};
|
|
64039
|
-
return listGroupRecords(ready, input);
|
|
64040
64106
|
},
|
|
64041
64107
|
"analytics.getGroup": async (input) => {
|
|
64042
|
-
|
|
64043
|
-
if (ready === null) return { group: null };
|
|
64044
|
-
return { group: await getGroupDetail(ready, input.groupId) };
|
|
64108
|
+
return { group: null };
|
|
64045
64109
|
}
|
|
64046
64110
|
};
|
|
64047
64111
|
}
|