@camstack/addon-post-analysis 1.2.117 → 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 customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-PMOtjzh2.mjs";
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";
@@ -25902,7 +25902,7 @@ var CLASS_RANK_VEHICLE = .8;
25902
25902
  var CLASS_RANK_ANIMAL = .5;
25903
25903
  var CLASS_RANK_DEFAULT = .25;
25904
25904
  var PERSON_CLASSES = new Set(["person", "face"]);
25905
- var VEHICLE_CLASSES = new Set([
25905
+ var VEHICLE_CLASSES$1 = new Set([
25906
25906
  "vehicle",
25907
25907
  "car",
25908
25908
  "truck",
@@ -25933,7 +25933,7 @@ function clamp01$1(x) {
25933
25933
  function classRank(className) {
25934
25934
  const c = className.toLowerCase();
25935
25935
  if (PERSON_CLASSES.has(c)) return 1;
25936
- if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
25936
+ if (VEHICLE_CLASSES$1.has(c)) return CLASS_RANK_VEHICLE;
25937
25937
  if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
25938
25938
  return CLASS_RANK_DEFAULT;
25939
25939
  }
@@ -26536,7 +26536,7 @@ var FaceGalleryProvider = class {
26536
26536
  const identities = await this.identityStore.listIdentities();
26537
26537
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
26538
26538
  const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
26539
- const base64 = await this.faceCropBase64(face);
26539
+ const cropUrl = this.cropUrlFor(face);
26540
26540
  return {
26541
26541
  faceId: face.id,
26542
26542
  deviceId: face.deviceId,
@@ -26545,7 +26545,7 @@ var FaceGalleryProvider = class {
26545
26545
  ...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
26546
26546
  ...identityName !== void 0 ? { identityName } : {},
26547
26547
  assigned: face.assigned,
26548
- ...base64 !== void 0 ? { base64 } : {},
26548
+ ...cropUrl !== void 0 ? { cropUrl } : {},
26549
26549
  ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
26550
26550
  ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
26551
26551
  ...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
@@ -29663,9 +29663,15 @@ var TrackCloser = class {
29663
29663
  await this.suppressZeroMedia(t.deviceId, t.trackId);
29664
29664
  return;
29665
29665
  }
29666
- 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
+ }
29667
29671
  await this.maybePromoteLastFrame(t, ownedMedia);
29668
- 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);
29669
29675
  this.deps.logger.info("track ended", {
29670
29676
  tags: { deviceId: t.deviceId },
29671
29677
  meta: {
@@ -29783,14 +29789,23 @@ var TrackCloser = class {
29783
29789
  * and the rate could only ever be counted from the database, after the fact.
29784
29790
  * A branch that drops work silently reads as "never happened".
29785
29791
  */
29786
- async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia) {
29787
- if (ownedMedia.some((m) => m.kind === "thumbnail")) return;
29792
+ async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback) {
29793
+ if (ownedMedia.some((m) => m.kind === "thumbnail")) return true;
29788
29794
  const derive = this.deps.deriveThumbnailFromKeyFrame;
29789
- 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
+ }
29790
29799
  const keyFrame = ownedMedia.find((m) => m.kind === "keyFrame");
29791
- 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
+ }
29792
29804
  const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
29793
- 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
+ }
29794
29809
  try {
29795
29810
  const derived = await derive({
29796
29811
  mediaKey: keyFrame.key,
@@ -29802,17 +29817,23 @@ var TrackCloser = class {
29802
29817
  },
29803
29818
  frameWidth: bestSeen.frameWidth,
29804
29819
  frameHeight: bestSeen.frameHeight,
29805
- timestamp: bestSeen.timestamp
29820
+ timestamp: keyFrame.timestamp
29806
29821
  });
29807
- 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
+ }
29808
29826
  const mediaStore = this.deps.mediaStore();
29809
- 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
+ }
29810
29831
  await mediaStore.put({
29811
29832
  deviceId: t.deviceId,
29812
29833
  ownerKind: "track",
29813
29834
  ownerId: t.trackId,
29814
29835
  kind: "thumbnail",
29815
- timestamp: bestSeen.timestamp,
29836
+ timestamp: keyFrame.timestamp,
29816
29837
  data: derived.thumbnail
29817
29838
  });
29818
29839
  await mediaStore.put({
@@ -29820,7 +29841,7 @@ var TrackCloser = class {
29820
29841
  ownerKind: "track",
29821
29842
  ownerId: t.trackId,
29822
29843
  kind: "thumbnailSmall",
29823
- timestamp: bestSeen.timestamp,
29844
+ timestamp: keyFrame.timestamp,
29824
29845
  data: derived.thumbnailSmall
29825
29846
  });
29826
29847
  this.deps.logger.info("thumbnail derived from keyFrame at close (no live best-shot landed)", {
@@ -29831,6 +29852,7 @@ var TrackCloser = class {
29831
29852
  skewMs: derived.skewMs
29832
29853
  }
29833
29854
  });
29855
+ return true;
29834
29856
  } catch (err) {
29835
29857
  this.deps.logger.debug("close-time keyFrame thumbnail derive failed", {
29836
29858
  tags: { deviceId: t.deviceId },
@@ -29839,10 +29861,11 @@ var TrackCloser = class {
29839
29861
  error: String(err)
29840
29862
  }
29841
29863
  });
29842
- this.logNoThumbnailAtClose(t, "derive-threw", {
29864
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-threw", {
29843
29865
  keyFrameKey: keyFrame.key,
29844
29866
  error: String(err)
29845
29867
  });
29868
+ return false;
29846
29869
  }
29847
29870
  }
29848
29871
  /**
@@ -30712,6 +30735,7 @@ var PlateGalleryProvider = class {
30712
30735
  logger;
30713
30736
  refreshGallery;
30714
30737
  emitPlateGalleryChanged;
30738
+ eventMediaBaseUrl;
30715
30739
  constructor(deps) {
30716
30740
  this.plateStore = deps.plateStore;
30717
30741
  this.vehicleStore = deps.vehicleStore;
@@ -30721,6 +30745,7 @@ var PlateGalleryProvider = class {
30721
30745
  this.logger = deps.logger;
30722
30746
  this.refreshGallery = deps.refreshGallery;
30723
30747
  this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
30748
+ this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
30724
30749
  }
30725
30750
  /** Best-effort emit — never allowed to fail the mutation it follows. */
30726
30751
  safeEmitPlateGalleryChanged(payload) {
@@ -30728,6 +30753,12 @@ var PlateGalleryProvider = class {
30728
30753
  this.emitPlateGalleryChanged(payload);
30729
30754
  } catch {}
30730
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
+ }
30731
30762
  async plateCropBase64(plate) {
30732
30763
  if (plate.mediaKey) {
30733
30764
  const m = await this.mediaStore.getByKey(plate.mediaKey);
@@ -30736,8 +30767,9 @@ var PlateGalleryProvider = class {
30736
30767
  const media = await this.mediaStore.listByOwner("plate", plate.id);
30737
30768
  return media.length > 0 ? media[0].base64 : void 0;
30738
30769
  }
30739
- async toPlateInfo(plate, nameMap) {
30740
- 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);
30741
30773
  const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
30742
30774
  return {
30743
30775
  plateId: plate.id,
@@ -30752,7 +30784,8 @@ var PlateGalleryProvider = class {
30752
30784
  ...vehicleName !== void 0 ? { vehicleName } : {},
30753
30785
  ...plate.plateBbox !== void 0 ? { plateBbox: plate.plateBbox } : {},
30754
30786
  ...plate.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: plate.keyFrameMediaKey } : {},
30755
- ...base64 !== void 0 ? { base64 } : {}
30787
+ ...base64 !== void 0 ? { base64 } : {},
30788
+ ...cropUrl !== void 0 ? { cropUrl } : {}
30756
30789
  };
30757
30790
  }
30758
30791
  async vehicleNameMap() {
@@ -30771,7 +30804,7 @@ var PlateGalleryProvider = class {
30771
30804
  const plate = await this.plateStore.get(`plate-${input.trackId}`);
30772
30805
  if (!plate || plate.deviceId !== input.deviceId) return null;
30773
30806
  const nameMap = await this.vehicleNameMap();
30774
- return this.toPlateInfo(plate, nameMap);
30807
+ return this.toPlateInfo(plate, nameMap, { includeCrops: false });
30775
30808
  }
30776
30809
  async getPlateMedia(input) {
30777
30810
  return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
@@ -35874,6 +35907,19 @@ function createEventMediaHandler(deps) {
35874
35907
  };
35875
35908
  }
35876
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
35877
35923
  //#region src/pipeline-analytics/event-thumbnail-resolver.ts
35878
35924
  /** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
35879
35925
  * `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
@@ -35882,29 +35928,47 @@ function createEventMediaHandler(deps) {
35882
35928
  function pickEventOwnedMedia(files) {
35883
35929
  return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
35884
35930
  }
35885
- /** Pick a track's fallback media in the shared cadence-preference order the
35886
- * KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
35887
- * rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
35888
- * 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. */
35889
35935
  function pickTrackFallbackMedia(files) {
35890
- 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];
35891
35937
  }
35892
35938
  /**
35893
- * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
35894
- * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
35895
- * frame at all — the regression class created by retiring `crop`/`fullFrame`
35896
- * production (2026-07-21), leaving degenerate tracks with only boxed
35897
- * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
35898
- * returning undefined. A boxed frame (with the target's box drawn) is a real
35899
- * preview; an empty tile is not. Clean is always tried FIRST so the common case
35900
- * 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.
35901
35944
  *
35902
35945
  * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
35903
35946
  * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
35904
35947
  * composition pure + unit-testable here.
35905
35948
  */
35906
- function resolveCropForcedMedia(files, pickClean) {
35907
- 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
+ };
35908
35972
  }
35909
35973
  /**
35910
35974
  * Resolve the default-path media for an event id: the event's own media when it
@@ -35918,6 +35982,14 @@ async function resolveDefaultEventMedia(deps) {
35918
35982
  return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
35919
35983
  }
35920
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
35921
35993
  //#region src/pipeline-analytics/face-settings.ts
35922
35994
  /**
35923
35995
  * Per-device face-recognition settings. Cascade: a per-device override on top
@@ -36436,6 +36508,21 @@ var FaceRecognizer = class {
36436
36508
  ...held,
36437
36509
  crop
36438
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
+ });
36439
36526
  this.deps.logger.debug("face: best-face held", {
36440
36527
  tags: {
36441
36528
  deviceId: input.deviceId,
@@ -36585,6 +36672,22 @@ var FaceRecognizer = class {
36585
36672
  });
36586
36673
  return;
36587
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
+ }
36588
36691
  const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
36589
36692
  try {
36590
36693
  await this.deps.faceStore.insert({
@@ -44164,6 +44267,12 @@ var PlateRecognizer = class {
44164
44267
  text: c.plateText,
44165
44268
  score: c.plateScore,
44166
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
+ },
44167
44276
  timestamp: input.timestamp,
44168
44277
  frameWidth: input.frameWidth,
44169
44278
  frameHeight: input.frameHeight,
@@ -44237,6 +44346,16 @@ var PlateRecognizer = class {
44237
44346
  }
44238
44347
  });
44239
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
+ });
44240
44359
  this.bestPlate.set(input.trackId, {
44241
44360
  text: input.text,
44242
44361
  score: input.score,
@@ -48271,14 +48390,23 @@ function isAfterCursor(row, cursor) {
48271
48390
  if (row.lastSeen < cursor.lastSeen) return true;
48272
48391
  return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
48273
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
+ }
48274
48402
  function netDisplacementPx(positions) {
48275
48403
  const first = positions[0];
48276
48404
  const last = positions[positions.length - 1];
48277
48405
  if (!first || !last) return 0;
48278
48406
  return Math.hypot(last.x - first.x, last.y - first.y);
48279
48407
  }
48280
- function isShortMotionlessTrack(lifeMs, movedPx) {
48281
- return lifeMs <= 25e3 && movedPx <= 8;
48408
+ function isShortMotionlessTrack(lifeMs, movedPx, className) {
48409
+ return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
48282
48410
  }
48283
48411
  //#endregion
48284
48412
  //#region src/pipeline-analytics/store/track-store.ts
@@ -48862,7 +48990,7 @@ var TrackStore = class {
48862
48990
  for (const [trackId, t] of this.active) {
48863
48991
  if (nowMs - t.lastSeen < this.config.ttlMs) continue;
48864
48992
  t.active = false;
48865
- 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";
48866
48994
  const record = cloneTrack(t);
48867
48995
  try {
48868
48996
  await this.persistCompleted(record);
@@ -56868,10 +56996,12 @@ var CONFIRMATION_CROP_MAX_WIDTH = 320;
56868
56996
  * a late confirmation is still worth running; see `DEFAULT_CAPTURE_PRIORITY`). */
56869
56997
  var ENRICHMENT_CAPTURE_STALE_MS = 3e3;
56870
56998
  /** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
56871
- * The `device.state-changed` push doesn't reliably reach a forked child, so
56872
- * each cached proxy re-pulls both slices on this timer (see ensureProxy) —
56873
- * a zone drawn in the editor shows up in per-zone stats within one tick. */
56874
- 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;
56875
57005
  /** How often the evaluating node re-reads the central scene set. */
56876
57006
  var SCENE_RELOAD_INTERVAL_MS = 3e4;
56877
57007
  /** How long the install coordinates are trusted between settings reads. */
@@ -56935,38 +57065,31 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
56935
57065
  *
56936
57066
  * `thumbnail` joined the clean set on 2026-07-17: the track best-thumbnail is
56937
57067
  * now a subject-centered crop (event-media-dispatcher), and for DEGENERATE
56938
- * tracks (no object event → no `crop`) it is the ONLY subject-centered media —
56939
- * without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
56940
- * which is how 65% of the timeline tiles rendered as whole scenes. It sits
56941
- * right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
56942
- *
56943
- * `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
56944
- * joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
56945
- * for tracks whose only clean media is the small key frame; it sits last
56946
- * (fullest-scene, but subject-centered enough to serve as a reasonable fallback
56947
- * over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
56948
- * 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.
56949
57077
  */
56950
57078
  var CLEAN_MEDIA_KINDS = [
56951
57079
  "crop",
56952
57080
  "thumbnail",
56953
- "thumbnailSmall",
56954
- "fullFrame",
56955
- "keyFrame",
56956
- "keyFrameSmall"
57081
+ "thumbnailSmall"
56957
57082
  ];
56958
57083
  /**
56959
- * Pick a CLEAN (never boxed) media file for a crop-forced request: the exact
56960
- * `preferKind` if it is itself clean and present, else the first available
56961
- * {@link CLEAN_MEDIA_KINDS} frame. Returns undefined when only boxed / no media
56962
- * 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.
56963
57089
  */
56964
57090
  function pickCleanMedia(files, preferKind) {
56965
- const isClean = (k) => CLEAN_MEDIA_KINDS.includes(k);
56966
- if (isClean(preferKind)) {
56967
- const exact = files.find((f) => f.kind === preferKind);
56968
- if (exact) return exact;
56969
- }
57091
+ const exact = files.find((f) => f.kind === preferKind);
57092
+ if (exact) return exact;
56970
57093
  for (const kind of CLEAN_MEDIA_KINDS) {
56971
57094
  const found = files.find((f) => f.kind === kind);
56972
57095
  if (found) return found;
@@ -58430,6 +58553,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
58430
58553
  }, trackId);
58431
58554
  },
58432
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
+ },
58433
58569
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
58434
58570
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
58435
58571
  onSemanticMatch: (match) => this.promoteSemanticRecognition({
@@ -58444,6 +58580,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
58444
58580
  confidence: match.confidence,
58445
58581
  ...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
58446
58582
  }),
58583
+ onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
58447
58584
  logger: logger.child("FaceRecognizer")
58448
58585
  });
58449
58586
  this.faceRecognizer.refreshGallery();
@@ -58476,6 +58613,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
58476
58613
  deriveCropFromKeyFrame,
58477
58614
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
58478
58615
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
58616
+ onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
58479
58617
  logger: logger.child("PlateRecognizer")
58480
58618
  });
58481
58619
  this.plateRecognizer.refreshGallery();
@@ -59845,6 +59983,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
59845
59983
  refreshGallery: () => {
59846
59984
  this.plateRecognizer?.refreshGallery();
59847
59985
  },
59986
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl,
59848
59987
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
59849
59988
  });
59850
59989
  return [
@@ -60901,7 +61040,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
60901
61040
  if (!first) return;
60902
61041
  const lifeMs = track.lastSeen - track.firstSeen;
60903
61042
  const moved = netDisplacementPx(track.positions ?? []);
60904
- if (!isShortMotionlessTrack(lifeMs, moved)) {
61043
+ if (!isShortMotionlessTrack(lifeMs, moved, track.className)) {
60905
61044
  this.phantomCells?.noteMovingClose({
60906
61045
  deviceId: track.deviceId,
60907
61046
  className: track.className,
@@ -61019,6 +61158,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
61019
61158
  }, d.score, d.label, frame.timestamp);
61020
61159
  if (this.plateRecognizer) {
61021
61160
  const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
61161
+ const body = this.residents.bestSeenBbox(trackId);
61022
61162
  await this.plateRecognizer.observePlateRead({
61023
61163
  deviceId,
61024
61164
  trackId,
@@ -61034,7 +61174,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
61034
61174
  frameWidth: frame.frameWidth,
61035
61175
  frameHeight: frame.frameHeight,
61036
61176
  cropPadding: mediaSettings.cropPadding,
61037
- ...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
+ } } : {}
61038
61184
  });
61039
61185
  }
61040
61186
  }
@@ -61335,6 +61481,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
61335
61481
  }
61336
61482
  }).catch(() => {})));
61337
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
+ }
61338
61529
  /** Attribution-time SEMANTIC best-shot promotion (D-semantic-bestshot,
61339
61530
  * 2026-08-21; see `pipeline/semantic-promotion.ts`): offer a frame from
61340
61531
  * which a recognition SUCCEEDED to the best-frame ranking with the
@@ -62840,15 +63031,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
62840
63031
  * applies to the very next frame even when frames stop briefly
62841
63032
  * (e.g. during binding flips).
62842
63033
  *
62843
- * RECONCILE: the push channel behind `subscribe` (`device.state-changed`
62844
- * via `live.onEvent`) does not reliably reach a forked addon child — a
62845
- * zone created AFTER the proxy's cold read stayed invisible until the
62846
- * addon respawned (live-diagnosed on device 617, 2026-07-16: zone slice
62847
- * populated hub-side, `zones: []` in every snapshot). Events are lossy
62848
- * telemetry (D8); the durable channel is RPC + reconcile — so each
62849
- * proxy also refreshes its two slices on a slow timer. `refresh()`
62850
- * round-trips `deviceState.getCapSlice` and fans out through the SAME
62851
- * 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.
62852
63039
  */
62853
63040
  async ensureProxy(deviceId) {
62854
63041
  const cached = this.proxies.get(deviceId);
@@ -64779,14 +64966,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
64779
64966
  async readEventThumbnail(id, preferKind) {
64780
64967
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
64781
64968
  if (preferKind !== void 0 && preferKind.length > 0) {
64782
- const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
64783
- const ownerTrackId = trackFiles.length === 0 ? await (this.eventStore?.getTrackIdForEvent(id) ?? Promise.resolve(null)) : null;
64784
- const owningTrackFiles = ownerTrackId === null ? [] : await (this.mediaStore?.listByOwner("track", ownerTrackId) ?? Promise.resolve([]));
64785
- const chosen = resolveCropForcedMedia([
64786
- ...eventFiles,
64787
- ...trackFiles,
64788
- ...owningTrackFiles
64789
- ], (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);
64790
64974
  if (!chosen) return null;
64791
64975
  return {
64792
64976
  bytes: Buffer.from(chosen.base64, "base64"),
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BmR_bLi_.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BYzwylnw.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }