@camstack/addon-post-analysis 1.2.116 → 1.2.118
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-CO6pe7bi.js} +104 -2
- package/dist/{dist-B13sFjR2.mjs → dist-Gn2fFMov.mjs} +93 -3
- 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-BYzwylnw.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-TlKH3NQ8.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +603 -355
- package/dist/pipeline-analytics/index.mjs +601 -353
- 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-Gn2fFMov.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) {
|
|
@@ -25900,7 +25902,7 @@ var CLASS_RANK_VEHICLE = .8;
|
|
|
25900
25902
|
var CLASS_RANK_ANIMAL = .5;
|
|
25901
25903
|
var CLASS_RANK_DEFAULT = .25;
|
|
25902
25904
|
var PERSON_CLASSES = new Set(["person", "face"]);
|
|
25903
|
-
var VEHICLE_CLASSES = new Set([
|
|
25905
|
+
var VEHICLE_CLASSES$1 = new Set([
|
|
25904
25906
|
"vehicle",
|
|
25905
25907
|
"car",
|
|
25906
25908
|
"truck",
|
|
@@ -25931,7 +25933,7 @@ function clamp01$1(x) {
|
|
|
25931
25933
|
function classRank(className) {
|
|
25932
25934
|
const c = className.toLowerCase();
|
|
25933
25935
|
if (PERSON_CLASSES.has(c)) return 1;
|
|
25934
|
-
if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
|
|
25936
|
+
if (VEHICLE_CLASSES$1.has(c)) return CLASS_RANK_VEHICLE;
|
|
25935
25937
|
if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
|
|
25936
25938
|
return CLASS_RANK_DEFAULT;
|
|
25937
25939
|
}
|
|
@@ -26534,7 +26536,7 @@ var FaceGalleryProvider = class {
|
|
|
26534
26536
|
const identities = await this.identityStore.listIdentities();
|
|
26535
26537
|
const nameMap = new Map(identities.map((i) => [i.id, i.name]));
|
|
26536
26538
|
const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
|
|
26537
|
-
const
|
|
26539
|
+
const cropUrl = this.cropUrlFor(face);
|
|
26538
26540
|
return {
|
|
26539
26541
|
faceId: face.id,
|
|
26540
26542
|
deviceId: face.deviceId,
|
|
@@ -26543,7 +26545,7 @@ var FaceGalleryProvider = class {
|
|
|
26543
26545
|
...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
|
|
26544
26546
|
...identityName !== void 0 ? { identityName } : {},
|
|
26545
26547
|
assigned: face.assigned,
|
|
26546
|
-
...
|
|
26548
|
+
...cropUrl !== void 0 ? { cropUrl } : {},
|
|
26547
26549
|
...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
|
|
26548
26550
|
...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
|
|
26549
26551
|
...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
|
|
@@ -29661,9 +29663,15 @@ var TrackCloser = class {
|
|
|
29661
29663
|
await this.suppressZeroMedia(t.deviceId, t.trackId);
|
|
29662
29664
|
return;
|
|
29663
29665
|
}
|
|
29664
|
-
|
|
29666
|
+
let thumbnailLanded = ownedMedia.some((m) => m.kind === "thumbnail");
|
|
29667
|
+
if (outcome === "raster-fallback" && closure?.rasterFallback) {
|
|
29668
|
+
await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
|
|
29669
|
+
thumbnailLanded = true;
|
|
29670
|
+
}
|
|
29665
29671
|
await this.maybePromoteLastFrame(t, ownedMedia);
|
|
29666
|
-
|
|
29672
|
+
const hasRasterFallback = closure?.rasterFallback !== void 0;
|
|
29673
|
+
if (!thumbnailLanded) thumbnailLanded = await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback);
|
|
29674
|
+
if (!thumbnailLanded && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
|
|
29667
29675
|
this.deps.logger.info("track ended", {
|
|
29668
29676
|
tags: { deviceId: t.deviceId },
|
|
29669
29677
|
meta: {
|
|
@@ -29781,14 +29789,23 @@ var TrackCloser = class {
|
|
|
29781
29789
|
* and the rate could only ever be counted from the database, after the fact.
|
|
29782
29790
|
* A branch that drops work silently reads as "never happened".
|
|
29783
29791
|
*/
|
|
29784
|
-
async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia) {
|
|
29785
|
-
if (ownedMedia.some((m) => m.kind === "thumbnail")) return;
|
|
29792
|
+
async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback) {
|
|
29793
|
+
if (ownedMedia.some((m) => m.kind === "thumbnail")) return true;
|
|
29786
29794
|
const derive = this.deps.deriveThumbnailFromKeyFrame;
|
|
29787
|
-
if (!derive)
|
|
29795
|
+
if (!derive) {
|
|
29796
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-not-wired");
|
|
29797
|
+
return false;
|
|
29798
|
+
}
|
|
29788
29799
|
const keyFrame = ownedMedia.find((m) => m.kind === "keyFrame");
|
|
29789
|
-
if (!keyFrame)
|
|
29800
|
+
if (!keyFrame) {
|
|
29801
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-key-frame");
|
|
29802
|
+
return false;
|
|
29803
|
+
}
|
|
29790
29804
|
const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
|
|
29791
|
-
if (!bestSeen)
|
|
29805
|
+
if (!bestSeen) {
|
|
29806
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
|
|
29807
|
+
return false;
|
|
29808
|
+
}
|
|
29792
29809
|
try {
|
|
29793
29810
|
const derived = await derive({
|
|
29794
29811
|
mediaKey: keyFrame.key,
|
|
@@ -29800,17 +29817,23 @@ var TrackCloser = class {
|
|
|
29800
29817
|
},
|
|
29801
29818
|
frameWidth: bestSeen.frameWidth,
|
|
29802
29819
|
frameHeight: bestSeen.frameHeight,
|
|
29803
|
-
timestamp:
|
|
29820
|
+
timestamp: keyFrame.timestamp
|
|
29804
29821
|
});
|
|
29805
|
-
if (!derived)
|
|
29822
|
+
if (!derived) {
|
|
29823
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
|
|
29824
|
+
return false;
|
|
29825
|
+
}
|
|
29806
29826
|
const mediaStore = this.deps.mediaStore();
|
|
29807
|
-
if (!mediaStore)
|
|
29827
|
+
if (!mediaStore) {
|
|
29828
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-media-store");
|
|
29829
|
+
return false;
|
|
29830
|
+
}
|
|
29808
29831
|
await mediaStore.put({
|
|
29809
29832
|
deviceId: t.deviceId,
|
|
29810
29833
|
ownerKind: "track",
|
|
29811
29834
|
ownerId: t.trackId,
|
|
29812
29835
|
kind: "thumbnail",
|
|
29813
|
-
timestamp:
|
|
29836
|
+
timestamp: keyFrame.timestamp,
|
|
29814
29837
|
data: derived.thumbnail
|
|
29815
29838
|
});
|
|
29816
29839
|
await mediaStore.put({
|
|
@@ -29818,7 +29841,7 @@ var TrackCloser = class {
|
|
|
29818
29841
|
ownerKind: "track",
|
|
29819
29842
|
ownerId: t.trackId,
|
|
29820
29843
|
kind: "thumbnailSmall",
|
|
29821
|
-
timestamp:
|
|
29844
|
+
timestamp: keyFrame.timestamp,
|
|
29822
29845
|
data: derived.thumbnailSmall
|
|
29823
29846
|
});
|
|
29824
29847
|
this.deps.logger.info("thumbnail derived from keyFrame at close (no live best-shot landed)", {
|
|
@@ -29829,6 +29852,7 @@ var TrackCloser = class {
|
|
|
29829
29852
|
skewMs: derived.skewMs
|
|
29830
29853
|
}
|
|
29831
29854
|
});
|
|
29855
|
+
return true;
|
|
29832
29856
|
} catch (err) {
|
|
29833
29857
|
this.deps.logger.debug("close-time keyFrame thumbnail derive failed", {
|
|
29834
29858
|
tags: { deviceId: t.deviceId },
|
|
@@ -29837,10 +29861,11 @@ var TrackCloser = class {
|
|
|
29837
29861
|
error: String(err)
|
|
29838
29862
|
}
|
|
29839
29863
|
});
|
|
29840
|
-
this.logNoThumbnailAtClose(t, "derive-threw", {
|
|
29864
|
+
if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-threw", {
|
|
29841
29865
|
keyFrameKey: keyFrame.key,
|
|
29842
29866
|
error: String(err)
|
|
29843
29867
|
});
|
|
29868
|
+
return false;
|
|
29844
29869
|
}
|
|
29845
29870
|
}
|
|
29846
29871
|
/**
|
|
@@ -30710,6 +30735,7 @@ var PlateGalleryProvider = class {
|
|
|
30710
30735
|
logger;
|
|
30711
30736
|
refreshGallery;
|
|
30712
30737
|
emitPlateGalleryChanged;
|
|
30738
|
+
eventMediaBaseUrl;
|
|
30713
30739
|
constructor(deps) {
|
|
30714
30740
|
this.plateStore = deps.plateStore;
|
|
30715
30741
|
this.vehicleStore = deps.vehicleStore;
|
|
@@ -30719,6 +30745,7 @@ var PlateGalleryProvider = class {
|
|
|
30719
30745
|
this.logger = deps.logger;
|
|
30720
30746
|
this.refreshGallery = deps.refreshGallery;
|
|
30721
30747
|
this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
|
|
30748
|
+
this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
|
|
30722
30749
|
}
|
|
30723
30750
|
/** Best-effort emit — never allowed to fail the mutation it follows. */
|
|
30724
30751
|
safeEmitPlateGalleryChanged(payload) {
|
|
@@ -30726,6 +30753,12 @@ var PlateGalleryProvider = class {
|
|
|
30726
30753
|
this.emitPlateGalleryChanged(payload);
|
|
30727
30754
|
} catch {}
|
|
30728
30755
|
}
|
|
30756
|
+
cropUrlFor(plate) {
|
|
30757
|
+
if (plate.mediaKey === void 0 || plate.mediaKey === "") return void 0;
|
|
30758
|
+
const base = this.eventMediaBaseUrl();
|
|
30759
|
+
if (base === null) return void 0;
|
|
30760
|
+
return `${base}/${encodeURIComponent(plate.mediaKey)}`;
|
|
30761
|
+
}
|
|
30729
30762
|
async plateCropBase64(plate) {
|
|
30730
30763
|
if (plate.mediaKey) {
|
|
30731
30764
|
const m = await this.mediaStore.getByKey(plate.mediaKey);
|
|
@@ -30734,8 +30767,9 @@ var PlateGalleryProvider = class {
|
|
|
30734
30767
|
const media = await this.mediaStore.listByOwner("plate", plate.id);
|
|
30735
30768
|
return media.length > 0 ? media[0].base64 : void 0;
|
|
30736
30769
|
}
|
|
30737
|
-
async toPlateInfo(plate, nameMap) {
|
|
30738
|
-
const base64 = await this.plateCropBase64(plate);
|
|
30770
|
+
async toPlateInfo(plate, nameMap, opts) {
|
|
30771
|
+
const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
|
|
30772
|
+
const cropUrl = this.cropUrlFor(plate);
|
|
30739
30773
|
const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
|
|
30740
30774
|
return {
|
|
30741
30775
|
plateId: plate.id,
|
|
@@ -30750,7 +30784,8 @@ var PlateGalleryProvider = class {
|
|
|
30750
30784
|
...vehicleName !== void 0 ? { vehicleName } : {},
|
|
30751
30785
|
...plate.plateBbox !== void 0 ? { plateBbox: plate.plateBbox } : {},
|
|
30752
30786
|
...plate.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: plate.keyFrameMediaKey } : {},
|
|
30753
|
-
...base64 !== void 0 ? { base64 } : {}
|
|
30787
|
+
...base64 !== void 0 ? { base64 } : {},
|
|
30788
|
+
...cropUrl !== void 0 ? { cropUrl } : {}
|
|
30754
30789
|
};
|
|
30755
30790
|
}
|
|
30756
30791
|
async vehicleNameMap() {
|
|
@@ -30769,7 +30804,7 @@ var PlateGalleryProvider = class {
|
|
|
30769
30804
|
const plate = await this.plateStore.get(`plate-${input.trackId}`);
|
|
30770
30805
|
if (!plate || plate.deviceId !== input.deviceId) return null;
|
|
30771
30806
|
const nameMap = await this.vehicleNameMap();
|
|
30772
|
-
return this.toPlateInfo(plate, nameMap);
|
|
30807
|
+
return this.toPlateInfo(plate, nameMap, { includeCrops: false });
|
|
30773
30808
|
}
|
|
30774
30809
|
async getPlateMedia(input) {
|
|
30775
30810
|
return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
|
|
@@ -35872,6 +35907,19 @@ function createEventMediaHandler(deps) {
|
|
|
35872
35907
|
};
|
|
35873
35908
|
}
|
|
35874
35909
|
//#endregion
|
|
35910
|
+
//#region src/pipeline-analytics/copy-face-crop-preview.ts
|
|
35911
|
+
var TRACK_PREVIEW_KINDS = new Set(["thumbnail", "thumbnailSmall"]);
|
|
35912
|
+
async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
|
|
35913
|
+
if ((await store.listTrackMedia(input.trackId)).some((m) => TRACK_PREVIEW_KINDS.has(m.kind))) return false;
|
|
35914
|
+
await store.putTrackThumbnail({
|
|
35915
|
+
deviceId: input.deviceId,
|
|
35916
|
+
trackId: input.trackId,
|
|
35917
|
+
timestamp: input.timestamp,
|
|
35918
|
+
data: input.crop
|
|
35919
|
+
});
|
|
35920
|
+
return true;
|
|
35921
|
+
}
|
|
35922
|
+
//#endregion
|
|
35875
35923
|
//#region src/pipeline-analytics/event-thumbnail-resolver.ts
|
|
35876
35924
|
/** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
|
|
35877
35925
|
* `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
|
|
@@ -35880,29 +35928,47 @@ function createEventMediaHandler(deps) {
|
|
|
35880
35928
|
function pickEventOwnedMedia(files) {
|
|
35881
35929
|
return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
|
|
35882
35930
|
}
|
|
35883
|
-
/** Pick a track's fallback media
|
|
35884
|
-
*
|
|
35885
|
-
*
|
|
35886
|
-
* Undefined when the
|
|
35931
|
+
/** Pick a track's fallback media when no subject crop exists: `thumbnailSmall`
|
|
35932
|
+
* → `thumbnail` → native `keyFrameSmall`/`keyFrame`/`fullFrame` → rolling
|
|
35933
|
+
* `lastFrame` → `firstFrame` → newest `snapshot` → any blob. A full frame
|
|
35934
|
+
* beats a class icon. Undefined when the owner has no media. */
|
|
35887
35935
|
function pickTrackFallbackMedia(files) {
|
|
35888
|
-
return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
35936
|
+
return files.find((f) => f.kind === "thumbnailSmall") ?? files.find((f) => f.kind === "thumbnail") ?? files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame") ?? files.find((f) => f.kind === "fullFrame") ?? files.find((f) => f.kind === "lastFrame") ?? files.find((f) => f.kind === "firstFrame") ?? [...files].reverse().find((f) => f.kind === "snapshot") ?? files[files.length - 1];
|
|
35889
35937
|
}
|
|
35890
35938
|
/**
|
|
35891
|
-
* Crop-forced resolution (the `?kind=crop`
|
|
35892
|
-
*
|
|
35893
|
-
*
|
|
35894
|
-
*
|
|
35895
|
-
*
|
|
35896
|
-
* returning undefined. A boxed frame (with the target's box drawn) is a real
|
|
35897
|
-
* preview; an empty tile is not. Clean is always tried FIRST so the common case
|
|
35898
|
-
* still serves an unboxed tile. Undefined only when the owner has NO media.
|
|
35939
|
+
* Crop-forced resolution (the `?kind=crop` / `?kind=thumbnailSmall` reel path).
|
|
35940
|
+
*
|
|
35941
|
+
* Ladder: body crop (track/event thumbnail) → tight face/plate crop → any
|
|
35942
|
+
* remaining image (keyFrame / lastFrame / …). The class icon is only honest
|
|
35943
|
+
* when the owner has no stored bytes at all.
|
|
35899
35944
|
*
|
|
35900
35945
|
* `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
|
|
35901
35946
|
* in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
|
|
35902
35947
|
* composition pure + unit-testable here.
|
|
35903
35948
|
*/
|
|
35904
|
-
function resolveCropForcedMedia(files, pickClean) {
|
|
35905
|
-
return pickClean(files) ?? pickTrackFallbackMedia(files);
|
|
35949
|
+
function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
|
|
35950
|
+
return pickClean(files) ?? pickClean(detailFiles) ?? pickTrackFallbackMedia([...files, ...detailFiles]);
|
|
35951
|
+
}
|
|
35952
|
+
/**
|
|
35953
|
+
* Event + track media (the body/scene) and face/plate media (the detail
|
|
35954
|
+
* crops). Face rows live under `face-${trackId}`; a track can close with
|
|
35955
|
+
* `hasEmbeddedFace` and ZERO track files (0e343f86).
|
|
35956
|
+
*/
|
|
35957
|
+
async function collectCropForcedMedia(source, id) {
|
|
35958
|
+
const eventFiles = await source.listByOwner("event", id);
|
|
35959
|
+
const trackFiles = await source.listByOwner("track", id);
|
|
35960
|
+
const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
|
|
35961
|
+
const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
|
|
35962
|
+
const trackId = ownerTrackId ?? id;
|
|
35963
|
+
const [faceFiles, plateFiles] = await Promise.all([source.listByOwner("face", `face-${trackId}`), source.listByOwner("plate", `plate-${trackId}`)]);
|
|
35964
|
+
return {
|
|
35965
|
+
bodyFiles: [
|
|
35966
|
+
...eventFiles,
|
|
35967
|
+
...trackFiles,
|
|
35968
|
+
...owningTrackFiles
|
|
35969
|
+
],
|
|
35970
|
+
detailFiles: [...faceFiles, ...plateFiles]
|
|
35971
|
+
};
|
|
35906
35972
|
}
|
|
35907
35973
|
/**
|
|
35908
35974
|
* Resolve the default-path media for an event id: the event's own media when it
|
|
@@ -35916,6 +35982,14 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
35916
35982
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
35917
35983
|
}
|
|
35918
35984
|
//#endregion
|
|
35985
|
+
//#region src/pipeline-analytics/ensure-track-hierarchy.ts
|
|
35986
|
+
async function ensureTrackHierarchyMedia(deps, input) {
|
|
35987
|
+
const tasks = [];
|
|
35988
|
+
if (!deps.isThumbnailLanded(input.trackId)) tasks.push(deps.captureBodyThumbnail(input));
|
|
35989
|
+
if (!deps.isKeyFrameLanded(input.trackId)) tasks.push(deps.captureKeyFrame(input));
|
|
35990
|
+
await Promise.all(tasks);
|
|
35991
|
+
}
|
|
35992
|
+
//#endregion
|
|
35919
35993
|
//#region src/pipeline-analytics/face-settings.ts
|
|
35920
35994
|
/**
|
|
35921
35995
|
* Per-device face-recognition settings. Cascade: a per-device override on top
|
|
@@ -36434,6 +36508,21 @@ var FaceRecognizer = class {
|
|
|
36434
36508
|
...held,
|
|
36435
36509
|
crop
|
|
36436
36510
|
});
|
|
36511
|
+
if (crop !== void 0 && input.frameHandle !== void 0) this.deps.onLiveHierarchy?.({
|
|
36512
|
+
deviceId: input.deviceId,
|
|
36513
|
+
trackId: c.trackId,
|
|
36514
|
+
timestamp: input.timestamp,
|
|
36515
|
+
bodyBbox: {
|
|
36516
|
+
x: c.bbox.x,
|
|
36517
|
+
y: c.bbox.y,
|
|
36518
|
+
w: c.bbox.w,
|
|
36519
|
+
h: c.bbox.h
|
|
36520
|
+
},
|
|
36521
|
+
frameWidth: input.frameWidth,
|
|
36522
|
+
frameHeight: input.frameHeight,
|
|
36523
|
+
cropPadding: input.cropPadding,
|
|
36524
|
+
frameHandle: input.frameHandle
|
|
36525
|
+
});
|
|
36437
36526
|
this.deps.logger.debug("face: best-face held", {
|
|
36438
36527
|
tags: {
|
|
36439
36528
|
deviceId: input.deviceId,
|
|
@@ -36583,6 +36672,22 @@ var FaceRecognizer = class {
|
|
|
36583
36672
|
});
|
|
36584
36673
|
return;
|
|
36585
36674
|
}
|
|
36675
|
+
try {
|
|
36676
|
+
await this.deps.copyCropToTrackPreview?.({
|
|
36677
|
+
deviceId,
|
|
36678
|
+
trackId,
|
|
36679
|
+
crop: held.crop,
|
|
36680
|
+
timestamp: held.timestamp
|
|
36681
|
+
});
|
|
36682
|
+
} catch (err) {
|
|
36683
|
+
this.deps.logger.debug("face: track preview copy failed", {
|
|
36684
|
+
tags: { deviceId },
|
|
36685
|
+
meta: {
|
|
36686
|
+
trackId,
|
|
36687
|
+
error: String(err)
|
|
36688
|
+
}
|
|
36689
|
+
});
|
|
36690
|
+
}
|
|
36586
36691
|
const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
|
|
36587
36692
|
try {
|
|
36588
36693
|
await this.deps.faceStore.insert({
|
|
@@ -39873,8 +39978,9 @@ var OverlayDetailStateStore = class {
|
|
|
39873
39978
|
*
|
|
39874
39979
|
* See docs/superpowers/specs/2026-07-17-package-zones-design.md §3.3 +
|
|
39875
39980
|
* docs/superpowers/specs/2026-07-17-detection-config-exposure-design.md
|
|
39876
|
-
* (surface C).
|
|
39877
|
-
*
|
|
39981
|
+
* (surface C). Confirmation is N frames (default 3); dwell default is 0
|
|
39982
|
+
* (an extra bar). The 15 s dwell plus a 5 s round-gap missed a real
|
|
39983
|
+
* parcel on device 615 (2026-08-24 13:40).
|
|
39878
39984
|
*/
|
|
39879
39985
|
var PackageDropSettingsSchema = object({
|
|
39880
39986
|
/**
|
|
@@ -39895,13 +40001,20 @@ var PackageDropSettingsSchema = object({
|
|
|
39895
40001
|
*/
|
|
39896
40002
|
packageDropEnabled: boolean().default(true),
|
|
39897
40003
|
/**
|
|
39898
|
-
*
|
|
39899
|
-
*
|
|
39900
|
-
*
|
|
39901
|
-
*
|
|
39902
|
-
*
|
|
40004
|
+
* Frames the box must be re-seen on before it is a parked package.
|
|
40005
|
+
* Same bar as tracker `minHits`: one frame is a flicker, two can still
|
|
40006
|
+
* be a passer-by's bag, three is "it stayed". A settle burst of three
|
|
40007
|
+
* frames in ~1 s is enough; no second governor round after the courier
|
|
40008
|
+
* left. Device 615 2026-08-24 13:40 expired at 1 round / 1.5 s dwell.
|
|
40009
|
+
*/
|
|
40010
|
+
packageDropMinFrames: number().int().min(1).max(20).default(3),
|
|
40011
|
+
/**
|
|
40012
|
+
* Extra OBSERVED dwell (seconds, since first-seen) on top of N frames.
|
|
40013
|
+
* 0 (the default) means the frame count is enough — a courier who drops
|
|
40014
|
+
* and leaves in 2 s still delivers. Raise this to reject a bag briefly
|
|
40015
|
+
* set down and snatched back.
|
|
39903
40016
|
*/
|
|
39904
|
-
packageDropDwellSec: number().int().min(0).max(3600).default(
|
|
40017
|
+
packageDropDwellSec: number().int().min(0).max(3600).default(0),
|
|
39905
40018
|
/** Reject tiny far-field blobs: min bbox area as a fraction of the frame. */
|
|
39906
40019
|
packageDropMinBboxAreaFrac: number().min(0).max(1).default(.004),
|
|
39907
40020
|
/**
|
|
@@ -39919,13 +40032,15 @@ var PackageDropSettingsSchema = object({
|
|
|
39919
40032
|
packageDropPickupEnabled: boolean().default(true),
|
|
39920
40033
|
/**
|
|
39921
40034
|
* 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
|
-
*
|
|
40035
|
+
* `delivered` if a non-package track (person/vehicle/animal) was active IN
|
|
40036
|
+
* A CONFIGURED PACKAGE DROP-OFF ZONE within this many seconds BEFORE the
|
|
40037
|
+
* box first appeared — and a departure only counts as `picked-up` with
|
|
40038
|
+
* somebody in that zone around the disappearance. Whole-camera motion (a
|
|
40039
|
+
* walker at the far gate) does not count. 0 disables both gates. Device
|
|
40040
|
+
* 615's streetlight wedge cycled delivered→picked-up ~23×/night with
|
|
40041
|
+
* nobody in the drop-off zone. Default 300 s: wide enough to absorb the
|
|
40042
|
+
* motion-settle governor's cadence (the box is often first SEEN minutes
|
|
40043
|
+
* after the courier left).
|
|
39929
40044
|
*/
|
|
39930
40045
|
packageDropActivityWindowSec: number().int().min(0).max(3600).default(300),
|
|
39931
40046
|
/**
|
|
@@ -39949,6 +40064,7 @@ function resolvePackageDropSettings(raw) {
|
|
|
39949
40064
|
};
|
|
39950
40065
|
return {
|
|
39951
40066
|
packageDropEnabled: pick("packageDropEnabled"),
|
|
40067
|
+
packageDropMinFrames: pick("packageDropMinFrames"),
|
|
39952
40068
|
packageDropDwellSec: pick("packageDropDwellSec"),
|
|
39953
40069
|
packageDropMinBboxAreaFrac: pick("packageDropMinBboxAreaFrac"),
|
|
39954
40070
|
packageDropMaxBboxAreaFrac: pick("packageDropMaxBboxAreaFrac"),
|
|
@@ -40206,29 +40322,6 @@ var DeferredBirthRegistry = class {
|
|
|
40206
40322
|
this.byDevice.delete(deviceKey);
|
|
40207
40323
|
}
|
|
40208
40324
|
};
|
|
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
40325
|
var groupActions = defineCustomActions({
|
|
40233
40326
|
"analytics.listGroups": customAction(object({
|
|
40234
40327
|
deviceId: number().int().optional(),
|
|
@@ -40242,61 +40335,6 @@ var groupActions = defineCustomActions({
|
|
|
40242
40335
|
})),
|
|
40243
40336
|
"analytics.getGroup": customAction(object({ groupId: string().min(1) }), object({ group: AnalyticsGroupDetailSchema.nullable() }))
|
|
40244
40337
|
});
|
|
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
40338
|
function trackToLite(track) {
|
|
40301
40339
|
return {
|
|
40302
40340
|
trackId: track.trackId,
|
|
@@ -40306,56 +40344,18 @@ function trackToLite(track) {
|
|
|
40306
40344
|
lastSeen: track.lastSeen
|
|
40307
40345
|
};
|
|
40308
40346
|
}
|
|
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
40347
|
/**
|
|
40355
40348
|
* Gallery and digest read the same ladder: groupCrop, then the native-frame
|
|
40356
|
-
* pointer, then member
|
|
40357
|
-
*
|
|
40349
|
+
* pointer, then a member's clean full frame. Plate / face / member
|
|
40350
|
+
* thumbnails never reach the picker — a plate read overwrites the vehicle
|
|
40351
|
+
* first-level crop under the same `thumbnail` kind. A multi with no crop
|
|
40352
|
+
* still has a picture if the sealer stored `nativeKey`.
|
|
40358
40353
|
*/
|
|
40354
|
+
var DIGEST_MEMBER_FALLBACK_KINDS = new Set([
|
|
40355
|
+
"keyFrame",
|
|
40356
|
+
"keyFrameSmall",
|
|
40357
|
+
"fullFrame"
|
|
40358
|
+
]);
|
|
40359
40359
|
function digestMediaRows(input) {
|
|
40360
40360
|
const owned = [];
|
|
40361
40361
|
if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
|
|
@@ -40369,7 +40369,7 @@ function digestMediaRows(input) {
|
|
|
40369
40369
|
timestamp: input.timestamp
|
|
40370
40370
|
});
|
|
40371
40371
|
if (owned.length > 0) return owned;
|
|
40372
|
-
return input.memberRows;
|
|
40372
|
+
return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
|
|
40373
40373
|
}
|
|
40374
40374
|
var MATCH_WINDOW_MS = 1e3;
|
|
40375
40375
|
var MOVE_EPS = .35;
|
|
@@ -40923,6 +40923,34 @@ function toInstant(t) {
|
|
|
40923
40923
|
confidence: t.importance
|
|
40924
40924
|
};
|
|
40925
40925
|
}
|
|
40926
|
+
function memberBoxesAt(members, at, frame) {
|
|
40927
|
+
const w = Math.max(frame.width, 1);
|
|
40928
|
+
const h = Math.max(frame.height, 1);
|
|
40929
|
+
const boxes = [];
|
|
40930
|
+
for (const member of members) {
|
|
40931
|
+
let best = member.positions[0];
|
|
40932
|
+
let bestDt = Number.POSITIVE_INFINITY;
|
|
40933
|
+
for (const p of member.positions) {
|
|
40934
|
+
const dt = Math.abs(p.timestamp - at);
|
|
40935
|
+
if (dt <= 1e3 && dt < bestDt) {
|
|
40936
|
+
best = p;
|
|
40937
|
+
bestDt = dt;
|
|
40938
|
+
}
|
|
40939
|
+
}
|
|
40940
|
+
if (best === void 0 || bestDt === Number.POSITIVE_INFINITY) continue;
|
|
40941
|
+
boxes.push({
|
|
40942
|
+
bbox: {
|
|
40943
|
+
x: best.bbox.x / w,
|
|
40944
|
+
y: best.bbox.y / h,
|
|
40945
|
+
w: best.bbox.w / w,
|
|
40946
|
+
h: best.bbox.h / h
|
|
40947
|
+
},
|
|
40948
|
+
label: member.className,
|
|
40949
|
+
observedAt: best.timestamp
|
|
40950
|
+
});
|
|
40951
|
+
}
|
|
40952
|
+
return boxes;
|
|
40953
|
+
}
|
|
40926
40954
|
function toSealer(track) {
|
|
40927
40955
|
return {
|
|
40928
40956
|
trackId: track.trackId,
|
|
@@ -40939,7 +40967,11 @@ var GroupSealer = class {
|
|
|
40939
40967
|
constructor(deps) {
|
|
40940
40968
|
this.deps = deps;
|
|
40941
40969
|
}
|
|
40970
|
+
get processEnabled() {
|
|
40971
|
+
return this.deps.enabled ?? false;
|
|
40972
|
+
}
|
|
40942
40973
|
async flushIdle(buffer, now = this.deps.now()) {
|
|
40974
|
+
if (!this.processEnabled) return 0;
|
|
40943
40975
|
let written = 0;
|
|
40944
40976
|
for (const group of buffer.idleGroups(now, GROUP_SESSION_IDLE_MS)) if (await this.persistLiveGroup(group, buffer)) {
|
|
40945
40977
|
buffer.drop([group.id]);
|
|
@@ -40952,6 +40984,7 @@ var GroupSealer = class {
|
|
|
40952
40984
|
* into the live buffer, persist whoever is already idle, keep the rest in RAM.
|
|
40953
40985
|
*/
|
|
40954
40986
|
async recoverCamera(deviceId, buffer) {
|
|
40987
|
+
if (!this.processEnabled) return 0;
|
|
40955
40988
|
if (buffer.isPrimed(deviceId)) return 0;
|
|
40956
40989
|
const now = this.deps.now();
|
|
40957
40990
|
try {
|
|
@@ -40980,6 +41013,7 @@ var GroupSealer = class {
|
|
|
40980
41013
|
* is what wedged hub-main.
|
|
40981
41014
|
*/
|
|
40982
41015
|
async sealWindow(deviceId, since, until) {
|
|
41016
|
+
if (!this.processEnabled) return 0;
|
|
40983
41017
|
try {
|
|
40984
41018
|
const closed = await this.deps.trackStore.queryHistorical({
|
|
40985
41019
|
deviceId,
|
|
@@ -41094,26 +41128,54 @@ var GroupSealer = class {
|
|
|
41094
41128
|
return;
|
|
41095
41129
|
}
|
|
41096
41130
|
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
|
-
|
|
41131
|
+
const raw = await sharp(source.jpeg).removeAlpha().toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
|
|
41132
|
+
if (raw.info.channels === 3) {
|
|
41133
|
+
const union = {
|
|
41134
|
+
x: instant.ux * raw.info.width,
|
|
41135
|
+
y: instant.uy * raw.info.height,
|
|
41136
|
+
w: instant.uw * raw.info.width,
|
|
41137
|
+
h: instant.uh * raw.info.height
|
|
41138
|
+
};
|
|
41139
|
+
const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
|
|
41140
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
41141
|
+
deviceId,
|
|
41142
|
+
ownerKind: "group",
|
|
41143
|
+
ownerId: draft.id,
|
|
41144
|
+
kind: "thumbnail",
|
|
41145
|
+
timestamp,
|
|
41146
|
+
data: crop
|
|
41147
|
+
});
|
|
41148
|
+
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
41149
|
+
}
|
|
41150
|
+
} catch (err) {
|
|
41151
|
+
this.deps.logger.warn("group sealer: crop failed", {
|
|
41152
|
+
tags: { deviceId },
|
|
41153
|
+
meta: {
|
|
41154
|
+
groupId: draft.id,
|
|
41155
|
+
error: String(err)
|
|
41156
|
+
}
|
|
41157
|
+
});
|
|
41158
|
+
}
|
|
41159
|
+
try {
|
|
41160
|
+
const composed = await composeGroupFrame({
|
|
41161
|
+
jpeg: source.jpeg,
|
|
41162
|
+
boxes: memberBoxesAt(instantMembers, timestamp, frame),
|
|
41163
|
+
at: timestamp,
|
|
41164
|
+
logger: this.deps.logger,
|
|
41165
|
+
deviceId,
|
|
41166
|
+
groupKey: draft.id
|
|
41167
|
+
});
|
|
41168
|
+
if (composed === null) return;
|
|
41169
|
+
await this.deps.mediaStore.put({
|
|
41107
41170
|
deviceId,
|
|
41108
41171
|
ownerKind: "group",
|
|
41109
41172
|
ownerId: draft.id,
|
|
41110
|
-
kind: "
|
|
41173
|
+
kind: "fullFrameBoxed",
|
|
41111
41174
|
timestamp,
|
|
41112
|
-
data:
|
|
41175
|
+
data: composed.bytes
|
|
41113
41176
|
});
|
|
41114
|
-
await this.deps.groupStore.setMediaKey(draft.id, mediaKey);
|
|
41115
41177
|
} catch (err) {
|
|
41116
|
-
this.deps.logger.warn("group sealer:
|
|
41178
|
+
this.deps.logger.warn("group sealer: boxed frame failed", {
|
|
41117
41179
|
tags: { deviceId },
|
|
41118
41180
|
meta: {
|
|
41119
41181
|
groupId: draft.id,
|
|
@@ -41138,6 +41200,37 @@ var GroupSealer = class {
|
|
|
41138
41200
|
}
|
|
41139
41201
|
};
|
|
41140
41202
|
//#endregion
|
|
41203
|
+
//#region src/pipeline-analytics/group/group-cursor.ts
|
|
41204
|
+
function encodeGroupCursor(cursor) {
|
|
41205
|
+
return Buffer.from(JSON.stringify({
|
|
41206
|
+
c: cursor.closedAt,
|
|
41207
|
+
i: cursor.groupId
|
|
41208
|
+
}), "utf8").toString("base64url");
|
|
41209
|
+
}
|
|
41210
|
+
function decodeGroupCursor(raw) {
|
|
41211
|
+
let parsed;
|
|
41212
|
+
try {
|
|
41213
|
+
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
41214
|
+
} catch {
|
|
41215
|
+
throw new Error("listGroups: malformed cursor");
|
|
41216
|
+
}
|
|
41217
|
+
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");
|
|
41218
|
+
return {
|
|
41219
|
+
closedAt: parsed.c,
|
|
41220
|
+
groupId: parsed.i
|
|
41221
|
+
};
|
|
41222
|
+
}
|
|
41223
|
+
function compareGroupDesc(a, b) {
|
|
41224
|
+
if (a.closedAt !== b.closedAt) return b.closedAt - a.closedAt;
|
|
41225
|
+
if (a.id === b.id) return 0;
|
|
41226
|
+
return a.id < b.id ? 1 : -1;
|
|
41227
|
+
}
|
|
41228
|
+
/** True when `row` sits strictly AFTER the cursor in DESC order. */
|
|
41229
|
+
function isAfterGroupCursor(row, cursor) {
|
|
41230
|
+
if (row.closedAt < cursor.closedAt) return true;
|
|
41231
|
+
return row.closedAt === cursor.closedAt && row.id < cursor.groupId;
|
|
41232
|
+
}
|
|
41233
|
+
//#endregion
|
|
41141
41234
|
//#region src/pipeline-analytics/group/group-store.ts
|
|
41142
41235
|
/**
|
|
41143
41236
|
* The group ENTITY store — a co-moving partition of one session, with one
|
|
@@ -41399,6 +41492,24 @@ var TrackGroupStore = class {
|
|
|
41399
41492
|
data: r.data
|
|
41400
41493
|
}));
|
|
41401
41494
|
}
|
|
41495
|
+
/**
|
|
41496
|
+
* Membership for a page of groups in ONE query. `listGroupRecords` used to
|
|
41497
|
+
* call {@link listMembers} per row — 60 groups is 60 settings-store RPCs,
|
|
41498
|
+
* ~20 s, and the viewer feed times out into a retry loop.
|
|
41499
|
+
*/
|
|
41500
|
+
async listMembersForGroups(groupIds) {
|
|
41501
|
+
if (groupIds.length === 0) return [];
|
|
41502
|
+
return (await this.store.query.query({
|
|
41503
|
+
collection: TRACK_GROUP_MEMBERS_COLLECTION,
|
|
41504
|
+
filter: {
|
|
41505
|
+
whereIn: { groupId: [...groupIds] },
|
|
41506
|
+
limit: 1e4
|
|
41507
|
+
}
|
|
41508
|
+
})).map((r) => this.normalizeMember({
|
|
41509
|
+
id: r.id,
|
|
41510
|
+
data: r.data
|
|
41511
|
+
}));
|
|
41512
|
+
}
|
|
41402
41513
|
async membersOfTracks(trackIds) {
|
|
41403
41514
|
const out = [];
|
|
41404
41515
|
for (const trackId of trackIds) {
|
|
@@ -41414,20 +41525,29 @@ var TrackGroupStore = class {
|
|
|
41414
41525
|
return out;
|
|
41415
41526
|
}
|
|
41416
41527
|
/**
|
|
41417
|
-
* Newest-closed page for the
|
|
41528
|
+
* Newest-closed page for the Groups feed.
|
|
41418
41529
|
*
|
|
41419
41530
|
* Time filter is overlap (`closedAt >= since` and `openedAt <= until`),
|
|
41420
41531
|
* same as {@link listOverlapping}. Pagination is applied AFTER the window
|
|
41421
41532
|
* so a day cursor is honest rather than a raw-row offset that skips live
|
|
41422
|
-
* groups on a busy camera.
|
|
41533
|
+
* groups on a busy camera. `deviceIds: []` is an empty page (same as
|
|
41534
|
+
* `listRecentTracks`); omit the field for every camera. `deviceId` stays
|
|
41535
|
+
* as the single-camera alias the custom-action bridge still sends.
|
|
41423
41536
|
*/
|
|
41424
41537
|
async listRecent(input) {
|
|
41425
41538
|
const limit = Math.min(Math.max(input.limit ?? 40, 1), 100);
|
|
41426
41539
|
const offset = Math.max(input.offset ?? 0, 0);
|
|
41427
|
-
const
|
|
41540
|
+
const deviceIds = input.deviceIds !== void 0 ? input.deviceIds : input.deviceId !== void 0 ? [input.deviceId] : void 0;
|
|
41541
|
+
if (deviceIds !== void 0 && deviceIds.length === 0) return {
|
|
41542
|
+
groups: [],
|
|
41543
|
+
hasMore: false,
|
|
41544
|
+
nextCursor: null
|
|
41545
|
+
};
|
|
41546
|
+
const cursor = input.cursor !== void 0 && input.cursor.length > 0 ? decodeGroupCursor(input.cursor) : null;
|
|
41547
|
+
const filtered = (await this.store.query.query({
|
|
41428
41548
|
collection: TRACK_GROUPS_COLLECTION,
|
|
41429
41549
|
filter: {
|
|
41430
|
-
...
|
|
41550
|
+
...deviceIds !== void 0 && deviceIds.length === 1 ? { where: { deviceId: deviceIds[0] } } : deviceIds !== void 0 ? { whereIn: { deviceId: [...deviceIds] } } : {},
|
|
41431
41551
|
orderBy: {
|
|
41432
41552
|
field: "closedAt",
|
|
41433
41553
|
direction: "desc"
|
|
@@ -41440,11 +41560,20 @@ var TrackGroupStore = class {
|
|
|
41440
41560
|
})).filter((g) => {
|
|
41441
41561
|
if (input.sinceMs !== void 0 && g.closedAt < input.sinceMs) return false;
|
|
41442
41562
|
if (input.untilMs !== void 0 && g.openedAt > input.untilMs) return false;
|
|
41563
|
+
if (cursor !== null && !isAfterGroupCursor(g, cursor)) return false;
|
|
41443
41564
|
return true;
|
|
41444
|
-
}).
|
|
41565
|
+
}).toSorted(compareGroupDesc);
|
|
41566
|
+
const slice = (cursor !== null ? filtered : filtered.slice(offset)).slice(0, limit + 1);
|
|
41567
|
+
const groups = slice.slice(0, limit);
|
|
41568
|
+
const hasMore = slice.length > limit;
|
|
41569
|
+
const last = groups[groups.length - 1];
|
|
41445
41570
|
return {
|
|
41446
|
-
groups
|
|
41447
|
-
hasMore
|
|
41571
|
+
groups,
|
|
41572
|
+
hasMore,
|
|
41573
|
+
nextCursor: hasMore && last !== void 0 ? encodeGroupCursor({
|
|
41574
|
+
closedAt: last.closedAt,
|
|
41575
|
+
groupId: last.id
|
|
41576
|
+
}) : null
|
|
41448
41577
|
};
|
|
41449
41578
|
}
|
|
41450
41579
|
/** Groups whose life overlaps `[since, until]` on one camera. */
|
|
@@ -42776,7 +42905,7 @@ function evaluateStationaryPromotion(input) {
|
|
|
42776
42905
|
};
|
|
42777
42906
|
}
|
|
42778
42907
|
var DEFAULT_PACKAGE_STILLNESS_CONFIG = {
|
|
42779
|
-
|
|
42908
|
+
minHits: 3,
|
|
42780
42909
|
roundGapMs: 5e3,
|
|
42781
42910
|
ttlMs: 5 * 6e4,
|
|
42782
42911
|
assocIou: .3
|
|
@@ -42809,8 +42938,9 @@ var PackageStillnessTracker = class {
|
|
|
42809
42938
|
const prev = next[index];
|
|
42810
42939
|
if (prev === void 0) continue;
|
|
42811
42940
|
consumed.add(prev.id);
|
|
42812
|
-
const updated = confirm(prev, input, s
|
|
42813
|
-
|
|
42941
|
+
const updated = confirm(prev, input, s);
|
|
42942
|
+
const minHits = input.minHits ?? this.config.minHits;
|
|
42943
|
+
if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
|
|
42814
42944
|
next[index] = updated;
|
|
42815
42945
|
continue;
|
|
42816
42946
|
}
|
|
@@ -42834,7 +42964,7 @@ var PackageStillnessTracker = class {
|
|
|
42834
42964
|
id: c.id,
|
|
42835
42965
|
className: c.className,
|
|
42836
42966
|
bbox: { ...c.bbox },
|
|
42837
|
-
|
|
42967
|
+
hits: c.hits,
|
|
42838
42968
|
firstSeenAt: c.firstSeenAt,
|
|
42839
42969
|
lastSeenAt: c.lastSeenAt,
|
|
42840
42970
|
activityBackedAtOpen: c.activityBackedAtOpen
|
|
@@ -42892,7 +43022,7 @@ var PackageStillnessTracker = class {
|
|
|
42892
43022
|
frameHeight: input.frameHeight,
|
|
42893
43023
|
firstSeenAt: input.timestamp,
|
|
42894
43024
|
lastSeenAt: input.timestamp,
|
|
42895
|
-
|
|
43025
|
+
hits: 1,
|
|
42896
43026
|
promotedAt: null,
|
|
42897
43027
|
activityBackedAtOpen,
|
|
42898
43028
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
@@ -42902,18 +43032,16 @@ var PackageStillnessTracker = class {
|
|
|
42902
43032
|
};
|
|
42903
43033
|
}
|
|
42904
43034
|
};
|
|
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;
|
|
43035
|
+
/** Re-confirm a candidate with this frame's box. Every sighting is a hit —
|
|
43036
|
+
* a settle burst of three frames in one second is three confirmations. */
|
|
43037
|
+
function confirm(prev, input, s) {
|
|
42910
43038
|
return {
|
|
42911
43039
|
...prev,
|
|
42912
43040
|
bbox: { ...s.bbox },
|
|
42913
43041
|
frameWidth: input.frameWidth,
|
|
42914
43042
|
frameHeight: input.frameHeight,
|
|
42915
43043
|
lastSeenAt: input.timestamp,
|
|
42916
|
-
|
|
43044
|
+
hits: prev.hits + 1,
|
|
42917
43045
|
...s.trackId !== void 0 ? { sourceTrackId: s.trackId } : {},
|
|
42918
43046
|
...s.label !== void 0 ? { label: s.label } : {},
|
|
42919
43047
|
...s.confidence !== void 0 ? { confidence: s.confidence } : {},
|
|
@@ -42938,6 +43066,31 @@ function toEntry(c) {
|
|
|
42938
43066
|
};
|
|
42939
43067
|
}
|
|
42940
43068
|
//#endregion
|
|
43069
|
+
//#region src/pipeline-analytics/pipeline/package/package-zone-activity.ts
|
|
43070
|
+
/**
|
|
43071
|
+
* Whether a live track is motion inside a configured package drop-off zone.
|
|
43072
|
+
*
|
|
43073
|
+
* The stillness activity gate used to stamp ANY non-package track on the
|
|
43074
|
+
* camera. A walker at the far gate then "delivered" the streetlight wedge
|
|
43075
|
+
* on the doormat. The operator's prerequisite is motion in the drop-off
|
|
43076
|
+
* zone the package rule points at — the same zones `PackageDropDetector`
|
|
43077
|
+
* uses for the centroid gate.
|
|
43078
|
+
*/
|
|
43079
|
+
function isDelivererInPackageZone(trackZones, packageZoneIds) {
|
|
43080
|
+
if (packageZoneIds.size === 0 || trackZones.length === 0) return false;
|
|
43081
|
+
for (const z of trackZones) if (packageZoneIds.has(z)) return true;
|
|
43082
|
+
return false;
|
|
43083
|
+
}
|
|
43084
|
+
/** Zone ids referenced by enabled `package`-stage rules. Disabled rules do not count. */
|
|
43085
|
+
function collectPackageZoneIds(rules) {
|
|
43086
|
+
const ids = /* @__PURE__ */ new Set();
|
|
43087
|
+
for (const r of rules) {
|
|
43088
|
+
if (r.enabled === false) continue;
|
|
43089
|
+
for (const z of r.zoneIds) ids.add(z);
|
|
43090
|
+
}
|
|
43091
|
+
return ids;
|
|
43092
|
+
}
|
|
43093
|
+
//#endregion
|
|
42941
43094
|
//#region src/pipeline-analytics/pipeline/stationary/stationary-zones.ts
|
|
42942
43095
|
/**
|
|
42943
43096
|
* Zone ids whose 0–1 polygon contains the entry's normalised bbox centroid.
|
|
@@ -44114,6 +44267,12 @@ var PlateRecognizer = class {
|
|
|
44114
44267
|
text: c.plateText,
|
|
44115
44268
|
score: c.plateScore,
|
|
44116
44269
|
bbox: c.plateBbox,
|
|
44270
|
+
bodyBbox: {
|
|
44271
|
+
x: c.bbox.x,
|
|
44272
|
+
y: c.bbox.y,
|
|
44273
|
+
w: c.bbox.w,
|
|
44274
|
+
h: c.bbox.h
|
|
44275
|
+
},
|
|
44117
44276
|
timestamp: input.timestamp,
|
|
44118
44277
|
frameWidth: input.frameWidth,
|
|
44119
44278
|
frameHeight: input.frameHeight,
|
|
@@ -44187,6 +44346,16 @@ var PlateRecognizer = class {
|
|
|
44187
44346
|
}
|
|
44188
44347
|
});
|
|
44189
44348
|
}
|
|
44349
|
+
if (crop !== void 0 && input.frameHandle !== void 0 && input.bodyBbox !== void 0) this.deps.onLiveHierarchy?.({
|
|
44350
|
+
deviceId: input.deviceId,
|
|
44351
|
+
trackId: input.trackId,
|
|
44352
|
+
timestamp: input.timestamp,
|
|
44353
|
+
bodyBbox: input.bodyBbox,
|
|
44354
|
+
frameWidth: input.frameWidth,
|
|
44355
|
+
frameHeight: input.frameHeight,
|
|
44356
|
+
cropPadding: input.cropPadding,
|
|
44357
|
+
frameHandle: input.frameHandle
|
|
44358
|
+
});
|
|
44190
44359
|
this.bestPlate.set(input.trackId, {
|
|
44191
44360
|
text: input.text,
|
|
44192
44361
|
score: input.score,
|
|
@@ -48221,14 +48390,23 @@ function isAfterCursor(row, cursor) {
|
|
|
48221
48390
|
if (row.lastSeen < cursor.lastSeen) return true;
|
|
48222
48391
|
return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
|
|
48223
48392
|
}
|
|
48393
|
+
var VEHICLE_CLASSES = new Set([
|
|
48394
|
+
"vehicle",
|
|
48395
|
+
"car",
|
|
48396
|
+
"truck",
|
|
48397
|
+
"bus"
|
|
48398
|
+
]);
|
|
48399
|
+
function motionlessMaxPxForClass(className) {
|
|
48400
|
+
return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
|
|
48401
|
+
}
|
|
48224
48402
|
function netDisplacementPx(positions) {
|
|
48225
48403
|
const first = positions[0];
|
|
48226
48404
|
const last = positions[positions.length - 1];
|
|
48227
48405
|
if (!first || !last) return 0;
|
|
48228
48406
|
return Math.hypot(last.x - first.x, last.y - first.y);
|
|
48229
48407
|
}
|
|
48230
|
-
function isShortMotionlessTrack(lifeMs, movedPx) {
|
|
48231
|
-
return lifeMs <= 25e3 && movedPx <=
|
|
48408
|
+
function isShortMotionlessTrack(lifeMs, movedPx, className) {
|
|
48409
|
+
return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
|
|
48232
48410
|
}
|
|
48233
48411
|
//#endregion
|
|
48234
48412
|
//#region src/pipeline-analytics/store/track-store.ts
|
|
@@ -48812,7 +48990,7 @@ var TrackStore = class {
|
|
|
48812
48990
|
for (const [trackId, t] of this.active) {
|
|
48813
48991
|
if (nowMs - t.lastSeen < this.config.ttlMs) continue;
|
|
48814
48992
|
t.active = false;
|
|
48815
|
-
if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions))) t.closedReason = "stationary-promotion";
|
|
48993
|
+
if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions), t.className)) t.closedReason = "stationary-promotion";
|
|
48816
48994
|
const record = cloneTrack(t);
|
|
48817
48995
|
try {
|
|
48818
48996
|
await this.persistCompleted(record);
|
|
@@ -49841,6 +50019,28 @@ var TrackStore = class {
|
|
|
49841
50019
|
return this.rowToTrack(row.id, row.data);
|
|
49842
50020
|
}
|
|
49843
50021
|
/**
|
|
50022
|
+
* Batch read of persisted tracks. `listGroupRecords` cannot call
|
|
50023
|
+
* {@link getPersistedByTrackId} per member — that is one settings-store RPC
|
|
50024
|
+
* per row and a 60-group page takes ~20 s, which the viewer feed times out.
|
|
50025
|
+
*/
|
|
50026
|
+
async getPersistedByTrackIds(trackIds) {
|
|
50027
|
+
if (trackIds.length === 0) return [];
|
|
50028
|
+
const PAGE = 500;
|
|
50029
|
+
const out = [];
|
|
50030
|
+
for (let i = 0; i < trackIds.length; i += PAGE) {
|
|
50031
|
+
const chunk = trackIds.slice(i, i + PAGE);
|
|
50032
|
+
const rows = await this.store.query.query({
|
|
50033
|
+
collection: TRACKS_COLLECTION,
|
|
50034
|
+
filter: {
|
|
50035
|
+
whereIn: { id: [...chunk] },
|
|
50036
|
+
limit: chunk.length
|
|
50037
|
+
}
|
|
50038
|
+
});
|
|
50039
|
+
for (const row of rows) out.push(this.rowToTrack(row.id, row.data));
|
|
50040
|
+
}
|
|
50041
|
+
return out;
|
|
50042
|
+
}
|
|
50043
|
+
/**
|
|
49844
50044
|
* Persist a SYNTHETIC track (a sensor/control event projected into the
|
|
49845
50045
|
* unified track store). Unlike `persistCompleted` there is no live in-RAM
|
|
49846
50046
|
* state and no trajectory envelope (synthetic tracks carry no positions) —
|
|
@@ -54913,11 +55113,22 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54913
55113
|
description: "Also emit a package-picked-up event when the delivered package leaves.",
|
|
54914
55114
|
default: PACKAGE_DROP_DEFAULTS.packageDropPickupEnabled
|
|
54915
55115
|
},
|
|
55116
|
+
{
|
|
55117
|
+
type: "slider",
|
|
55118
|
+
key: "packageDropMinFrames",
|
|
55119
|
+
label: "Confirmation frames",
|
|
55120
|
+
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.",
|
|
55121
|
+
min: 1,
|
|
55122
|
+
max: 10,
|
|
55123
|
+
step: 1,
|
|
55124
|
+
default: PACKAGE_DROP_DEFAULTS.packageDropMinFrames,
|
|
55125
|
+
showValue: true
|
|
55126
|
+
},
|
|
54916
55127
|
{
|
|
54917
55128
|
type: "slider",
|
|
54918
55129
|
key: "packageDropDwellSec",
|
|
54919
55130
|
label: "Minimum dwell",
|
|
54920
|
-
description: "
|
|
55131
|
+
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
55132
|
min: 0,
|
|
54922
55133
|
max: 300,
|
|
54923
55134
|
step: 5,
|
|
@@ -54951,7 +55162,7 @@ function buildGlobalSettingsSchema(cap) {
|
|
|
54951
55162
|
type: "slider",
|
|
54952
55163
|
key: "packageDropActivityWindowSec",
|
|
54953
55164
|
label: "Deliverer window",
|
|
54954
|
-
description: "A delivery only counts if a person or vehicle was
|
|
55165
|
+
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
55166
|
min: 0,
|
|
54956
55167
|
max: 1800,
|
|
54957
55168
|
step: 30,
|
|
@@ -56785,10 +56996,12 @@ var CONFIRMATION_CROP_MAX_WIDTH = 320;
|
|
|
56785
56996
|
* a late confirmation is still worth running; see `DEFAULT_CAPTURE_PRIORITY`). */
|
|
56786
56997
|
var ENRICHMENT_CAPTURE_STALE_MS = 3e3;
|
|
56787
56998
|
/** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
|
|
56788
|
-
* The `device.state-changed` push
|
|
56789
|
-
*
|
|
56790
|
-
*
|
|
56791
|
-
|
|
56999
|
+
* The `device.state-changed` push reaches a forked child through
|
|
57000
|
+
* `createEventBusSliceSource` (the DeviceProxy no longer waits on
|
|
57001
|
+
* `live.onEvent`). Events are still lossy telemetry (D8); this timer
|
|
57002
|
+
* is the RPC reconcile, not the mechanism. Five minutes matches the
|
|
57003
|
+
* viewer's `RECONCILE_MS`. */
|
|
57004
|
+
var ZONE_SLICE_RECONCILE_MS = 3e5;
|
|
56792
57005
|
/** How often the evaluating node re-reads the central scene set. */
|
|
56793
57006
|
var SCENE_RELOAD_INTERVAL_MS = 3e4;
|
|
56794
57007
|
/** How long the install coordinates are trusted between settings reads. */
|
|
@@ -56852,38 +57065,31 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
|
|
|
56852
57065
|
*
|
|
56853
57066
|
* `thumbnail` joined the clean set on 2026-07-17: the track best-thumbnail is
|
|
56854
57067
|
* now a subject-centered crop (event-media-dispatcher), and for DEGENERATE
|
|
56855
|
-
* tracks (no object event → no `crop`) it is the ONLY subject-centered media
|
|
56856
|
-
*
|
|
56857
|
-
*
|
|
56858
|
-
*
|
|
56859
|
-
*
|
|
56860
|
-
*
|
|
56861
|
-
*
|
|
56862
|
-
*
|
|
56863
|
-
*
|
|
56864
|
-
* over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
|
|
56865
|
-
* keep resolving.
|
|
57068
|
+
* tracks (no object event → no `crop`) it is the ONLY subject-centered media.
|
|
57069
|
+
*
|
|
57070
|
+
* Full-scene kinds (`fullFrame`, `keyFrame`, `keyFrameSmall`) stay out of
|
|
57071
|
+
* this list so a crop is preferred when one exists. `resolveCropForcedMedia`
|
|
57072
|
+
* then falls through to those full-scene kinds — a keyFrame beats an icon.
|
|
57073
|
+
* An *explicit* `?kind=keyFrame` / `?kind=snapshot` still matches exactly, so
|
|
57074
|
+
* videoclips and other kind-specific fetches keep working.
|
|
57075
|
+
*
|
|
57076
|
+
* The retired-but-still-stored kind (`crop`) stays listed so old rows resolve.
|
|
56866
57077
|
*/
|
|
56867
57078
|
var CLEAN_MEDIA_KINDS = [
|
|
56868
57079
|
"crop",
|
|
56869
57080
|
"thumbnail",
|
|
56870
|
-
"thumbnailSmall"
|
|
56871
|
-
"fullFrame",
|
|
56872
|
-
"keyFrame",
|
|
56873
|
-
"keyFrameSmall"
|
|
57081
|
+
"thumbnailSmall"
|
|
56874
57082
|
];
|
|
56875
57083
|
/**
|
|
56876
|
-
* Pick
|
|
56877
|
-
* `
|
|
56878
|
-
* {@link CLEAN_MEDIA_KINDS}
|
|
56879
|
-
*
|
|
57084
|
+
* Pick media for a crop-forced request: the exact `preferKind` when present
|
|
57085
|
+
* (so `?kind=snapshot` still serves a snapshot), else the first available
|
|
57086
|
+
* subject crop in {@link CLEAN_MEDIA_KINDS}. Returns undefined when no crop
|
|
57087
|
+
* is in the list — the caller then falls through to any remaining image
|
|
57088
|
+
* (`resolveCropForcedMedia`); the class icon is only for a total miss.
|
|
56880
57089
|
*/
|
|
56881
57090
|
function pickCleanMedia(files, preferKind) {
|
|
56882
|
-
const
|
|
56883
|
-
if (
|
|
56884
|
-
const exact = files.find((f) => f.kind === preferKind);
|
|
56885
|
-
if (exact) return exact;
|
|
56886
|
-
}
|
|
57091
|
+
const exact = files.find((f) => f.kind === preferKind);
|
|
57092
|
+
if (exact) return exact;
|
|
56887
57093
|
for (const kind of CLEAN_MEDIA_KINDS) {
|
|
56888
57094
|
const found = files.find((f) => f.kind === kind);
|
|
56889
57095
|
if (found) return found;
|
|
@@ -57376,11 +57582,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
57376
57582
|
*/
|
|
57377
57583
|
packageStillness = new PackageStillnessTracker({ newId: () => randomUUID() });
|
|
57378
57584
|
/**
|
|
57379
|
-
* Frame-clock timestamp of the last NON-package track seen
|
|
57380
|
-
*
|
|
57381
|
-
* deliverer").
|
|
57382
|
-
*
|
|
57383
|
-
*
|
|
57585
|
+
* Frame-clock timestamp of the last NON-package track seen IN A PACKAGE
|
|
57586
|
+
* DROP-OFF ZONE per device — the activity mirror behind the package
|
|
57587
|
+
* activity gate ("a parcel needs a deliverer"). A walker at the far gate
|
|
57588
|
+
* does not stamp this. Deliberately not {@link lastTrackActivityMs}: that
|
|
57589
|
+
* map is stamped by ANY live track, including the package-class FP itself,
|
|
57590
|
+
* which would let the streetlight wedge vouch for its own delivery.
|
|
57384
57591
|
*/
|
|
57385
57592
|
lastNonPackageActivityAt = /* @__PURE__ */ new Map();
|
|
57386
57593
|
/** Last time the oversize-sighting drop was logged per device — one line
|
|
@@ -58346,6 +58553,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58346
58553
|
}, trackId);
|
|
58347
58554
|
},
|
|
58348
58555
|
markEmbeddedFace: (trackId) => stores.trackStore.markEmbeddedFace(trackId),
|
|
58556
|
+
copyCropToTrackPreview: async (input) => {
|
|
58557
|
+
await copyFaceCropIfTrackHasNoThumbnail({
|
|
58558
|
+
listTrackMedia: (trackId) => stores.mediaStore.listByOwner("track", trackId),
|
|
58559
|
+
putTrackThumbnail: (put) => stores.mediaStore.put({
|
|
58560
|
+
deviceId: put.deviceId,
|
|
58561
|
+
ownerKind: "track",
|
|
58562
|
+
ownerId: put.trackId,
|
|
58563
|
+
kind: "thumbnail",
|
|
58564
|
+
timestamp: put.timestamp,
|
|
58565
|
+
data: put.data
|
|
58566
|
+
})
|
|
58567
|
+
}, input);
|
|
58568
|
+
},
|
|
58349
58569
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
58350
58570
|
emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
|
|
58351
58571
|
onSemanticMatch: (match) => this.promoteSemanticRecognition({
|
|
@@ -58360,6 +58580,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58360
58580
|
confidence: match.confidence,
|
|
58361
58581
|
...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
|
|
58362
58582
|
}),
|
|
58583
|
+
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
58363
58584
|
logger: logger.child("FaceRecognizer")
|
|
58364
58585
|
});
|
|
58365
58586
|
this.faceRecognizer.refreshGallery();
|
|
@@ -58392,6 +58613,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58392
58613
|
deriveCropFromKeyFrame,
|
|
58393
58614
|
getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
|
|
58394
58615
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
|
|
58616
|
+
onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
|
|
58395
58617
|
logger: logger.child("PlateRecognizer")
|
|
58396
58618
|
});
|
|
58397
58619
|
this.plateRecognizer.refreshGallery();
|
|
@@ -58747,13 +58969,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58747
58969
|
buildSummaryPorts(stores) {
|
|
58748
58970
|
return {
|
|
58749
58971
|
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
58972
|
const windowTracks = await stores.trackStore.queryHistorical({
|
|
58758
58973
|
deviceId,
|
|
58759
58974
|
since: sinceMs,
|
|
@@ -58761,33 +58976,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
58761
58976
|
projection: "slim",
|
|
58762
58977
|
limit: Math.max(limit, 2e3)
|
|
58763
58978
|
});
|
|
58764
|
-
const trackById = new Map(windowTracks.map((t) => [t.trackId, t]));
|
|
58765
58979
|
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;
|
|
58980
|
+
return windowTracks.map((t) => ({
|
|
58981
|
+
trackId: t.trackId,
|
|
58982
|
+
deviceId,
|
|
58983
|
+
className: t.className,
|
|
58984
|
+
firstSeen: t.firstSeen,
|
|
58985
|
+
lastSeen: t.lastSeen,
|
|
58986
|
+
zones: [...t.zonesVisited],
|
|
58987
|
+
...t.importance !== void 0 ? { importance: t.importance } : {},
|
|
58988
|
+
...t.classes !== void 0 && t.classes.length > 0 ? { classes: t.classes } : {},
|
|
58989
|
+
...t.closedReason === "stationary-promotion" ? { stationary: true } : {},
|
|
58990
|
+
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
58991
|
+
}));
|
|
58791
58992
|
},
|
|
58792
58993
|
listMediaInfo: async (id) => {
|
|
58793
58994
|
const group = await stores.groupStore.get(id);
|
|
@@ -59782,6 +59983,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
59782
59983
|
refreshGallery: () => {
|
|
59783
59984
|
this.plateRecognizer?.refreshGallery();
|
|
59784
59985
|
},
|
|
59986
|
+
eventMediaBaseUrl: () => this.eventMediaBaseUrl,
|
|
59785
59987
|
emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
|
|
59786
59988
|
});
|
|
59787
59989
|
return [
|
|
@@ -60270,6 +60472,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60270
60472
|
deviceId,
|
|
60271
60473
|
tracked: result.tracked,
|
|
60272
60474
|
packageClasses,
|
|
60475
|
+
packageZoneIds: collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []),
|
|
60273
60476
|
settings: packageSettings,
|
|
60274
60477
|
frameWidth: result.frameWidth,
|
|
60275
60478
|
frameHeight: result.frameHeight,
|
|
@@ -60837,7 +61040,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60837
61040
|
if (!first) return;
|
|
60838
61041
|
const lifeMs = track.lastSeen - track.firstSeen;
|
|
60839
61042
|
const moved = netDisplacementPx(track.positions ?? []);
|
|
60840
|
-
if (!isShortMotionlessTrack(lifeMs, moved)) {
|
|
61043
|
+
if (!isShortMotionlessTrack(lifeMs, moved, track.className)) {
|
|
60841
61044
|
this.phantomCells?.noteMovingClose({
|
|
60842
61045
|
deviceId: track.deviceId,
|
|
60843
61046
|
className: track.className,
|
|
@@ -60955,6 +61158,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60955
61158
|
}, d.score, d.label, frame.timestamp);
|
|
60956
61159
|
if (this.plateRecognizer) {
|
|
60957
61160
|
const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
|
|
61161
|
+
const body = this.residents.bestSeenBbox(trackId);
|
|
60958
61162
|
await this.plateRecognizer.observePlateRead({
|
|
60959
61163
|
deviceId,
|
|
60960
61164
|
trackId,
|
|
@@ -60970,7 +61174,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
60970
61174
|
frameWidth: frame.frameWidth,
|
|
60971
61175
|
frameHeight: frame.frameHeight,
|
|
60972
61176
|
cropPadding: mediaSettings.cropPadding,
|
|
60973
|
-
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
|
|
61177
|
+
...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {},
|
|
61178
|
+
...body !== void 0 ? { bodyBbox: {
|
|
61179
|
+
x: body.x,
|
|
61180
|
+
y: body.y,
|
|
61181
|
+
w: body.w,
|
|
61182
|
+
h: body.h
|
|
61183
|
+
} } : {}
|
|
60974
61184
|
});
|
|
60975
61185
|
}
|
|
60976
61186
|
}
|
|
@@ -61271,6 +61481,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61271
61481
|
}
|
|
61272
61482
|
}).catch(() => {})));
|
|
61273
61483
|
}
|
|
61484
|
+
/**
|
|
61485
|
+
* A face or plate crop just landed from a still-live handle. Fill the
|
|
61486
|
+
* missing parent rungs — person/vehicle thumbnail (display crop, same
|
|
61487
|
+
* surface that produced the detail) and native keyFrame — so Events never
|
|
61488
|
+
* has a child crop without a body and a scene.
|
|
61489
|
+
*/
|
|
61490
|
+
requestTrackHierarchy(input) {
|
|
61491
|
+
ensureTrackHierarchyMedia({
|
|
61492
|
+
isThumbnailLanded: (trackId) => this.residents.isThumbnailLanded(trackId),
|
|
61493
|
+
isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
|
|
61494
|
+
captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
|
|
61495
|
+
captureKeyFrame: async (req) => {
|
|
61496
|
+
await this.persistKeyFrames(req.deviceId, req.timestamp, [{
|
|
61497
|
+
trackId: req.trackId,
|
|
61498
|
+
retry: false
|
|
61499
|
+
}], req.frameHandle);
|
|
61500
|
+
}
|
|
61501
|
+
}, input);
|
|
61502
|
+
}
|
|
61503
|
+
async captureHierarchyBodyThumbnail(input) {
|
|
61504
|
+
const displayCrop = this.captureDisplayCropFn;
|
|
61505
|
+
const mediaStore = this.mediaStore;
|
|
61506
|
+
if (!displayCrop || !mediaStore) return;
|
|
61507
|
+
await this.captureScheduler.request({
|
|
61508
|
+
deviceId: input.deviceId,
|
|
61509
|
+
trackId: input.trackId,
|
|
61510
|
+
kind: "thumbnail",
|
|
61511
|
+
coalesceKey: captureCoalesceKey(input.deviceId, "thumbnail", `${input.trackId}:hierarchy`),
|
|
61512
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
61513
|
+
exec: async () => {
|
|
61514
|
+
if (this.residents.isThumbnailLanded(input.trackId)) return;
|
|
61515
|
+
const jpeg = await displayCrop(input.frameHandle, input.bodyBbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId);
|
|
61516
|
+
if (!jpeg) return;
|
|
61517
|
+
await mediaStore.put({
|
|
61518
|
+
deviceId: input.deviceId,
|
|
61519
|
+
ownerKind: "track",
|
|
61520
|
+
ownerId: input.trackId,
|
|
61521
|
+
kind: "thumbnail",
|
|
61522
|
+
timestamp: input.timestamp,
|
|
61523
|
+
data: jpeg
|
|
61524
|
+
});
|
|
61525
|
+
this.residents.markThumbnailLanded(input.deviceId, input.trackId);
|
|
61526
|
+
}
|
|
61527
|
+
}).catch(() => {});
|
|
61528
|
+
}
|
|
61274
61529
|
/** Attribution-time SEMANTIC best-shot promotion (D-semantic-bestshot,
|
|
61275
61530
|
* 2026-08-21; see `pipeline/semantic-promotion.ts`): offer a frame from
|
|
61276
61531
|
* which a recognition SUCCEEDED to the best-frame ranking with the
|
|
@@ -61984,25 +62239,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
61984
62239
|
}
|
|
61985
62240
|
/** Hub-only: assign this sighting in RAM. First activity on a camera
|
|
61986
62241
|
* 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
|
-
}
|
|
62242
|
+
enqueueLiveGroup(track, now) {}
|
|
62006
62243
|
/** Unified retention (Phase 1): earliest retained footage per device — the
|
|
62007
62244
|
* follow-recordings cutoff source. Cached 10 min: availability answers in
|
|
62008
62245
|
* ~70 ms but the sweep asks once per device per pass. `null` = no footage
|
|
@@ -62448,7 +62685,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62448
62685
|
runPackageStillness(input) {
|
|
62449
62686
|
if (this.packageDropDetector === null) return;
|
|
62450
62687
|
const tags = { deviceId: input.deviceId };
|
|
62451
|
-
for (const t of input.tracked)
|
|
62688
|
+
for (const t of input.tracked) {
|
|
62689
|
+
if (input.packageClasses.has(t.className)) continue;
|
|
62690
|
+
if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
|
|
62452
62691
|
this.lastNonPackageActivityAt.set(input.deviceId, input.timestamp);
|
|
62453
62692
|
break;
|
|
62454
62693
|
}
|
|
@@ -62495,6 +62734,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62495
62734
|
frameWidth: input.frameWidth,
|
|
62496
62735
|
frameHeight: input.frameHeight,
|
|
62497
62736
|
timestamp: input.timestamp,
|
|
62737
|
+
minHits: input.settings.packageDropMinFrames,
|
|
62498
62738
|
minDwellMs: input.settings.packageDropDwellSec * 1e3,
|
|
62499
62739
|
activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
|
|
62500
62740
|
...lastActivityAt !== void 0 ? { lastActivityAt } : {}
|
|
@@ -62598,9 +62838,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62598
62838
|
for (const ex of outcome.expired) {
|
|
62599
62839
|
const observedDwellMs = ex.lastSeenAt - ex.firstSeenAt;
|
|
62600
62840
|
const requiredDwellMs = input.settings.packageDropDwellSec * 1e3;
|
|
62841
|
+
const requiredHits = input.settings.packageDropMinFrames;
|
|
62601
62842
|
const reasons = [];
|
|
62602
62843
|
if (!ex.activityBackedAtOpen) reasons.push("no-activity-before-open");
|
|
62603
|
-
if (ex.
|
|
62844
|
+
if (ex.hits < requiredHits) reasons.push("hits");
|
|
62604
62845
|
if (observedDwellMs < requiredDwellMs) reasons.push("dwell");
|
|
62605
62846
|
this.ctx.logger.info("package candidate expired without promotion", {
|
|
62606
62847
|
tags,
|
|
@@ -62608,8 +62849,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62608
62849
|
candidateId: ex.id,
|
|
62609
62850
|
className: ex.className,
|
|
62610
62851
|
bbox: ex.bbox,
|
|
62611
|
-
|
|
62612
|
-
|
|
62852
|
+
hits: ex.hits,
|
|
62853
|
+
minHits: requiredHits,
|
|
62613
62854
|
observedDwellMs,
|
|
62614
62855
|
requiredDwellMs,
|
|
62615
62856
|
activityBackedAtOpen: ex.activityBackedAtOpen,
|
|
@@ -62790,15 +63031,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62790
63031
|
* applies to the very next frame even when frames stop briefly
|
|
62791
63032
|
* (e.g. during binding flips).
|
|
62792
63033
|
*
|
|
62793
|
-
* RECONCILE: the push channel behind `subscribe`
|
|
62794
|
-
* via `
|
|
62795
|
-
*
|
|
62796
|
-
*
|
|
62797
|
-
*
|
|
62798
|
-
* telemetry (D8); the durable channel is RPC + reconcile — so each
|
|
62799
|
-
* proxy also refreshes its two slices on a slow timer. `refresh()`
|
|
62800
|
-
* round-trips `deviceState.getCapSlice` and fans out through the SAME
|
|
62801
|
-
* subscribe callbacks above, so a zone edit lands within one interval.
|
|
63034
|
+
* RECONCILE: the push channel behind `subscribe` is `device.state-changed`
|
|
63035
|
+
* via the addon's `ctx.eventBus` (`createEventBusSliceSource`). That
|
|
63036
|
+
* declares UDS interest so the parent actually fans the slice here. Events
|
|
63037
|
+
* remain lossy (D8); `refresh()` round-trips `deviceState.getCapSlice` on
|
|
63038
|
+
* this timer as the backstop.
|
|
62802
63039
|
*/
|
|
62803
63040
|
async ensureProxy(deviceId) {
|
|
62804
63041
|
const cached = this.proxies.get(deviceId);
|
|
@@ -62871,6 +63108,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
62871
63108
|
async listRecentTracks(input) {
|
|
62872
63109
|
return this.queryFacade.listRecentTracks(input);
|
|
62873
63110
|
}
|
|
63111
|
+
async listGroups(input) {
|
|
63112
|
+
return {
|
|
63113
|
+
groups: [],
|
|
63114
|
+
nextCursor: null
|
|
63115
|
+
};
|
|
63116
|
+
}
|
|
63117
|
+
async getGroup(input) {
|
|
63118
|
+
return null;
|
|
63119
|
+
}
|
|
62874
63120
|
/**
|
|
62875
63121
|
* Every event kind the device can produce: built-ins (motion + audio),
|
|
62876
63122
|
* detection classes actually observed on the device, and sensor kinds
|
|
@@ -64015,33 +64261,38 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
64015
64261
|
buildFaceRescoreActionHandlers() {
|
|
64016
64262
|
return { "face.rescoreTrack": (input) => this.rescoreFaceForTrack(input) };
|
|
64017
64263
|
}
|
|
64018
|
-
|
|
64019
|
-
const
|
|
64020
|
-
|
|
64021
|
-
|
|
64022
|
-
|
|
64023
|
-
|
|
64024
|
-
|
|
64025
|
-
|
|
64026
|
-
|
|
64027
|
-
|
|
64028
|
-
|
|
64029
|
-
|
|
64030
|
-
|
|
64264
|
+
groupQueryPorts() {
|
|
64265
|
+
const groupStore = this.groupStore;
|
|
64266
|
+
if (groupStore === null) return null;
|
|
64267
|
+
return {
|
|
64268
|
+
groupStore,
|
|
64269
|
+
loadTracks: async (trackIds) => {
|
|
64270
|
+
const map = /* @__PURE__ */ new Map();
|
|
64271
|
+
const tracks = this.trackStore;
|
|
64272
|
+
if (!tracks || trackIds.length === 0) return map;
|
|
64273
|
+
const missing = [];
|
|
64274
|
+
for (const id of trackIds) {
|
|
64275
|
+
const active = tracks.getActiveByTrack(id);
|
|
64276
|
+
if (active !== null) map.set(id, trackToLite(active));
|
|
64277
|
+
else missing.push(id);
|
|
64278
|
+
}
|
|
64279
|
+
if (missing.length === 0) return map;
|
|
64280
|
+
for (const persisted of await tracks.getPersistedByTrackIds(missing)) map.set(persisted.trackId, trackToLite(persisted));
|
|
64281
|
+
return map;
|
|
64282
|
+
},
|
|
64283
|
+
eventMediaBaseUrl: () => this.eventMediaBaseUrl
|
|
64031
64284
|
};
|
|
64285
|
+
}
|
|
64286
|
+
buildGroupActionHandlers() {
|
|
64032
64287
|
return {
|
|
64033
64288
|
"analytics.listGroups": async (input) => {
|
|
64034
|
-
|
|
64035
|
-
if (ready === null) return {
|
|
64289
|
+
return {
|
|
64036
64290
|
groups: [],
|
|
64037
64291
|
hasMore: false
|
|
64038
64292
|
};
|
|
64039
|
-
return listGroupRecords(ready, input);
|
|
64040
64293
|
},
|
|
64041
64294
|
"analytics.getGroup": async (input) => {
|
|
64042
|
-
|
|
64043
|
-
if (ready === null) return { group: null };
|
|
64044
|
-
return { group: await getGroupDetail(ready, input.groupId) };
|
|
64295
|
+
return { group: null };
|
|
64045
64296
|
}
|
|
64046
64297
|
};
|
|
64047
64298
|
}
|
|
@@ -64715,14 +64966,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
64715
64966
|
async readEventThumbnail(id, preferKind) {
|
|
64716
64967
|
const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
|
|
64717
64968
|
if (preferKind !== void 0 && preferKind.length > 0) {
|
|
64718
|
-
const
|
|
64719
|
-
|
|
64720
|
-
|
|
64721
|
-
|
|
64722
|
-
|
|
64723
|
-
...trackFiles,
|
|
64724
|
-
...owningTrackFiles
|
|
64725
|
-
], (files) => pickCleanMedia(files, preferKind));
|
|
64969
|
+
const layers = await collectCropForcedMedia({
|
|
64970
|
+
listByOwner: (ownerKind, ownerId) => this.mediaStore?.listByOwner(ownerKind, ownerId) ?? Promise.resolve([]),
|
|
64971
|
+
getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
|
|
64972
|
+
}, id);
|
|
64973
|
+
const chosen = resolveCropForcedMedia(layers.bodyFiles, (files) => pickCleanMedia(files, preferKind), layers.detailFiles);
|
|
64726
64974
|
if (!chosen) return null;
|
|
64727
64975
|
return {
|
|
64728
64976
|
bytes: Buffer.from(chosen.base64, "base64"),
|