@camstack/addon-post-analysis 1.1.22 → 1.1.24

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,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DbgDsnRE.js");
6
- const require_resolve_frame = require("../resolve-frame-nQMd6Z8B.js");
5
+ const require_dist = require("../dist-B0VgBdBN.js");
6
+ const require_resolve_frame = require("../resolve-frame-DBXwF5fk.js");
7
7
  let _camstack_shm_ring = require("@camstack/shm-ring");
8
8
  let sharp = require("sharp");
9
9
  sharp = require_dist.__toESM(sharp);
@@ -699,17 +699,24 @@ function normalizeToPixel(p, width, height) {
699
699
  //#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
700
700
  var DEFAULT_TRACKER_CONFIG = {
701
701
  iouThreshold: .3,
702
- maxMissedFrames: 30,
702
+ maxMissedMs: 4e3,
703
703
  minHits: 3,
704
704
  predictiveCoasting: true,
705
705
  classGating: true,
706
706
  occlusionEnabled: true,
707
707
  occlusionContainment: .6,
708
- occlusionMaxMissedFrames: 60,
709
- maxTrackLifetimeMs: 3e5
708
+ occlusionMaxMissedMs: 8e3,
709
+ maxTrackLifetimeMs: 3e5,
710
+ rescueIouThreshold: .1,
711
+ rescueCentroidFactor: .75,
712
+ resurrectionWindowMs: 8e3,
713
+ stationarySpeedPx: 2
710
714
  };
711
715
  var MAX_PATH_LENGTH = 300;
712
716
  var nextTrackId = 1;
717
+ function clamp(value, min, max) {
718
+ return Math.max(min, Math.min(max, value));
719
+ }
713
720
  function iou$1(a, b) {
714
721
  const ax1 = a.x, ay1 = a.y, ax2 = a.x + a.w, ay2 = a.y + a.h;
715
722
  const bx1 = b.x, by1 = b.y, bx2 = b.x + b.w, by2 = b.y + b.h;
@@ -739,26 +746,51 @@ var SortTracker = class {
739
746
  };
740
747
  }
741
748
  /** Where a track is expected this frame — extrapolated by velocity while
742
- * coasting (predictiveCoasting), else its last known bbox. */
749
+ * coasting (predictiveCoasting), else its last known bbox. A stationary
750
+ * track (speed < stationarySpeedPx) is frozen so bbox jitter can't walk the
751
+ * box off a sitting object; otherwise the drift is capped at one bbox size
752
+ * so a stale velocity can't fling the box across the frame. Public so the
753
+ * clamp is directly unit-testable. */
743
754
  predicted(t) {
744
755
  if (!this.config.predictiveCoasting) return t.bbox;
756
+ if (Math.hypot(t.velocity.dx, t.velocity.dy) < this.config.stationarySpeedPx) return t.bbox;
745
757
  const f = t.age + 1;
758
+ const driftX = clamp(t.velocity.dx * f, -t.bbox.w, t.bbox.w);
759
+ const driftY = clamp(t.velocity.dy * f, -t.bbox.h, t.bbox.h);
746
760
  return {
747
- x: t.bbox.x + t.velocity.dx * f,
748
- y: t.bbox.y + t.velocity.dy * f,
761
+ x: t.bbox.x + driftX,
762
+ y: t.bbox.y + driftY,
749
763
  w: t.bbox.w,
750
764
  h: t.bbox.h
751
765
  };
752
766
  }
767
+ /**
768
+ * Loose same-class gate used by the rescue and resurrection passes: accept
769
+ * when the detection overlaps the track's LAST-KNOWN bbox by `rescueIou`, or
770
+ * its centroid is within `rescueCentroidFactor × bbox-diagonal`. Always class-
771
+ * gated (a rescue must never cross classes), independent of `classGating`.
772
+ */
773
+ looseMatch(track, det) {
774
+ if (track.class !== det.class) return false;
775
+ if (iou$1(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return true;
776
+ const tc = bboxCentroid(track.bbox);
777
+ const dc = bboxCentroid(det.bbox);
778
+ const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
779
+ const diag = Math.hypot(track.bbox.w, track.bbox.h);
780
+ return dist <= this.config.rescueCentroidFactor * diag;
781
+ }
753
782
  update(detections, timestamp) {
754
783
  if (this.config.maxTrackLifetimeMs > 0) {
755
784
  const alive = [];
756
785
  for (const track of this.tracks) if (timestamp - track.firstSeen > this.config.maxTrackLifetimeMs) {
757
786
  track.lost = true;
787
+ track.lostAt = timestamp;
788
+ track.resurrectable = false;
758
789
  this.lostTracks.push(track);
759
790
  } else alive.push(track);
760
791
  this.tracks = alive;
761
792
  }
793
+ this.lostTracks = this.lostTracks.filter((t) => timestamp - t.lostAt <= this.config.resurrectionWindowMs);
762
794
  const used = /* @__PURE__ */ new Set();
763
795
  const matchedTracks = /* @__PURE__ */ new Set();
764
796
  const matched = /* @__PURE__ */ new Map();
@@ -783,6 +815,27 @@ var SortTracker = class {
783
815
  matchedTracks.add(pair.track);
784
816
  used.add(pair.detIdx);
785
817
  }
818
+ const rescuePairs = [];
819
+ for (const track of this.tracks) {
820
+ if (matchedTracks.has(track)) continue;
821
+ for (let di = 0; di < detections.length; di++) {
822
+ if (used.has(di)) continue;
823
+ const det = detections[di];
824
+ if (!this.looseMatch(track, det)) continue;
825
+ rescuePairs.push({
826
+ track,
827
+ detIdx: di,
828
+ score: iou$1(track.bbox, det.bbox)
829
+ });
830
+ }
831
+ }
832
+ rescuePairs.sort((a, b) => b.score - a.score);
833
+ for (const pair of rescuePairs) {
834
+ if (matchedTracks.has(pair.track) || used.has(pair.detIdx)) continue;
835
+ matched.set(pair.track, detections[pair.detIdx]);
836
+ matchedTracks.add(pair.track);
837
+ used.add(pair.detIdx);
838
+ }
786
839
  for (const [track, det] of matched) {
787
840
  const prevCenter = bboxCentroid({
788
841
  x: track.bbox.x,
@@ -821,12 +874,47 @@ var SortTracker = class {
821
874
  }
822
875
  const occluded = this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(this.predicted(track), ob) >= this.config.occlusionContainment);
823
876
  track.age++;
824
- const limit = occluded ? this.config.occlusionMaxMissedFrames : this.config.maxMissedFrames;
825
- if (track.age > limit) {
877
+ if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
826
878
  track.lost = true;
879
+ track.lostAt = timestamp;
880
+ track.resurrectable = true;
827
881
  this.lostTracks.push(track);
828
882
  } else surviving.push(track);
829
883
  }
884
+ for (let di = 0; di < detections.length; di++) {
885
+ if (used.has(di)) continue;
886
+ const det = detections[di];
887
+ let best;
888
+ let bestScore = -1;
889
+ for (const lost of this.lostTracks) {
890
+ if (!lost.resurrectable) continue;
891
+ if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
892
+ if (!this.looseMatch(lost, det)) continue;
893
+ const score = iou$1(lost.bbox, det.bbox);
894
+ if (score > bestScore) {
895
+ best = lost;
896
+ bestScore = score;
897
+ }
898
+ }
899
+ if (!best) continue;
900
+ this.lostTracks = this.lostTracks.filter((t) => t !== best);
901
+ best.bbox = det.bbox;
902
+ best.class = det.class;
903
+ best.originalClass = det.originalClass;
904
+ best.score = det.score;
905
+ best.age = 0;
906
+ best.hits++;
907
+ best.lost = false;
908
+ best.lastSeen = timestamp;
909
+ best.velocity = {
910
+ dx: 0,
911
+ dy: 0
912
+ };
913
+ best.path.push(det.bbox);
914
+ if (best.path.length > MAX_PATH_LENGTH) best.path.shift();
915
+ surviving.push(best);
916
+ used.add(di);
917
+ }
830
918
  for (let di = 0; di < detections.length; di++) {
831
919
  if (used.has(di)) continue;
832
920
  const det = detections[di];
@@ -846,7 +934,9 @@ var SortTracker = class {
846
934
  dx: 0,
847
935
  dy: 0
848
936
  },
849
- lost: false
937
+ lost: false,
938
+ lostAt: 0,
939
+ resurrectable: true
850
940
  });
851
941
  }
852
942
  this.tracks = surviving;
@@ -961,6 +1051,7 @@ var StateAnalyzer = class {
961
1051
  //#region src/pipeline-analytics/pipeline/events/event-filter.ts
962
1052
  var DEFAULT_EVENT_EMITTER_CONFIG = {
963
1053
  minTrackAge: 3,
1054
+ minTrackAgeMs: 0,
964
1055
  cooldownSec: 5,
965
1056
  enabledTypes: [
966
1057
  "object.entering",
@@ -991,6 +1082,13 @@ var DetectionEventEmitter = class {
991
1082
  config;
992
1083
  previousStates = /* @__PURE__ */ new Map();
993
1084
  lastEmitted = /* @__PURE__ */ new Map();
1085
+ /**
1086
+ * Last-known track per id. A `leaving` state fires for a track that is ABSENT
1087
+ * from the current frame, so it can't be resolved from `trackMap` — it is
1088
+ * resolved from this snapshot instead (without it, leaving events never
1089
+ * fired: the latent "0 `left` events in 24h" bug).
1090
+ */
1091
+ lastKnownTracks = /* @__PURE__ */ new Map();
994
1092
  constructor(config = {}) {
995
1093
  this.config = {
996
1094
  ...DEFAULT_EVENT_EMITTER_CONFIG,
@@ -1002,10 +1100,12 @@ var DetectionEventEmitter = class {
1002
1100
  const now = Date.now();
1003
1101
  const stateMap = new Map(states.map((s) => [s.trackId, s]));
1004
1102
  const trackMap = new Map(tracks.map((t) => [t.trackId, t]));
1103
+ for (const t of tracks) this.lastKnownTracks.set(t.trackId, t);
1005
1104
  for (const state of states) {
1006
- const track = trackMap.get(state.trackId);
1105
+ const track = trackMap.get(state.trackId) ?? this.lastKnownTracks.get(state.trackId);
1007
1106
  if (!track) continue;
1008
1107
  if (track.trackAge < this.config.minTrackAge) continue;
1108
+ if (state.dwellTimeMs < this.config.minTrackAgeMs) continue;
1009
1109
  const eventType = STATE_TO_EVENT[state.state];
1010
1110
  if (!eventType) continue;
1011
1111
  if (!this.config.enabledTypes.includes(eventType)) continue;
@@ -1051,12 +1151,16 @@ var DetectionEventEmitter = class {
1051
1151
  trackPath: [...track.path]
1052
1152
  });
1053
1153
  }
1054
- for (const state of states) if (state.state === "leaving") this.previousStates.delete(state.trackId);
1154
+ for (const state of states) if (state.state === "leaving") {
1155
+ this.previousStates.delete(state.trackId);
1156
+ this.lastKnownTracks.delete(state.trackId);
1157
+ }
1055
1158
  return events;
1056
1159
  }
1057
1160
  reset() {
1058
1161
  this.previousStates.clear();
1059
1162
  this.lastEmitted.clear();
1163
+ this.lastKnownTracks.clear();
1060
1164
  }
1061
1165
  };
1062
1166
  //#endregion
@@ -1717,10 +1821,13 @@ var TrackStore = class {
1717
1821
  try {
1718
1822
  await this.persistCompleted(record);
1719
1823
  } catch (err) {
1720
- this.logger.warn("persist completed track failed", { meta: {
1721
- trackId,
1722
- error: String(err)
1723
- } });
1824
+ this.logger.warn("persist completed track failed", {
1825
+ tags: { deviceId: t.deviceId },
1826
+ meta: {
1827
+ trackId,
1828
+ error: String(err)
1829
+ }
1830
+ });
1724
1831
  }
1725
1832
  this.active.delete(trackId);
1726
1833
  expired.push(record);
@@ -1947,10 +2054,13 @@ var MediaStore = class {
1947
2054
  });
1948
2055
  return key;
1949
2056
  } catch (err) {
1950
- this.logger.warn("media put failed", { meta: {
1951
- key,
1952
- error: String(err)
1953
- } });
2057
+ this.logger.warn("media put failed", {
2058
+ tags: { deviceId: params.deviceId },
2059
+ meta: {
2060
+ key,
2061
+ error: String(err)
2062
+ }
2063
+ });
1954
2064
  throw err;
1955
2065
  }
1956
2066
  }
@@ -2139,11 +2249,14 @@ var MediaStore = class {
2139
2249
  relativePath: oldPath
2140
2250
  });
2141
2251
  } catch (err) {
2142
- this.logger.warn("MediaStore.reown: old blob delete failed (best-effort)", { meta: {
2143
- key,
2144
- oldPath,
2145
- error: String(err)
2146
- } });
2252
+ this.logger.warn("MediaStore.reown: old blob delete failed (best-effort)", {
2253
+ tags: { deviceId },
2254
+ meta: {
2255
+ key,
2256
+ oldPath,
2257
+ error: String(err)
2258
+ }
2259
+ });
2147
2260
  }
2148
2261
  try {
2149
2262
  await this.store.delete.mutate({
@@ -2151,10 +2264,13 @@ var MediaStore = class {
2151
2264
  key
2152
2265
  });
2153
2266
  } catch (err) {
2154
- this.logger.warn("MediaStore.reown: old index row delete failed (best-effort)", { meta: {
2155
- key,
2156
- error: String(err)
2157
- } });
2267
+ this.logger.warn("MediaStore.reown: old index row delete failed (best-effort)", {
2268
+ tags: { deviceId },
2269
+ meta: {
2270
+ key,
2271
+ error: String(err)
2272
+ }
2273
+ });
2158
2274
  }
2159
2275
  return newKey;
2160
2276
  }
@@ -2355,10 +2471,13 @@ var EventStore = class {
2355
2471
  }
2356
2472
  });
2357
2473
  } catch (err) {
2358
- this.logger.warn("insertMotion failed", { meta: {
2359
- eventId: ev.id,
2360
- error: String(err)
2361
- } });
2474
+ this.logger.warn("insertMotion failed", {
2475
+ tags: { deviceId: ev.deviceId },
2476
+ meta: {
2477
+ eventId: ev.id,
2478
+ error: String(err)
2479
+ }
2480
+ });
2362
2481
  }
