@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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Dk-BdVHw.js");
5
+ const require_dist = require("../dist-CO6pe7bi.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -25924,7 +25924,7 @@ var CLASS_RANK_VEHICLE = .8;
25924
25924
  var CLASS_RANK_ANIMAL = .5;
25925
25925
  var CLASS_RANK_DEFAULT = .25;
25926
25926
  var PERSON_CLASSES = new Set(["person", "face"]);
25927
- var VEHICLE_CLASSES = new Set([
25927
+ var VEHICLE_CLASSES$1 = new Set([
25928
25928
  "vehicle",
25929
25929
  "car",
25930
25930
  "truck",
@@ -25955,7 +25955,7 @@ function clamp01$1(x) {
25955
25955
  function classRank(className) {
25956
25956
  const c = className.toLowerCase();
25957
25957
  if (PERSON_CLASSES.has(c)) return 1;
25958
- if (VEHICLE_CLASSES.has(c)) return CLASS_RANK_VEHICLE;
25958
+ if (VEHICLE_CLASSES$1.has(c)) return CLASS_RANK_VEHICLE;
25959
25959
  if (ANIMAL_CLASSES.has(c)) return CLASS_RANK_ANIMAL;
25960
25960
  return CLASS_RANK_DEFAULT;
25961
25961
  }
@@ -26558,7 +26558,7 @@ var FaceGalleryProvider = class {
26558
26558
  const identities = await this.identityStore.listIdentities();
26559
26559
  const nameMap = new Map(identities.map((i) => [i.id, i.name]));
26560
26560
  const identityName = face.recognizedIdentityId ? nameMap.get(face.recognizedIdentityId) : void 0;
26561
- const base64 = await this.faceCropBase64(face);
26561
+ const cropUrl = this.cropUrlFor(face);
26562
26562
  return {
26563
26563
  faceId: face.id,
26564
26564
  deviceId: face.deviceId,
@@ -26567,7 +26567,7 @@ var FaceGalleryProvider = class {
26567
26567
  ...face.recognizedIdentityId != null ? { recognizedIdentityId: face.recognizedIdentityId } : {},
26568
26568
  ...identityName !== void 0 ? { identityName } : {},
26569
26569
  assigned: face.assigned,
26570
- ...base64 !== void 0 ? { base64 } : {},
26570
+ ...cropUrl !== void 0 ? { cropUrl } : {},
26571
26571
  ...face.faceBbox !== void 0 ? { faceBbox: face.faceBbox } : {},
26572
26572
  ...face.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: face.keyFrameMediaKey } : {},
26573
26573
  ...face.bestMatchScore != null ? { bestMatchScore: face.bestMatchScore } : {},
@@ -29687,9 +29687,15 @@ var TrackCloser = class {
29687
29687
  await this.suppressZeroMedia(t.deviceId, t.trackId);
29688
29688
  return;
29689
29689
  }
29690
- if (outcome === "raster-fallback" && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
29690
+ let thumbnailLanded = ownedMedia.some((m) => m.kind === "thumbnail");
29691
+ if (outcome === "raster-fallback" && closure?.rasterFallback) {
29692
+ await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
29693
+ thumbnailLanded = true;
29694
+ }
29691
29695
  await this.maybePromoteLastFrame(t, ownedMedia);
29692
- await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia);
29696
+ const hasRasterFallback = closure?.rasterFallback !== void 0;
29697
+ if (!thumbnailLanded) thumbnailLanded = await this.maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback);
29698
+ if (!thumbnailLanded && closure?.rasterFallback) await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
29693
29699
  this.deps.logger.info("track ended", {
29694
29700
  tags: { deviceId: t.deviceId },
29695
29701
  meta: {
@@ -29807,14 +29813,23 @@ var TrackCloser = class {
29807
29813
  * and the rate could only ever be counted from the database, after the fact.
29808
29814
  * A branch that drops work silently reads as "never happened".
29809
29815
  */
29810
- async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia) {
29811
- if (ownedMedia.some((m) => m.kind === "thumbnail")) return;
29816
+ async maybeDeriveThumbnailFromKeyFrame(t, ownedMedia, hasRasterFallback) {
29817
+ if (ownedMedia.some((m) => m.kind === "thumbnail")) return true;
29812
29818
  const derive = this.deps.deriveThumbnailFromKeyFrame;
29813
- if (!derive) return this.logNoThumbnailAtClose(t, "derive-not-wired");
29819
+ if (!derive) {
29820
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-not-wired");
29821
+ return false;
29822
+ }
29814
29823
  const keyFrame = ownedMedia.find((m) => m.kind === "keyFrame");
29815
- if (!keyFrame) return this.logNoThumbnailAtClose(t, "no-key-frame");
29824
+ if (!keyFrame) {
29825
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-key-frame");
29826
+ return false;
29827
+ }
29816
29828
  const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
29817
- if (!bestSeen) return this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
29829
+ if (!bestSeen) {
29830
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-best-seen-bbox");
29831
+ return false;
29832
+ }
29818
29833
  try {
29819
29834
  const derived = await derive({
29820
29835
  mediaKey: keyFrame.key,
@@ -29826,17 +29841,23 @@ var TrackCloser = class {
29826
29841
  },
29827
29842
  frameWidth: bestSeen.frameWidth,
29828
29843
  frameHeight: bestSeen.frameHeight,
29829
- timestamp: bestSeen.timestamp
29844
+ timestamp: keyFrame.timestamp
29830
29845
  });
29831
- if (!derived) return this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
29846
+ if (!derived) {
29847
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-returned-null", { keyFrameKey: keyFrame.key });
29848
+ return false;
29849
+ }
29832
29850
  const mediaStore = this.deps.mediaStore();
29833
- if (!mediaStore) return this.logNoThumbnailAtClose(t, "no-media-store");
29851
+ if (!mediaStore) {
29852
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "no-media-store");
29853
+ return false;
29854
+ }
29834
29855
  await mediaStore.put({
29835
29856
  deviceId: t.deviceId,
29836
29857
  ownerKind: "track",
29837
29858
  ownerId: t.trackId,
29838
29859
  kind: "thumbnail",
29839
- timestamp: bestSeen.timestamp,
29860
+ timestamp: keyFrame.timestamp,
29840
29861
  data: derived.thumbnail
29841
29862
  });
29842
29863
  await mediaStore.put({
@@ -29844,7 +29865,7 @@ var TrackCloser = class {
29844
29865
  ownerKind: "track",
29845
29866
  ownerId: t.trackId,
29846
29867
  kind: "thumbnailSmall",
29847
- timestamp: bestSeen.timestamp,
29868
+ timestamp: keyFrame.timestamp,
29848
29869
  data: derived.thumbnailSmall
29849
29870
  });
29850
29871
  this.deps.logger.info("thumbnail derived from keyFrame at close (no live best-shot landed)", {
@@ -29855,6 +29876,7 @@ var TrackCloser = class {
29855
29876
  skewMs: derived.skewMs
29856
29877
  }
29857
29878
  });
29879
+ return true;
29858
29880
  } catch (err) {
29859
29881
  this.deps.logger.debug("close-time keyFrame thumbnail derive failed", {
29860
29882
  tags: { deviceId: t.deviceId },
@@ -29863,10 +29885,11 @@ var TrackCloser = class {
29863
29885
  error: String(err)
29864
29886
  }
29865
29887
  });
29866
- this.logNoThumbnailAtClose(t, "derive-threw", {
29888
+ if (!hasRasterFallback) this.logNoThumbnailAtClose(t, "derive-threw", {
29867
29889
  keyFrameKey: keyFrame.key,
29868
29890
  error: String(err)
29869
29891
  });
29892
+ return false;
29870
29893
  }
29871
29894
  }
29872
29895
  /**
@@ -30736,6 +30759,7 @@ var PlateGalleryProvider = class {
30736
30759
  logger;
30737
30760
  refreshGallery;
30738
30761
  emitPlateGalleryChanged;
30762
+ eventMediaBaseUrl;
30739
30763
  constructor(deps) {
30740
30764
  this.plateStore = deps.plateStore;
30741
30765
  this.vehicleStore = deps.vehicleStore;
@@ -30745,6 +30769,7 @@ var PlateGalleryProvider = class {
30745
30769
  this.logger = deps.logger;
30746
30770
  this.refreshGallery = deps.refreshGallery;
30747
30771
  this.emitPlateGalleryChanged = deps.emitPlateGalleryChanged;
30772
+ this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
30748
30773
  }
30749
30774
  /** Best-effort emit — never allowed to fail the mutation it follows. */
30750
30775
  safeEmitPlateGalleryChanged(payload) {
@@ -30752,6 +30777,12 @@ var PlateGalleryProvider = class {
30752
30777
  this.emitPlateGalleryChanged(payload);
30753
30778
  } catch {}
30754
30779
  }
30780
+ cropUrlFor(plate) {
30781
+ if (plate.mediaKey === void 0 || plate.mediaKey === "") return void 0;
30782
+ const base = this.eventMediaBaseUrl();
30783
+ if (base === null) return void 0;
30784
+ return `${base}/${encodeURIComponent(plate.mediaKey)}`;
30785
+ }
30755
30786
  async plateCropBase64(plate) {
30756
30787
  if (plate.mediaKey) {
30757
30788
  const m = await this.mediaStore.getByKey(plate.mediaKey);
@@ -30760,8 +30791,9 @@ var PlateGalleryProvider = class {
30760
30791
  const media = await this.mediaStore.listByOwner("plate", plate.id);
30761
30792
  return media.length > 0 ? media[0].base64 : void 0;
30762
30793
  }
30763
- async toPlateInfo(plate, nameMap) {
30764
- const base64 = await this.plateCropBase64(plate);
30794
+ async toPlateInfo(plate, nameMap, opts) {
30795
+ const base64 = opts?.includeCrops ?? true ? await this.plateCropBase64(plate) : void 0;
30796
+ const cropUrl = this.cropUrlFor(plate);
30765
30797
  const vehicleName = plate.recognizedVehicleId != null ? nameMap?.get(plate.recognizedVehicleId) : void 0;
30766
30798
  return {
30767
30799
  plateId: plate.id,
@@ -30776,7 +30808,8 @@ var PlateGalleryProvider = class {
30776
30808
  ...vehicleName !== void 0 ? { vehicleName } : {},
30777
30809
  ...plate.plateBbox !== void 0 ? { plateBbox: plate.plateBbox } : {},
30778
30810
  ...plate.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: plate.keyFrameMediaKey } : {},
30779
- ...base64 !== void 0 ? { base64 } : {}
30811
+ ...base64 !== void 0 ? { base64 } : {},
30812
+ ...cropUrl !== void 0 ? { cropUrl } : {}
30780
30813
  };
30781
30814
  }
30782
30815
  async vehicleNameMap() {
@@ -30795,7 +30828,7 @@ var PlateGalleryProvider = class {
30795
30828
  const plate = await this.plateStore.get(`plate-${input.trackId}`);
30796
30829
  if (!plate || plate.deviceId !== input.deviceId) return null;
30797
30830
  const nameMap = await this.vehicleNameMap();
30798
- return this.toPlateInfo(plate, nameMap);
30831
+ return this.toPlateInfo(plate, nameMap, { includeCrops: false });
30799
30832
  }
30800
30833
  async getPlateMedia(input) {
30801
30834
  return (await this.mediaStore.listByOwner("plate", input.plateId)).map((m) => ({
@@ -35936,6 +35969,19 @@ function createEventMediaHandler(deps) {
35936
35969
  };
35937
35970
  }
35938
35971
  //#endregion
35972
+ //#region src/pipeline-analytics/copy-face-crop-preview.ts
35973
+ var TRACK_PREVIEW_KINDS = new Set(["thumbnail", "thumbnailSmall"]);
35974
+ async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
35975
+ if ((await store.listTrackMedia(input.trackId)).some((m) => TRACK_PREVIEW_KINDS.has(m.kind))) return false;
35976
+ await store.putTrackThumbnail({
35977
+ deviceId: input.deviceId,
35978
+ trackId: input.trackId,
35979
+ timestamp: input.timestamp,
35980
+ data: input.crop
35981
+ });
35982
+ return true;
35983
+ }
35984
+ //#endregion
35939
35985
  //#region src/pipeline-analytics/event-thumbnail-resolver.ts
35940
35986
  /** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
35941
35987
  * `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
@@ -35944,29 +35990,47 @@ function createEventMediaHandler(deps) {
35944
35990
  function pickEventOwnedMedia(files) {
35945
35991
  return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
35946
35992
  }
35947
- /** Pick a track's fallback media in the shared cadence-preference order the
35948
- * KeyEvent path uses: `thumbnailSmall` (480 fast-load) → best `thumbnail` →
35949
- * rolling `lastFrame` → `firstFrame` → newest `snapshot` → any track blob.
35950
- * Undefined when the track owns no media. */
35993
+ /** Pick a track's fallback media when no subject crop exists: `thumbnailSmall`
35994
+ * → `thumbnail` → native `keyFrameSmall`/`keyFrame`/`fullFrame` → rolling
35995
+ * `lastFrame` → `firstFrame` → newest `snapshot` → any blob. A full frame
35996
+ * beats a class icon. Undefined when the owner has no media. */
35951
35997
  function pickTrackFallbackMedia(files) {
35952
- 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];
35998
+ 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];
35953
35999
  }
35954
36000
  /**
35955
- * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
35956
- * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
35957
- * frame at all — the regression class created by retiring `crop`/`fullFrame`
35958
- * production (2026-07-21), leaving degenerate tracks with only boxed
35959
- * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
35960
- * returning undefined. A boxed frame (with the target's box drawn) is a real
35961
- * preview; an empty tile is not. Clean is always tried FIRST so the common case
35962
- * still serves an unboxed tile. Undefined only when the owner has NO media.
36001
+ * Crop-forced resolution (the `?kind=crop` / `?kind=thumbnailSmall` reel path).
36002
+ *
36003
+ * Ladder: body crop (track/event thumbnail) → tight face/plate crop → any
36004
+ * remaining image (keyFrame / lastFrame / …). The class icon is only honest
36005
+ * when the owner has no stored bytes at all.
35963
36006
  *
35964
36007
  * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
35965
36008
  * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
35966
36009
  * composition pure + unit-testable here.
35967
36010
  */
35968
- function resolveCropForcedMedia(files, pickClean) {
35969
- return pickClean(files) ?? pickTrackFallbackMedia(files);
36011
+ function resolveCropForcedMedia(files, pickClean, detailFiles = []) {
36012
+ return pickClean(files) ?? pickClean(detailFiles) ?? pickTrackFallbackMedia([...files, ...detailFiles]);
36013
+ }
36014
+ /**
36015
+ * Event + track media (the body/scene) and face/plate media (the detail
36016
+ * crops). Face rows live under `face-${trackId}`; a track can close with
36017
+ * `hasEmbeddedFace` and ZERO track files (0e343f86).
36018
+ */
36019
+ async function collectCropForcedMedia(source, id) {
36020
+ const eventFiles = await source.listByOwner("event", id);
36021
+ const trackFiles = await source.listByOwner("track", id);
36022
+ const ownerTrackId = trackFiles.length === 0 ? await source.getTrackIdForEvent(id) : null;
36023
+ const owningTrackFiles = ownerTrackId === null ? [] : await source.listByOwner("track", ownerTrackId);
36024
+ const trackId = ownerTrackId ?? id;
36025
+ const [faceFiles, plateFiles] = await Promise.all([source.listByOwner("face", `face-${trackId}`), source.listByOwner("plate", `plate-${trackId}`)]);
36026
+ return {
36027
+ bodyFiles: [
36028
+ ...eventFiles,
36029
+ ...trackFiles,
36030
+ ...owningTrackFiles
36031
+ ],
36032
+ detailFiles: [...faceFiles, ...plateFiles]
36033
+ };
35970
36034
  }
35971
36035
  /**
35972
36036
  * Resolve the default-path media for an event id: the event's own media when it
@@ -35980,6 +36044,14 @@ async function resolveDefaultEventMedia(deps) {
35980
36044
  return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
35981
36045
  }
35982
36046
  //#endregion
36047
+ //#region src/pipeline-analytics/ensure-track-hierarchy.ts
36048
+ async function ensureTrackHierarchyMedia(deps, input) {
36049
+ const tasks = [];
36050
+ if (!deps.isThumbnailLanded(input.trackId)) tasks.push(deps.captureBodyThumbnail(input));
36051
+ if (!deps.isKeyFrameLanded(input.trackId)) tasks.push(deps.captureKeyFrame(input));
36052
+ await Promise.all(tasks);
36053
+ }
36054
+ //#endregion
35983
36055
  //#region src/pipeline-analytics/face-settings.ts
35984
36056
  /**
35985
36057
  * Per-device face-recognition settings. Cascade: a per-device override on top
@@ -36498,6 +36570,21 @@ var FaceRecognizer = class {
36498
36570
  ...held,
36499
36571
  crop
36500
36572
  });
36573
+ if (crop !== void 0 && input.frameHandle !== void 0) this.deps.onLiveHierarchy?.({
36574
+ deviceId: input.deviceId,
36575
+ trackId: c.trackId,
36576
+ timestamp: input.timestamp,
36577
+ bodyBbox: {
36578
+ x: c.bbox.x,
36579
+ y: c.bbox.y,
36580
+ w: c.bbox.w,
36581
+ h: c.bbox.h
36582
+ },
36583
+ frameWidth: input.frameWidth,
36584
+ frameHeight: input.frameHeight,
36585
+ cropPadding: input.cropPadding,
36586
+ frameHandle: input.frameHandle
36587
+ });
36501
36588
  this.deps.logger.debug("face: best-face held", {
36502
36589
  tags: {
36503
36590
  deviceId: input.deviceId,
@@ -36647,6 +36734,22 @@ var FaceRecognizer = class {
36647
36734
  });
36648
36735
  return;
36649
36736
  }
36737
+ try {
36738
+ await this.deps.copyCropToTrackPreview?.({
36739
+ deviceId,
36740
+ trackId,
36741
+ crop: held.crop,
36742
+ timestamp: held.timestamp
36743
+ });
36744
+ } catch (err) {
36745
+ this.deps.logger.debug("face: track preview copy failed", {
36746
+ tags: { deviceId },
36747
+ meta: {
36748
+ trackId,
36749
+ error: String(err)
36750
+ }
36751
+ });
36752
+ }
36650
36753
  const keyFrameMediaKey = this.deps.getKeyFrameMediaKey?.(trackId);
36651
36754
  try {
36652
36755
  await this.deps.faceStore.insert({
@@ -44227,6 +44330,12 @@ var PlateRecognizer = class {
44227
44330
  text: c.plateText,
44228
44331
  score: c.plateScore,
44229
44332
  bbox: c.plateBbox,
44333
+ bodyBbox: {
44334
+ x: c.bbox.x,
44335
+ y: c.bbox.y,
44336
+ w: c.bbox.w,
44337
+ h: c.bbox.h
44338
+ },
44230
44339
  timestamp: input.timestamp,
44231
44340
  frameWidth: input.frameWidth,
44232
44341
  frameHeight: input.frameHeight,
@@ -44300,6 +44409,16 @@ var PlateRecognizer = class {
44300
44409
  }
44301
44410
  });
44302
44411
  }
44412
+ if (crop !== void 0 && input.frameHandle !== void 0 && input.bodyBbox !== void 0) this.deps.onLiveHierarchy?.({
44413
+ deviceId: input.deviceId,
44414
+ trackId: input.trackId,
44415
+ timestamp: input.timestamp,
44416
+ bodyBbox: input.bodyBbox,
44417
+ frameWidth: input.frameWidth,
44418
+ frameHeight: input.frameHeight,
44419
+ cropPadding: input.cropPadding,
44420
+ frameHandle: input.frameHandle
44421
+ });
44303
44422
  this.bestPlate.set(input.trackId, {
44304
44423
  text: input.text,
44305
44424
  score: input.score,
@@ -48334,14 +48453,23 @@ function isAfterCursor(row, cursor) {
48334
48453
  if (row.lastSeen < cursor.lastSeen) return true;
48335
48454
  return row.lastSeen === cursor.lastSeen && row.id < cursor.trackId;
48336
48455
  }
48456
+ var VEHICLE_CLASSES = new Set([
48457
+ "vehicle",
48458
+ "car",
48459
+ "truck",
48460
+ "bus"
48461
+ ]);
48462
+ function motionlessMaxPxForClass(className) {
48463
+ return VEHICLE_CLASSES.has(className.toLowerCase()) ? 8 : 40;
48464
+ }
48337
48465
  function netDisplacementPx(positions) {
48338
48466
  const first = positions[0];
48339
48467
  const last = positions[positions.length - 1];
48340
48468
  if (!first || !last) return 0;
48341
48469
  return Math.hypot(last.x - first.x, last.y - first.y);
48342
48470
  }
48343
- function isShortMotionlessTrack(lifeMs, movedPx) {
48344
- return lifeMs <= 25e3 && movedPx <= 8;
48471
+ function isShortMotionlessTrack(lifeMs, movedPx, className) {
48472
+ return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
48345
48473
  }
48346
48474
  //#endregion
48347
48475
  //#region src/pipeline-analytics/store/track-store.ts
@@ -48925,7 +49053,7 @@ var TrackStore = class {
48925
49053
  for (const [trackId, t] of this.active) {
48926
49054
  if (nowMs - t.lastSeen < this.config.ttlMs) continue;
48927
49055
  t.active = false;
48928
- if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions))) t.closedReason = "stationary-promotion";
49056
+ if (t.positions.length > 0 && isShortMotionlessTrack(t.lastSeen - t.firstSeen, netDisplacementPx(t.positions), t.className)) t.closedReason = "stationary-promotion";
48929
49057
  const record = cloneTrack(t);
48930
49058
  try {
48931
49059
  await this.persistCompleted(record);
@@ -56931,10 +57059,12 @@ var CONFIRMATION_CROP_MAX_WIDTH = 320;
56931
57059
  * a late confirmation is still worth running; see `DEFAULT_CAPTURE_PRIORITY`). */
56932
57060
  var ENRICHMENT_CAPTURE_STALE_MS = 3e3;
56933
57061
  /** Reconcile cadence for the per-device `zones` / `zoneRules` slice handles.
56934
- * The `device.state-changed` push doesn't reliably reach a forked child, so
56935
- * each cached proxy re-pulls both slices on this timer (see ensureProxy) —
56936
- * a zone drawn in the editor shows up in per-zone stats within one tick. */
56937
- var ZONE_SLICE_RECONCILE_MS = 3e4;
57062
+ * The `device.state-changed` push reaches a forked child through
57063
+ * `createEventBusSliceSource` (the DeviceProxy no longer waits on
57064
+ * `live.onEvent`). Events are still lossy telemetry (D8); this timer
57065
+ * is the RPC reconcile, not the mechanism. Five minutes matches the
57066
+ * viewer's `RECONCILE_MS`. */
57067
+ var ZONE_SLICE_RECONCILE_MS = 3e5;
56938
57068
  /** How often the evaluating node re-reads the central scene set. */
56939
57069
  var SCENE_RELOAD_INTERVAL_MS = 3e4;
56940
57070
  /** How long the install coordinates are trusted between settings reads. */
@@ -56998,38 +57128,31 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
56998
57128
  *
56999
57129
  * `thumbnail` joined the clean set on 2026-07-17: the track best-thumbnail is
57000
57130
  * now a subject-centered crop (event-media-dispatcher), and for DEGENERATE
57001
- * tracks (no object event → no `crop`) it is the ONLY subject-centered media —
57002
- * without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
57003
- * which is how 65% of the timeline tiles rendered as whole scenes. It sits
57004
- * right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
57005
- *
57006
- * `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
57007
- * joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
57008
- * for tracks whose only clean media is the small key frame; it sits last
57009
- * (fullest-scene, but subject-centered enough to serve as a reasonable fallback
57010
- * over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
57011
- * keep resolving.
57131
+ * tracks (no object event → no `crop`) it is the ONLY subject-centered media.
57132
+ *
57133
+ * Full-scene kinds (`fullFrame`, `keyFrame`, `keyFrameSmall`) stay out of
57134
+ * this list so a crop is preferred when one exists. `resolveCropForcedMedia`
57135
+ * then falls through to those full-scene kinds — a keyFrame beats an icon.
57136
+ * An *explicit* `?kind=keyFrame` / `?kind=snapshot` still matches exactly, so
57137
+ * videoclips and other kind-specific fetches keep working.
57138
+ *
57139
+ * The retired-but-still-stored kind (`crop`) stays listed so old rows resolve.
57012
57140
  */
57013
57141
  var CLEAN_MEDIA_KINDS = [
57014
57142
  "crop",
57015
57143
  "thumbnail",
57016
- "thumbnailSmall",
57017
- "fullFrame",
57018
- "keyFrame",
57019
- "keyFrameSmall"
57144
+ "thumbnailSmall"
57020
57145
  ];
57021
57146
  /**
57022
- * Pick a CLEAN (never boxed) media file for a crop-forced request: the exact
57023
- * `preferKind` if it is itself clean and present, else the first available
57024
- * {@link CLEAN_MEDIA_KINDS} frame. Returns undefined when only boxed / no media
57025
- * exists (caller 404s → the viewer shows an icon).
57147
+ * Pick media for a crop-forced request: the exact `preferKind` when present
57148
+ * (so `?kind=snapshot` still serves a snapshot), else the first available
57149
+ * subject crop in {@link CLEAN_MEDIA_KINDS}. Returns undefined when no crop
57150
+ * is in the list — the caller then falls through to any remaining image
57151
+ * (`resolveCropForcedMedia`); the class icon is only for a total miss.
57026
57152
  */
57027
57153
  function pickCleanMedia(files, preferKind) {
57028
- const isClean = (k) => CLEAN_MEDIA_KINDS.includes(k);
57029
- if (isClean(preferKind)) {
57030
- const exact = files.find((f) => f.kind === preferKind);
57031
- if (exact) return exact;
57032
- }
57154
+ const exact = files.find((f) => f.kind === preferKind);
57155
+ if (exact) return exact;
57033
57156
  for (const kind of CLEAN_MEDIA_KINDS) {
57034
57157
  const found = files.find((f) => f.kind === kind);
57035
57158
  if (found) return found;
@@ -58493,6 +58616,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58493
58616
  }, trackId);
58494
58617
  },
58495
58618
  markEmbeddedFace: (trackId) => stores.trackStore.markEmbeddedFace(trackId),
58619
+ copyCropToTrackPreview: async (input) => {
58620
+ await copyFaceCropIfTrackHasNoThumbnail({
58621
+ listTrackMedia: (trackId) => stores.mediaStore.listByOwner("track", trackId),
58622
+ putTrackThumbnail: (put) => stores.mediaStore.put({
58623
+ deviceId: put.deviceId,
58624
+ ownerKind: "track",
58625
+ ownerId: put.trackId,
58626
+ kind: "thumbnail",
58627
+ timestamp: put.timestamp,
58628
+ data: put.data
58629
+ })
58630
+ }, input);
58631
+ },
58496
58632
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
58497
58633
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
58498
58634
  onSemanticMatch: (match) => this.promoteSemanticRecognition({
@@ -58507,6 +58643,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58507
58643
  confidence: match.confidence,
58508
58644
  ...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
58509
58645
  }),
58646
+ onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
58510
58647
  logger: logger.child("FaceRecognizer")
58511
58648
  });
58512
58649
  this.faceRecognizer.refreshGallery();
@@ -58539,6 +58676,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
58539
58676
  deriveCropFromKeyFrame,
58540
58677
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
58541
58678
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload),
58679
+ onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
58542
58680
  logger: logger.child("PlateRecognizer")
58543
58681
  });
58544
58682
  this.plateRecognizer.refreshGallery();
@@ -59908,6 +60046,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
59908
60046
  refreshGallery: () => {
59909
60047
  this.plateRecognizer?.refreshGallery();
59910
60048
  },
60049
+ eventMediaBaseUrl: () => this.eventMediaBaseUrl,
59911
60050
  emitPlateGalleryChanged: (payload) => this.emitPlateGalleryChanged(payload)
59912
60051
  });
59913
60052
  return [
@@ -60964,7 +61103,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60964
61103
  if (!first) return;
60965
61104
  const lifeMs = track.lastSeen - track.firstSeen;
60966
61105
  const moved = netDisplacementPx(track.positions ?? []);
60967
- if (!isShortMotionlessTrack(lifeMs, moved)) {
61106
+ if (!isShortMotionlessTrack(lifeMs, moved, track.className)) {
60968
61107
  this.phantomCells?.noteMovingClose({
60969
61108
  deviceId: track.deviceId,
60970
61109
  className: track.className,
@@ -61082,6 +61221,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
61082
61221
  }, d.score, d.label, frame.timestamp);
61083
61222
  if (this.plateRecognizer) {
61084
61223
  const mediaSettings = await this.resolveDeviceMediaSettings(deviceId);
61224
+ const body = this.residents.bestSeenBbox(trackId);
61085
61225
  await this.plateRecognizer.observePlateRead({
61086
61226
  deviceId,
61087
61227
  trackId,
@@ -61097,7 +61237,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
61097
61237
  frameWidth: frame.frameWidth,
61098
61238
  frameHeight: frame.frameHeight,
61099
61239
  cropPadding: mediaSettings.cropPadding,
61100
- ...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {}
61240
+ ...frame.frameHandle !== void 0 ? { frameHandle: frame.frameHandle } : {},
61241
+ ...body !== void 0 ? { bodyBbox: {
61242
+ x: body.x,
61243
+ y: body.y,
61244
+ w: body.w,
61245
+ h: body.h
61246
+ } } : {}
61101
61247
  });
61102
61248
  }
61103
61249
  }
@@ -61398,6 +61544,51 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
61398
61544
  }
61399
61545
  }).catch(() => {})));
61400
61546
  }
61547
+ /**
61548
+ * A face or plate crop just landed from a still-live handle. Fill the
61549
+ * missing parent rungs — person/vehicle thumbnail (display crop, same
61550
+ * surface that produced the detail) and native keyFrame — so Events never
61551
+ * has a child crop without a body and a scene.
61552
+ */
61553
+ requestTrackHierarchy(input) {
61554
+ ensureTrackHierarchyMedia({
61555
+ isThumbnailLanded: (trackId) => this.residents.isThumbnailLanded(trackId),
61556
+ isKeyFrameLanded: (trackId) => this.residents.keyFrameCapture(trackId).landed,
61557
+ captureBodyThumbnail: (req) => this.captureHierarchyBodyThumbnail(req),
61558
+ captureKeyFrame: async (req) => {
61559
+ await this.persistKeyFrames(req.deviceId, req.timestamp, [{
61560
+ trackId: req.trackId,
61561
+ retry: false
61562
+ }], req.frameHandle);
61563
+ }
61564
+ }, input);
61565
+ }
61566
+ async captureHierarchyBodyThumbnail(input) {
61567
+ const displayCrop = this.captureDisplayCropFn;
61568
+ const mediaStore = this.mediaStore;
61569
+ if (!displayCrop || !mediaStore) return;
61570
+ await this.captureScheduler.request({
61571
+ deviceId: input.deviceId,
61572
+ trackId: input.trackId,
61573
+ kind: "thumbnail",
61574
+ coalesceKey: captureCoalesceKey(input.deviceId, "thumbnail", `${input.trackId}:hierarchy`),
61575
+ staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
61576
+ exec: async () => {
61577
+ if (this.residents.isThumbnailLanded(input.trackId)) return;
61578
+ const jpeg = await displayCrop(input.frameHandle, input.bodyBbox, input.frameWidth, input.frameHeight, input.cropPadding, input.trackId);
61579
+ if (!jpeg) return;
61580
+ await mediaStore.put({
61581
+ deviceId: input.deviceId,
61582
+ ownerKind: "track",
61583
+ ownerId: input.trackId,
61584
+ kind: "thumbnail",
61585
+ timestamp: input.timestamp,
61586
+ data: jpeg
61587
+ });
61588
+ this.residents.markThumbnailLanded(input.deviceId, input.trackId);
61589
+ }
61590
+ }).catch(() => {});
61591
+ }
61401
61592
  /** Attribution-time SEMANTIC best-shot promotion (D-semantic-bestshot,
61402
61593
  * 2026-08-21; see `pipeline/semantic-promotion.ts`): offer a frame from
61403
61594
  * which a recognition SUCCEEDED to the best-frame ranking with the
@@ -62903,15 +63094,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62903
63094
  * applies to the very next frame even when frames stop briefly
62904
63095
  * (e.g. during binding flips).
62905
63096
  *
62906
- * RECONCILE: the push channel behind `subscribe` (`device.state-changed`
62907
- * via `live.onEvent`) does not reliably reach a forked addon child — a
62908
- * zone created AFTER the proxy's cold read stayed invisible until the
62909
- * addon respawned (live-diagnosed on device 617, 2026-07-16: zone slice
62910
- * populated hub-side, `zones: []` in every snapshot). Events are lossy
62911
- * telemetry (D8); the durable channel is RPC + reconcile — so each
62912
- * proxy also refreshes its two slices on a slow timer. `refresh()`
62913
- * round-trips `deviceState.getCapSlice` and fans out through the SAME
62914
- * subscribe callbacks above, so a zone edit lands within one interval.
63097
+ * RECONCILE: the push channel behind `subscribe` is `device.state-changed`
63098
+ * via the addon's `ctx.eventBus` (`createEventBusSliceSource`). That
63099
+ * declares UDS interest so the parent actually fans the slice here. Events
63100
+ * remain lossy (D8); `refresh()` round-trips `deviceState.getCapSlice` on
63101
+ * this timer as the backstop.
62915
63102
  */
62916
63103
  async ensureProxy(deviceId) {
62917
63104
  const cached = this.proxies.get(deviceId);
@@ -64842,14 +65029,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
64842
65029
  async readEventThumbnail(id, preferKind) {
64843
65030
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
64844
65031
  if (preferKind !== void 0 && preferKind.length > 0) {
64845
- const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
64846
- const ownerTrackId = trackFiles.length === 0 ? await (this.eventStore?.getTrackIdForEvent(id) ?? Promise.resolve(null)) : null;
64847
- const owningTrackFiles = ownerTrackId === null ? [] : await (this.mediaStore?.listByOwner("track", ownerTrackId) ?? Promise.resolve([]));
64848
- const chosen = resolveCropForcedMedia([
64849
- ...eventFiles,
64850
- ...trackFiles,
64851
- ...owningTrackFiles
64852
- ], (files) => pickCleanMedia(files, preferKind));
65032
+ const layers = await collectCropForcedMedia({
65033
+ listByOwner: (ownerKind, ownerId) => this.mediaStore?.listByOwner(ownerKind, ownerId) ?? Promise.resolve([]),
65034
+ getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
65035
+ }, id);
65036
+ const chosen = resolveCropForcedMedia(layers.bodyFiles, (files) => pickCleanMedia(files, preferKind), layers.detailFiles);
64853
65037
  if (!chosen) return null;
64854
65038
  return {
64855
65039
  bytes: Buffer.from(chosen.base64, "base64"),