@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.
@@ -1,4 +1,4 @@
1
- import { $ as deriveRecordingMode, At as array, B as TimelapseRulePatchSchema, Bt as EventCategory, C as NcScheduleSchema, Ct as DeviceType, D as NcSystemEventKindSchema, Dt as nodePin, E as NcSnoozeSuppressedSchema, Et as isDeviceScopedCap, F as SCENE_DEFAULT_UNCOVERED_POLICY, Ft as object, G as alarmPanelCapability, H as TrackSourceSchema, I as SCENE_DIVERGED, It as partialRecord, J as audioModeOf, K as assertTimelapseCadences, L as SceneMonitorSchema, Lt as record, M as RetrainStatusSchema, Mt as discriminatedUnion, Nt as literal, O as NcTaxonomySchema, Ot as sleep, P as SCENE_DEFAULT_ANCHOR_THRESHOLD, Pt as number, Q as defineCustomActions, R as TIMELAPSE_DENSE_FLOOR_SEC, Rt as string, S as NcRuleTargetSchema, St as BaseAddon, T as NcSnoozeSchema, Tt as hydrateSchema, U as VISIT_MERGE_GAP_MS, V as TimelapseRuleSchema, W as addonWidgetsSourceCapability, X as cosineSimilarity$1, Y as buildEventKindDescriptor, Z as customAction, _ as NC_TAXONOMY, _t as systemEventFilterApplies, a as DETECTION_MACRO_CLASSES, at as isScheduleActive, b as NcRulePatchSchema, bt as zoneAnalyticsCapability, c as EVENT_KIND_BY_CAP, d as LabelAttributionSchema, dt as plateGalleryCapability, f as MACRO_LABELS, ft as readDeviceStateFrom, gt as subKindsOf, h as NC_DEFAULT_SNOOZE_MINUTES, ht as sceneMonitorCapability, it as isDetectionMacroClass, j as RECORDING_EXPORT_MAX_READ_BYTES, jt as boolean, k as OpsLogEntrySchema, kt as _enum, l as EVENT_PAD_MS, lt as pickClusterStepModels, m as NC_CONDITION_CATALOG, n as CLUSTER_MODEL_SCOPED_STEPS, nt as faceGalleryCapability, ot as kebabToCamel, p as NC_ALARM_SYSTEM_EVENT_KINDS, pt as readTimelapseGeneratedAt, q as audioMetricsCapability, r as DEFAULT_EVENT_COLOR, s as DeclaredDevices, st as notificationRulesCapability, t as BaseDevice, tt as encodeVectorBase64, u as FULL_IMAGE_BBOX, ut as pipelineAnalyticsCapability, v as NcConditionDescriptorSchema, vt as vectorDimFromBase64, w as NcSnoozeInputSchema, wt as createEvent, x as NcRuleSchema, xt as errMsg, y as NcRuleInputSchema, yt as videoclipsCapability, z as TimelapseRuleInputSchema, zt as unknown } from "../dist-B13sFjR2.mjs";
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
- * For a group: `groupCrop` (union crop, the group's own blob) then
15146
- * `nativeFrame` (pointer at the anchor's clean keyFrame). A digest of a
15147
- * cohort may show the native still when the crop was not written; a
15148
- * member's `keyFrame` listed as itself stays off this ladder so tracker
15149
- * ID churn cannot refill a mosaic with eleven driveways. Boxed frames stay
15150
- * absent: a drawn rectangle is a permanent lie in a digest tile.
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
- "thumbnail",
15156
- "thumbnailSmall",
15157
- "snapshot"
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; `snapshot` is a
15164
- * filmstrip and its rows accumulate, so "the first one" would systematically
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 base64 = await this.faceCropBase64(face);
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
- ...base64 !== void 0 ? { base64 } : {},
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
- if (outcome === "raster-fallback" && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
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
- await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia);
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) return this.logNoThumbnailAtClose(t, "derive-not-wired");
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) return this.logNoThumbnailAtClose(t, "no-key-frame");
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) return this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
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: bestSeen.timestamp
29820
+ timestamp: keyFrame.timestamp
29804
29821
  });