2363
2482
  }
2364
2483
  async insertObject(ev) {
@@ -2372,10 +2491,13 @@ var EventStore = class {
2372
2491
  }
2373
2492
  });
2374
2493
  } catch (err) {
2375
- this.logger.warn("insertObject failed", { meta: {
2376
- eventId: ev.id,
2377
- error: String(err)
2378
- } });
2494
+ this.logger.warn("insertObject failed", {
2495
+ tags: { deviceId: ev.deviceId },
2496
+ meta: {
2497
+ eventId: ev.id,
2498
+ error: String(err)
2499
+ }
2500
+ });
2379
2501
  }
2380
2502
  }
2381
2503
  async insertAudio(ev) {
@@ -2389,10 +2511,13 @@ var EventStore = class {
2389
2511
  }
2390
2512
  });
2391
2513
  } catch (err) {
2392
- this.logger.warn("insertAudio failed", { meta: {
2393
- eventId: ev.id,
2394
- error: String(err)
2395
- } });
2514
+ this.logger.warn("insertAudio failed", {
2515
+ tags: { deviceId: ev.deviceId },
2516
+ meta: {
2517
+ eventId: ev.id,
2518
+ error: String(err)
2519
+ }
2520
+ });
2396
2521
  }
2397
2522
  }
2398
2523
  buildFilter(q) {
@@ -2861,25 +2986,34 @@ var EventMediaDispatcher = class {
2861
2986
  getRemoteFrame: this.deps.getRemoteFrame
2862
2987
  });
2863
2988
  } catch (err) {
2864
- this.deps.logger.debug("event media: resolveFrame threw", { meta: {
2865
- deviceId,
2866
- shmId: frameHandle.shmId,
2867
- error: String(err)
2868
- } });
2989
+ this.deps.logger.debug("event media: resolveFrame threw", {
2990
+ tags: { deviceId },
2991
+ meta: {
2992
+ deviceId,
2993
+ shmId: frameHandle.shmId,
2994
+ error: String(err)
2995
+ }
2996
+ });
2869
2997
  return;
2870
2998
  }
2871
2999
  if (!decoded) {
2872
- this.deps.logger.debug("event media: frame recycled before resolve", { meta: {
2873
- deviceId,
2874
- shmId: frameHandle.shmId
2875
- } });
3000
+ this.deps.logger.debug("event media: frame recycled before resolve", {
3001
+ tags: { deviceId },
3002
+ meta: {
3003
+ deviceId,
3004
+ shmId: frameHandle.shmId
3005
+ }
3006
+ });
2876
3007
  return;
2877
3008
  }
2878
3009
  if (decoded.format !== "rgb") {
2879
- this.deps.logger.debug("event media: resolved frame is not RGB", { meta: {
2880
- deviceId,
2881
- format: decoded.format
2882
- } });
3010
+ this.deps.logger.debug("event media: resolved frame is not RGB", {
3011
+ tags: { deviceId },
3012
+ meta: {
3013
+ deviceId,
3014
+ format: decoded.format
3015
+ }
3016
+ });
2883
3017
  return;
2884
3018
  }