29805
- if (!derived) return this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
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) return this.logNoThumbnailAtClose(t, "no-media-store");
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: bestSeen.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: bestSeen.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 in the shared cadence-preference order the
35884
- * KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
35885
- * rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
35886
- * Undefined when the track owns no media. */
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` reel/timeline path). Prefer a CLEAN
35892
- * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
35893
- * frame at all — the regression class created by retiring `crop`/`fullFrame`
35894
- * production (2026-07-21), leaving degenerate tracks with only boxed
35895
- * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
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). Dwell default is 15s per operator decision (the design's
39877
- * §3.3 draft said 45s).
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
- * Minimum OBSERVED dwell (seconds, since first-seen) before a newly
39899
- * promoted stationary package counts as a delivery. Kills a bag briefly
39900
- * set down and snatched back. Operator default 15s. Values beyond the
39901
- * stationary promotion window (30s) are satisfied only once the entry
39902
- * has actually been observed that long.
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(15),
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 on
39923
- * the device within this many seconds BEFORE the box first appeared — and a
39924
- * departure only counts as `picked-up` with somebody around the
39925
- * disappearance. 0 disables both gates. Device 615's streetlight wedge
39926
- * cycled delivered→picked-up ~23×/night with nobody in frame, ever.
39927
- * Default 300 s: wide enough to absorb the motion-settle governor's cadence
39928
- * (the box is often first SEEN minutes after the courier left).
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 subject crops. A multi with no crop still has a
40357
- * picture if the sealer stored `nativeKey`.
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 !== 3) return;
41099
- const union = {
41100
- x: instant.ux * raw.info.width,
41101
- y: instant.uy * raw.info.height,
41102
- w: instant.uw * raw.info.width,
41103
- h: instant.uh * raw.info.height
41104
- };
41105
- const crop = await cropGroupFromRgb(raw.data, raw.info.width, raw.info.height, union);
41106
- const mediaKey = await this.deps.mediaStore.put({
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: "thumbnail",
41173
+ kind: "fullFrameBoxed",
41111
41174
  timestamp,
41112
- data: crop
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: crop failed", {
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 Library Groups tab.
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 slice = (await this.store.query.query({
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
- ...input.deviceId !== void 0 ? { where: { deviceId: input.deviceId } } : {},
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
- }).slice(offset, offset + limit + 1);
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: slice.slice(0, limit),
41447
- hasMore: slice.length > limit
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
- minRounds: 2,
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, this.config.roundGapMs);
42813
- if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.rounds >= this.config.minRounds && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
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
- rounds: c.rounds,
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
- rounds: 1,
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. A new ROUND is only counted
42906
- * when the gap since the last sighting clears `roundGapMs`, so a burst of
42907
- * frames inside one governor round counts once. */
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
- rounds: newRound ? prev.rounds + 1 : prev.rounds,
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 <= 8;
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: "How long a package must stay parked before it counts as a delivery. Higher = fewer false positives from bags briefly set down.",
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 active on this camera within this many seconds before the package appeared — and a pick-up only with somebody around when it vanished. 0 disables the requirement.",
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 doesn't reliably reach a forked child, so
56789
- * each cached proxy re-pulls both slices on this timer (see ensureProxy) —
56790
- * a zone drawn in the editor shows up in per-zone stats within one tick. */
56791
- var ZONE_SLICE_RECONCILE_MS = 3e4;
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
- * without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
56857
- * which is how 65% of the timeline tiles rendered as whole scenes. It sits
56858
- * right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
56859
- *
56860
- * `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
56861
- * joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
56862
- * for tracks whose only clean media is the small key frame; it sits last
56863
- * (fullest-scene, but subject-centered enough to serve as a reasonable fallback
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 a CLEAN (never boxed) media file for a crop-forced request: the exact
56877
- * `preferKind` if it is itself clean and present, else the first available
56878
- * {@link CLEAN_MEDIA_KINDS} frame. Returns undefined when only boxed / no media
56879
- * exists (caller 404s → the viewer shows an icon).
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 isClean = (k) => CLEAN_MEDIA_KINDS.includes(k);
56883
- if (isClean(preferKind)) {
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 per device — the
57380
- * activity mirror behind the package activity gate ("a parcel needs a
57381
- * deliverer"). Deliberately not {@link lastTrackActivityMs}: that map is
57382
- * stamped by ANY live track, including the package-class FP itself, which
57383
- * would let the streetlight wedge vouch for its own delivery.
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
- const out = [];
58767
- for (const group of groups) {
58768
- const members = await stores.groupStore.listMembers(group.id);
58769
- const tracks = members.map((m) => trackById.get(m.trackId)).filter((t) => t !== void 0);
58770
- if (tracks.length === 0) continue;
58771
- const classes = [...new Set(tracks.flatMap((t) => [t.className, ...t.classes ?? []]))];
58772
- const zones = [...new Set(tracks.flatMap((t) => [...t.zonesVisited]))];
58773
- const importance = Math.max(0, ...tracks.map((t) => t.importance ?? 0));
58774
- const primary = tracks.reduce((best, t) => (t.importance ?? 0) > (best.importance ?? 0) ? t : best);
58775
- out.push({
58776
- trackId: group.id,
58777
- groupId: group.id,
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) if (!input.packageClasses.has(t.className)) {
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.rounds < 2) reasons.push("rounds");
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
- rounds: ex.rounds,
62612
- minRounds: 2,
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` (`device.state-changed`
62794
- * via `live.onEvent`) does not reliably reach a forked addon child — a
62795
- * zone created AFTER the proxy's cold read stayed invisible until the
62796
- * addon respawned (live-diagnosed on device 617, 2026-07-16: zone slice
62797
- * populated hub-side, `zones: []` in every snapshot). Events are lossy
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
- buildGroupActionHandlers() {
64019
- const ports = () => {
64020
- const groupStore = this.groupStore;
64021
- if (groupStore === null) return null;
64022
- return {
64023
- groupStore,
64024
- loadTrack: async (trackId) => {
64025
- const tracks = this.trackStore;
64026
- if (!tracks) return null;
64027
- return tracks.getActiveByTrack(trackId) ?? await tracks.getPersistedByTrackId(trackId);
64028
- },
64029
- eventMediaBaseUrl: () => this.eventMediaBaseUrl
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
- const ready = ports();
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
- const ready = ports();
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 trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
64719
- const ownerTrackId = trackFiles.length === 0 ? await (this.eventStore?.getTrackIdForEvent(id) ?? Promise.resolve(null)) : null;
64720
- const owningTrackFiles = ownerTrackId === null ? [] : await (this.mediaStore?.listByOwner("track", ownerTrackId) ?? Promise.resolve([]));
64721
- const chosen = resolveCropForcedMedia([
64722
- ...eventFiles,
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"),