2885
3019
  const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
@@ -2921,11 +3055,14 @@ var EventMediaDispatcher = class {
2921
3055
  data: crop
2922
3056
  });
2923
3057
  } catch (err) {
2924
- this.deps.logger.warn("event media: crop failed", { meta: {
2925
- deviceId,
2926
- eventId: ev.eventId,
2927
- error: err instanceof Error ? err.message : String(err)
2928
- } });
3058
+ this.deps.logger.warn("event media: crop failed", {
3059
+ tags: { deviceId },
3060
+ meta: {
3061
+ deviceId,
3062
+ eventId: ev.eventId,
3063
+ error: err instanceof Error ? err.message : String(err)
3064
+ }
3065
+ });
2929
3066
  }
2930
3067
  try {
2931
3068
  const fullFrame = await drawBoxedFrame(frameData, fw, fh, [], { quality: MEDIA_QUALITY });
@@ -2938,11 +3075,14 @@ var EventMediaDispatcher = class {
2938
3075
  data: fullFrame
2939
3076
  });
2940
3077
  } catch (err) {
2941
- this.deps.logger.warn("event media: clear full frame failed", { meta: {
2942
- deviceId,
2943
- eventId: ev.eventId,
2944
- error: err instanceof Error ? err.message : String(err)
2945
- } });
3078
+ this.deps.logger.warn("event media: clear full frame failed", {
3079
+ tags: { deviceId },
3080
+ meta: {
3081
+ deviceId,
3082
+ eventId: ev.eventId,
3083
+ error: err instanceof Error ? err.message : String(err)
3084
+ }
3085
+ });
2946
3086
  }
2947
3087
  try {
2948
3088
  const fullFrameBoxed = await drawBoxedFrame(frameData, fw, fh, [box], { quality: MEDIA_QUALITY });
@@ -2955,11 +3095,14 @@ var EventMediaDispatcher = class {
2955
3095
  data: fullFrameBoxed
2956
3096
  });
2957
3097
  } catch (err) {
2958
- this.deps.logger.warn("event media: boxed full frame failed", { meta: {
2959
- deviceId,
2960
- eventId: ev.eventId,
2961
- error: err instanceof Error ? err.message : String(err)
2962
- } });
3098
+ this.deps.logger.warn("event media: boxed full frame failed", {
3099
+ tags: { deviceId },
3100
+ meta: {
3101
+ deviceId,
3102
+ eventId: ev.eventId,
3103
+ error: err instanceof Error ? err.message : String(err)
3104
+ }
3105
+ });
2963
3106
  }
2964
3107
  if (ev.childCrops) for (const child of ev.childCrops) try {
2965
3108
  const childRegion = squareSafeCropRegion(child.bbox, {
@@ -2989,11 +3132,14 @@ var EventMediaDispatcher = class {
2989
3132
  data: childCropData
2990
3133
  });
2991
3134
  } catch (err) {
2992
- this.deps.logger.warn(`event media: ${child.kind} failed`, { meta: {
2993
- deviceId,
2994
- eventId: ev.eventId,
2995
- error: err instanceof Error ? err.message : String(err)
2996
- } });
3135
+ this.deps.logger.warn(`event media: ${child.kind} failed`, {
3136
+ tags: { deviceId },
3137
+ meta: {
3138
+ deviceId,
3139
+ eventId: ev.eventId,
3140
+ error: err instanceof Error ? err.message : String(err)
3141
+ }
3142
+ });
2997
3143
  }
2998
3144
  }
2999
3145
  async writeTrackFrame(deviceId, frameData, fw, fh, tf) {
@@ -3012,11 +3158,14 @@ var EventMediaDispatcher = class {
3012
3158
  data: boxed
3013
3159
  });
3014
3160
  } catch (err) {
3015
- this.deps.logger.warn("event media: track frame failed", { meta: {
3016
- deviceId,
3017
- trackId: tf.trackId,
3018
- error: err instanceof Error ? err.message : String(err)
3019
- } });
3161
+ this.deps.logger.warn("event media: track frame failed", {
3162
+ tags: { deviceId },
3163
+ meta: {
3164
+ deviceId,
3165
+ trackId: tf.trackId,
3166
+ error: err instanceof Error ? err.message : String(err)
3167
+ }
3168
+ });
3020
3169
  }
3021
3170
  }
3022
3171
  };
@@ -3732,22 +3881,22 @@ function resolveDetectionSensitivitySettings(raw) {
3732
3881
  stationaryThresholdSec: pick("stationaryThresholdSec")
3733
3882
  };
3734
3883
  }
3735
- //#endregion
3736
- //#region src/pipeline-analytics/tracking-settings.ts
3737
- /**
3738
- * Per-device tracker tuning. These drive the SORT tracker (association,
3739
- * coasting, occlusion, lifetime) and the detector-dropout frame skip. Every
3740
- * field is independently overridable per camera from the admin UI; unknown or
3741
- * invalid values fall back to the field default (never throws on a bad blob).
3742
- *
3743
- * Note: `minHits` lives in detection-sensitivity-settings (it predates this
3744
- * file and already flows to the tracker) — it is NOT duplicated here.
3745
- */
3746
3884
  var TrackingSettingsSchema = require_dist.object({
3747
3885
  /** IoU required to match a (predicted) track to a detection. */
3748
3886
  iouThreshold: require_dist.number().min(0).max(1).default(.3),
3749
- /** Frames a track may miss detections (coast) before it is dropped. */
3887
+ /** Wall-clock coasting budget (ms) before a missed track is dropped. Time-
3888
+ * based so behaviour is identical across camera frame rates. */
3889
+ maxMissedMs: require_dist.number().min(0).default(4e3),
3890
+ /** Minimum wall-clock track age (ms) before it may produce events. */
3891
+ minTrackAgeMs: require_dist.number().min(0).default(1e3),
3892
+ /** Extended coasting budget (ms) while a track is occluded. */
3893
+ occlusionMaxMissedMs: require_dist.number().min(0).default(8e3),
3894
+ /** @deprecated frame-based alias for {@link maxMissedMs}. Retained so
3895
+ * existing per-device blobs keep working — resolved into `maxMissedMs`
3896
+ * (× {@link NOMINAL_FRAME_MS}) only when `maxMissedMs` is unset. */
3750
3897
  maxMissedFrames: require_dist.number().int().min(0).default(30),
3898
+ /** @deprecated frame-based alias for {@link occlusionMaxMissedMs}. */
3899
+ occlusionMaxMissedFrames: require_dist.number().int().min(0).default(60),
3751
3900
  /** Hard cap on total track lifetime — retire+restart beyond this to bound
3752
3901
  * drift / long-lived false positives. 0 = unlimited. */
3753
3902
  maxTrackLifetimeSec: require_dist.number().min(0).default(300),
@@ -3761,8 +3910,17 @@ var TrackingSettingsSchema = require_dist.object({
3761
3910
  /** Containment ratio (intersection / occluded-area) above which a missed
3762
3911
  * track is considered hidden behind another track. */
3763
3912
  occlusionContainment: require_dist.number().min(0).max(1).default(.6),
3764
- /** Extended coasting budget (frames) while a track is occluded. */
3765
- occlusionMaxMissedFrames: require_dist.number().int().min(0).default(60),
3913
+ /** Loose IoU gate (vs. last-known bbox) for the second (rescue) association
3914
+ * pass + graveyard resurrection. */
3915
+ rescueIouThreshold: require_dist.number().min(0).max(1).default(.1),
3916
+ /** Centroid gate for rescue/resurrection: fraction of the bbox diagonal. */
3917
+ rescueCentroidFactor: require_dist.number().min(0).default(.75),
3918
+ /** Wall-clock graveyard retention (ms): a dropped track re-attaches to a new
3919
+ * same-class detection (keeping its id) for this long. */
3920
+ resurrectionWindowMs: require_dist.number().min(0).default(8e3),
3921
+ /** Speed (px/frame) below which a track's prediction is frozen (stationary
3922
+ * jitter can't drift the box off a sitting object). */
3923
+ stationarySpeedPx: require_dist.number().min(0).default(2),
3766
3924
  /** Skip frames where detections suddenly drop to zero (detector glitch). */
3767
3925
  dropoutSkipEnabled: require_dist.boolean().default(true),
3768
3926
  /** Max consecutive all-zero frames absorbed as a glitch before the scene is
@@ -3777,15 +3935,26 @@ var TRACKING_DEFAULTS = TrackingSettingsSchema.parse({});
3777
3935
  */
3778
3936
  function resolveTrackingSettings(raw) {
3779
3937
  const s = TrackingSettingsSchema.shape;
3938
+ const maxMissedFrames = s.maxMissedFrames.catch(TRACKING_DEFAULTS.maxMissedFrames).parse(raw.maxMissedFrames);
3939
+ const occlusionMaxMissedFrames = s.occlusionMaxMissedFrames.catch(TRACKING_DEFAULTS.occlusionMaxMissedFrames).parse(raw.occlusionMaxMissedFrames);
3940
+ const maxMissedMs = raw.maxMissedMs !== void 0 ? s.maxMissedMs.catch(TRACKING_DEFAULTS.maxMissedMs).parse(raw.maxMissedMs) : raw.maxMissedFrames !== void 0 ? Math.round(maxMissedFrames * 133) : TRACKING_DEFAULTS.maxMissedMs;
3941
+ const occlusionMaxMissedMs = raw.occlusionMaxMissedMs !== void 0 ? s.occlusionMaxMissedMs.catch(TRACKING_DEFAULTS.occlusionMaxMissedMs).parse(raw.occlusionMaxMissedMs) : raw.occlusionMaxMissedFrames !== void 0 ? Math.round(occlusionMaxMissedFrames * 133) : TRACKING_DEFAULTS.occlusionMaxMissedMs;
3780
3942
  return {
3781
3943
  iouThreshold: s.iouThreshold.catch(TRACKING_DEFAULTS.iouThreshold).parse(raw.iouThreshold),
3782
- maxMissedFrames: s.maxMissedFrames.catch(TRACKING_DEFAULTS.maxMissedFrames).parse(raw.maxMissedFrames),
3944
+ maxMissedMs,
3945
+ minTrackAgeMs: s.minTrackAgeMs.catch(TRACKING_DEFAULTS.minTrackAgeMs).parse(raw.minTrackAgeMs),
3946
+ occlusionMaxMissedMs,
3947
+ maxMissedFrames,
3948
+ occlusionMaxMissedFrames,
3783
3949
  maxTrackLifetimeSec: s.maxTrackLifetimeSec.catch(TRACKING_DEFAULTS.maxTrackLifetimeSec).parse(raw.maxTrackLifetimeSec),
3784
3950
  predictiveCoasting: s.predictiveCoasting.catch(TRACKING_DEFAULTS.predictiveCoasting).parse(raw.predictiveCoasting),
3785
3951
  classGating: s.classGating.catch(TRACKING_DEFAULTS.classGating).parse(raw.classGating),
3786
3952
  occlusionEnabled: s.occlusionEnabled.catch(TRACKING_DEFAULTS.occlusionEnabled).parse(raw.occlusionEnabled),
3787
3953
  occlusionContainment: s.occlusionContainment.catch(TRACKING_DEFAULTS.occlusionContainment).parse(raw.occlusionContainment),
3788
- occlusionMaxMissedFrames: s.occlusionMaxMissedFrames.catch(TRACKING_DEFAULTS.occlusionMaxMissedFrames).parse(raw.occlusionMaxMissedFrames),
3954
+ rescueIouThreshold: s.rescueIouThreshold.catch(TRACKING_DEFAULTS.rescueIouThreshold).parse(raw.rescueIouThreshold),
3955
+ rescueCentroidFactor: s.rescueCentroidFactor.catch(TRACKING_DEFAULTS.rescueCentroidFactor).parse(raw.rescueCentroidFactor),
3956
+ resurrectionWindowMs: s.resurrectionWindowMs.catch(TRACKING_DEFAULTS.resurrectionWindowMs).parse(raw.resurrectionWindowMs),
3957
+ stationarySpeedPx: s.stationarySpeedPx.catch(TRACKING_DEFAULTS.stationarySpeedPx).parse(raw.stationarySpeedPx),
3789
3958
  dropoutSkipEnabled: s.dropoutSkipEnabled.catch(TRACKING_DEFAULTS.dropoutSkipEnabled).parse(raw.dropoutSkipEnabled),
3790
3959
  dropoutMaxSkipFrames: s.dropoutMaxSkipFrames.catch(TRACKING_DEFAULTS.dropoutMaxSkipFrames).parse(raw.dropoutMaxSkipFrames)
3791
3960
  };
@@ -4228,10 +4397,13 @@ var FaceStore = class {
4228
4397
  value: rest
4229
4398
  });
4230
4399
  } catch (err) {
4231
- this.logger.warn("FaceStore.insert failed", { meta: {
4232
- faceId: id,
4233
- error: String(err)
4234
- } });
4400
+ this.logger.warn("FaceStore.insert failed", {
4401
+ tags: { deviceId: face.deviceId },
4402
+ meta: {
4403
+ faceId: id,
4404
+ error: String(err)
4405
+ }
4406
+ });
4235
4407
  }
4236
4408
  }
4237
4409
  /**
@@ -4298,10 +4470,13 @@ var FaceStore = class {
4298
4470
  });
4299
4471
  deleted.push(id);
4300
4472
  } catch (err) {
4301
- this.logger.warn("FaceStore.prune delete failed", { meta: {
4302
- faceId: id,
4303
- error: String(err)
4304
- } });
4473
+ this.logger.warn("FaceStore.prune delete failed", {
4474
+ tags: { deviceId: input.deviceId },
4475
+ meta: {
4476
+ faceId: id,
4477
+ error: String(err)
4478
+ }
4479
+ });
4305
4480
  }
4306
4481
  return deleted;
4307
4482
  }
@@ -4587,10 +4762,13 @@ var ObjectEmbeddingStore = class {
4587
4762
  value: record
4588
4763
  });
4589
4764
  } catch (err) {
4590
- this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", { meta: {
4591
- trackId: input.trackId,
4592
- error: String(err)
4593
- } });
4765
+ this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", {
4766
+ tags: { deviceId: input.deviceId },
4767
+ meta: {
4768
+ trackId: input.trackId,
4769
+ error: String(err)
4770
+ }
4771
+ });
4594
4772
  }
4595
4773
  }
4596
4774
  /**
@@ -4802,18 +4980,24 @@ var FaceRecognizer = class {
4802
4980
  try {
4803
4981
  await this.deps.trackStore.setLabel(c.trackId, name);
4804
4982
  } catch (err) {
4805
- this.deps.logger.warn("setLabel failed", { meta: {
4806
- trackId: c.trackId,
4807
- error: String(err)
4808
- } });
4983
+ this.deps.logger.warn("setLabel failed", {
4984
+ tags: { deviceId: input.deviceId },
4985
+ meta: {
4986
+ trackId: c.trackId,
4987
+ error: String(err)
4988
+ }
4989
+ });
4809
4990
  }
4810
4991
  try {
4811
4992
  await this.deps.eventStore.setLabelForTrack(c.trackId, name);
4812
4993
  } catch (err) {
4813
- this.deps.logger.warn("setLabelForTrack failed", { meta: {
4814
- trackId: c.trackId,
4815
- error: String(err)
4816
- } });
4994
+ this.deps.logger.warn("setLabelForTrack failed", {
4995
+ tags: { deviceId: input.deviceId },
4996
+ meta: {
4997
+ trackId: c.trackId,
4998
+ error: String(err)
4999
+ }
5000
+ });
4817
5001
  }
4818
5002
  }
4819
5003
  }
@@ -4825,10 +5009,13 @@ var FaceRecognizer = class {
4825
5009
  if (input.frameHandle !== void 0) try {
4826
5010
  crop = await this.deps.captureCrop(input.frameHandle, cropBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
4827
5011
  } catch (err) {
4828
- this.deps.logger.debug("FaceRecognizer crop capture failed", { meta: {
4829
- trackId: c.trackId,
4830
- error: String(err)
4831
- } });
5012
+ this.deps.logger.debug("FaceRecognizer crop capture failed", {
5013
+ tags: { deviceId: input.deviceId },
5014
+ meta: {
5015
+ trackId: c.trackId,
5016
+ error: String(err)
5017
+ }
5018
+ });
4832
5019
  }
4833
5020
  this.bestFace.set(c.trackId, {
4834
5021
  score: c.confidence,
@@ -4884,10 +5071,13 @@ var FaceRecognizer = class {
4884
5071
  data: held.crop
4885
5072
  });
4886
5073
  } catch (err) {
4887
- this.deps.logger.warn("FaceRecognizer face crop put failed", { meta: {
4888
- faceId,
4889
- error: String(err)
4890
- } });
5074
+ this.deps.logger.warn("FaceRecognizer face crop put failed", {
5075
+ tags: { deviceId },
5076
+ meta: {
5077
+ faceId,
5078
+ error: String(err)
5079
+ }
5080
+ });
4891
5081
  }
4892
5082
  try {
4893
5083
  await this.deps.faceStore.insert({
@@ -4913,10 +5103,13 @@ var FaceRecognizer = class {
4913
5103
  }
4914
5104
  });
4915
5105
  } catch (err) {
4916
- this.deps.logger.warn("FaceRecognizer faceStore insert failed", { meta: {
4917
- faceId,
4918
- error: String(err)
4919
- } });
5106
+ this.deps.logger.warn("FaceRecognizer faceStore insert failed", {
5107
+ tags: { deviceId },
5108
+ meta: {
5109
+ faceId,
5110
+ error: String(err)
5111
+ }
5112
+ });
4920
5113
  }
4921
5114
  }
4922
5115
  };
@@ -4996,10 +5189,13 @@ var PlateStore = class {
4996
5189
  value: rest
4997
5190
  });
4998
5191
  } catch (err) {
4999
- this.logger.warn("PlateStore.insert failed", { meta: {
5000
- plateId: id,
5001
- error: String(err)
5002
- } });
5192
+ this.logger.warn("PlateStore.insert failed", {
5193
+ tags: { deviceId: plate.deviceId },
5194
+ meta: {
5195
+ plateId: id,
5196
+ error: String(err)
5197
+ }
5198
+ });
5003
5199
  }
5004
5200
  }
5005
5201
  normalizeRow(r) {
@@ -5155,10 +5351,13 @@ var PlateRecognizer = class {
5155
5351
  if (input.frameHandle !== void 0) try {
5156
5352
  crop = await this.deps.captureCrop(input.frameHandle, c.plateBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
5157
5353
  } catch (err) {
5158
- this.deps.logger.debug("PlateRecognizer crop capture failed", { meta: {
5159
- trackId: c.trackId,
5160
- error: String(err)
5161
- } });
5354
+ this.deps.logger.debug("PlateRecognizer crop capture failed", {
5355
+ tags: { deviceId: input.deviceId },
5356
+ meta: {
5357
+ trackId: c.trackId,
5358
+ error: String(err)
5359
+ }
5360
+ });
5162
5361
  }
5163
5362
  this.bestPlate.set(c.trackId, {
5164
5363
  text: c.plateText,
@@ -5187,10 +5386,13 @@ var PlateRecognizer = class {
5187
5386
  data: held.crop
5188
5387
  });
5189
5388
  } catch (err) {
5190
- this.deps.logger.warn("PlateRecognizer plate crop put failed", { meta: {
5191
- plateId,
5192
- error: String(err)
5193
- } });
5389
+ this.deps.logger.warn("PlateRecognizer plate crop put failed", {
5390
+ tags: { deviceId },
5391
+ meta: {
5392
+ plateId,
5393
+ error: String(err)
5394
+ }
5395
+ });
5194
5396
  }
5195
5397
  try {
5196
5398
  await this.deps.plateStore.insert({
@@ -5216,10 +5418,13 @@ var PlateRecognizer = class {
5216
5418
  }
5217
5419
  });
5218
5420
  } catch (err) {
5219
- this.deps.logger.warn("PlateRecognizer plateStore insert failed", { meta: {
5220
- plateId,
5221
- error: String(err)
5222
- } });
5421
+ this.deps.logger.warn("PlateRecognizer plateStore insert failed", {
5422
+ tags: { deviceId },
5423
+ meta: {
5424
+ plateId,
5425
+ error: String(err)
5426
+ }
5427
+ });
5223
5428
  }
5224
5429
  }
5225
5430
  };
@@ -5666,7 +5871,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
5666
5871
  let storage = this.ctx.kernel.storage;
5667
5872
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
5668
5873
  if (mediaRoot) {
5669
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-CdzrYoKs.js"));
5874
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-Cx0LM0Sq.js"));
5670
5875
  storage = new FilesystemStorageProvider(mediaRoot);
5671
5876
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
5672
5877
  }
@@ -6530,7 +6735,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6530
6735
  } : void 0, { classificationMinScore: settings.classificationMinScore });
6531
6736
  if (route.kind === "classification") {
6532
6737
  if (!topClassification) {
6533
- this.ctx.logger.warn("classifyAudioFrame returned classification but topClassification is null", { meta: { deviceId } });
6738
+ this.ctx.logger.warn("classifyAudioFrame returned classification but topClassification is null", {
6739
+ tags: { deviceId },
6740
+ meta: { deviceId }
6741
+ });
6534
6742
  return;
6535
6743
  }
6536
6744
  const last = this.lastAudioInsertByDevice.get(deviceId);
@@ -6891,16 +7099,21 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
6891
7099
  p = new FrameProcessor(deviceId, {
6892
7100
  minHits,
6893
7101
  iouThreshold: trk.iouThreshold,
6894
- maxMissedFrames: trk.maxMissedFrames,
7102
+ maxMissedMs: trk.maxMissedMs,
6895
7103
  predictiveCoasting: trk.predictiveCoasting,
6896
7104
  classGating: trk.classGating,
6897
7105
  occlusionEnabled: trk.occlusionEnabled,
6898
7106
  occlusionContainment: trk.occlusionContainment,
6899
- occlusionMaxMissedFrames: trk.occlusionMaxMissedFrames,
7107
+ occlusionMaxMissedMs: trk.occlusionMaxMissedMs,
7108
+ rescueIouThreshold: trk.rescueIouThreshold,
7109
+ rescueCentroidFactor: trk.rescueCentroidFactor,
7110
+ resurrectionWindowMs: trk.resurrectionWindowMs,
7111
+ stationarySpeedPx: trk.stationarySpeedPx,
6900
7112
  maxTrackLifetimeMs: trk.maxTrackLifetimeSec * 1e3
6901
7113
  }, { stationaryThresholdSec }, {
6902
7114
  minTrackAge,
6903
- cooldownSec
7115
+ cooldownSec,
7116
+ minTrackAgeMs: trk.minTrackAgeMs
6904
7117
  }, source);
6905
7118
  this.processors.set(key, p);
6906
7119
  }
@@ -7080,13 +7293,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7080
7293
  const { counts, ids } = await this.eventStore.pruneBefore(input);
7081
7294
  if (ids.length > 0 && this.mediaStore) await this.mediaStore.deleteForEvents([...ids]);
7082
7295
  const { motion, object, audio } = counts;
7083
- if (motion + object + audio > 0) this.ctx.logger.info("analytics event eviction (floor)", { meta: {
7084
- deviceId: input.deviceId,
7085
- cutoffMs: input.cutoffMs,
7086
- motion,
7087
- object,
7088
- audio
7089
- } });
7296
+ if (motion + object + audio > 0) this.ctx.logger.info("analytics event eviction (floor)", {
7297
+ tags: { deviceId: input.deviceId },
7298
+ meta: {
7299
+ deviceId: input.deviceId,
7300
+ cutoffMs: input.cutoffMs,
7301
+ motion,
7302
+ object,
7303
+ audio
7304
+ }
7305
+ });
7090
7306
  return counts;
7091
7307
  }
7092
7308
  /**
@@ -7458,12 +7674,53 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7458
7674
  },
7459
7675
  {
7460
7676
  type: "number",
7461
- key: "maxMissedFrames",
7462
- label: "Max missed frames",
7463
- description: "Frames a track may miss detections (coast) before it is dropped.",
7677
+ key: "maxMissedMs",
7678
+ label: "Max coasting time",
7679
+ description: "How long (ms) a track may miss detections before it is dropped. Time-based so it behaves the same on a 5fps and a 23fps camera.",
7464
7680
  min: 0,
7465
- step: 1,
7466
- default: TRACKING_DEFAULTS.maxMissedFrames
7681
+ step: 100,
7682
+ default: TRACKING_DEFAULTS.maxMissedMs,
7683
+ unit: "ms"
7684
+ },
7685
+ {
7686
+ type: "number",
7687
+ key: "minTrackAgeMs",
7688
+ label: "Min track age",
7689
+ description: "A track must exist for at least this long (ms) before it can produce events — stops sub-second flicker fragments from becoming events.",
7690
+ min: 0,
7691
+ step: 100,
7692
+ default: TRACKING_DEFAULTS.minTrackAgeMs,
7693
+ unit: "ms"
7694
+ },
7695
+ {
7696
+ type: "number",
7697
+ key: "resurrectionWindowMs",
7698
+ label: "Resurrection window",
7699
+ description: "How long (ms) a dropped track can be re-attached to a re-appearing same-class detection (keeping its id) — the main fix for a stationary object fragmenting into many tracks.",
7700
+ min: 0,
7701
+ step: 500,
7702
+ default: TRACKING_DEFAULTS.resurrectionWindowMs,
7703
+ unit: "ms"
7704
+ },
7705
+ {
7706
+ type: "number",
7707
+ key: "rescueIouThreshold",
7708
+ label: "Rescue IoU threshold",
7709
+ description: "Looser overlap (vs. a track’s last-known box) for the second association + resurrection passes. Lower = more aggressive re-attachment.",
7710
+ min: 0,
7711
+ max: 1,
7712
+ step: .05,
7713
+ default: TRACKING_DEFAULTS.rescueIouThreshold
7714
+ },
7715
+ {
7716
+ type: "number",
7717
+ key: "stationarySpeedPx",
7718
+ label: "Stationary speed",
7719
+ description: "Speed (px/frame) below which a track is treated as stationary and its prediction is frozen, so bbox jitter cannot walk the box off a sitting object.",
7720
+ min: 0,
7721
+ step: .5,
7722
+ default: TRACKING_DEFAULTS.stationarySpeedPx,
7723
+ unit: "px"
7467
7724
  },
7468
7725
  {
7469
7726
  type: "number",
@@ -7508,12 +7765,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
7508
7765
  },
7509
7766
  {
7510
7767
  type: "number",
7511
- key: "occlusionMaxMissedFrames",
7512
- label: "Occlusion max missed frames",
7513
- description: "Extended coasting budget (frames) while a track is occluded.",
7768
+ key: "occlusionMaxMissedMs",
7769
+ label: "Occlusion max coasting time",
7770
+ description: "Extended coasting time (ms) while a track is hidden behind another.",
7514
7771
  min: 0,
7515
- step: 1,
7516
- default: TRACKING_DEFAULTS.occlusionMaxMissedFrames
7772
+ step: 100,
7773
+ default: TRACKING_DEFAULTS.occlusionMaxMissedMs,
7774
+ unit: "ms"
7517
7775
  },
7518
7776
  {
7519
7777
  type: "boolean",