@camstack/addon-post-analysis 1.2.3 → 1.2.5

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-Ck79MtSp.js");
5
+ const require_dist = require("../dist-Dlb4YrCt.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let sharp = require("sharp");
8
8
  sharp = require_dist.__toESM(sharp);
@@ -3360,6 +3360,24 @@ async function cascadeDeleteTracks(registry, trackStore, trackIds) {
3360
3360
  for (const trackId of trackIds) await trackStore.deletePersisted(trackId);
3361
3361
  }
3362
3362
  /**
3363
+ * Full whole-track cascade for ONE track: events → their media + track media →
3364
+ * track row. Delegates to the generalized `cascadeDeleteTracks` so the shipped
3365
+ * `deleteTracks` provider path flows through the same engine while preserving
3366
+ * the exact legacy call order (event ids stay local to the delete). Adapts the
3367
+ * narrow legacy store shapes onto `TrackScopedStore` — a superset store (faces /
3368
+ * embeddings) is opted in by the CALLER's registry, not by this legacy shim.
3369
+ */
3370
+ async function cascadeDeleteTrack(stores, trackId) {
3371
+ await cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
3372
+ for (const id of ids) {
3373
+ const eventIds = await stores.eventStore.deleteByTrack(id);
3374
+ if (eventIds.length > 0) await stores.mediaStore.deleteForEvents([...eventIds]);
3375
+ }
3376
+ } }, { deleteByTracks: async (ids) => {
3377
+ await stores.mediaStore.deleteForTracks([...ids]);
3378
+ } }], stores.trackStore, [trackId]);
3379
+ }
3380
+ /**
3363
3381
  * Widened batch whole-track deletion — the ONE engine behind the three §5 entry
3364
3382
  * points. Runs the FULL registry cascade per track (via `cascadeDeleteTracks`
3365
3383
  * with a single-id list so the track root is deleted last), fires cleanup on
@@ -6478,6 +6496,36 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
6478
6496
  };
6479
6497
  }
6480
6498
  //#endregion
6499
+ //#region src/shared/frame/crop-extractor.ts
6500
+ /**
6501
+ * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
6502
+ * Coordinates are clamped to frame bounds to avoid out-of-range errors.
6503
+ */
6504
+ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
6505
+ const rawLeft = Math.round(bbox.x * frameWidth);
6506
+ const rawTop = Math.round(bbox.y * frameHeight);
6507
+ const rawWidth = Math.round(bbox.w * frameWidth);
6508
+ const rawHeight = Math.round(bbox.h * frameHeight);
6509
+ const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
6510
+ const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
6511
+ const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
6512
+ const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
6513
+ return {
6514
+ crop: await (0, sharp.default)(frameData, { raw: {
6515
+ width: frameWidth,
6516
+ height: frameHeight,
6517
+ channels: 3
6518
+ } }).extract({
6519
+ left,
6520
+ top,
6521
+ width,
6522
+ height
6523
+ }).jpeg({ quality: 90 }).toBuffer(),
6524
+ width,
6525
+ height
6526
+ };
6527
+ }
6528
+ //#endregion
6481
6529
  //#region src/shared/frame/square-subject-crop.ts
6482
6530
  /** Expansion factor applied to the bbox long side to frame the subject with a
6483
6531
  * little breathing room (operator choice: ×1.2). */
@@ -6716,9 +6764,13 @@ var EventMediaDispatcher = class {
6716
6764
  const snapshots = input.snapshots ?? [];
6717
6765
  const empty = {
6718
6766
  storedSnapshots: [],
6719
- thumbnailTrackIds: []
6767
+ thumbnailTrackIds: [],
6768
+ firstFrameTrackIds: [],
6769
+ lastFrameTrackIds: [],
6770
+ rasterFallbacks: []
6720
6771
  };
6721
- if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
6772
+ const extraCandidateCount = input.rasterFallbackCandidates?.length ?? 0;
6773
+ if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0 && extraCandidateCount === 0) return empty;
6722
6774
  let decoded;
6723
6775
  try {
6724
6776
  decoded = await resolveFrame(frameHandle, { getRemoteFrame: this.deps.getRemoteFrame });
@@ -6768,21 +6820,82 @@ var EventMediaDispatcher = class {
6768
6820
  });
6769
6821
  return empty;
6770
6822
  }
6823
+ const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
6771
6824
  for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6772
- for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6825
+ const firstFrameTrackIds = [];
6826
+ for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
6773
6827
  const storedSnapshots = [];
6774
6828
  const thumbnailTrackIds = [];
6829
+ const lastFrameTrackIds = [];
6775
6830
  for (const sn of snapshots) {
6776
6831
  const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
6777
6832
  if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
6778
6833
  if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
6834
+ if (res.lastFrameWritten) lastFrameTrackIds.push(sn.trackId);
6779
6835
  }
6780
6836
  return {
6781
6837
  storedSnapshots,
6782
- thumbnailTrackIds
6838
+ thumbnailTrackIds,
6839
+ firstFrameTrackIds,
6840
+ lastFrameTrackIds,
6841
+ rasterFallbacks
6783
6842
  };
6784
6843
  }
6785
6844
  /**
6845
+ * Cut ONE clean detection-raster subject crop per WANTED track observed this
6846
+ * frame — the "first available detection-raster frame" of the zero-media
6847
+ * fallback. Candidate bboxes come from this frame's firstFrame/snapshot targets
6848
+ * AND (widened) the explicit `extraCandidates` (confirmed tracks with no such
6849
+ * target). Cropped from the already-resolved `frameData` at its REAL resolution
6850
+ * via {@link extractCrop} (extract-only — NEVER upscaled). Deduped per trackId
6851
+ * (first candidate wins; target-derived candidates precede the extras). A
6852
+ * per-track encode failure is skipped (logged) — a missing fallback simply
6853
+ * leaves the track with no last-resort preview, never an error.
6854
+ */
6855
+ async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted, extraCandidates) {
6856
+ if (!wanted || wanted.size === 0) return [];
6857
+ const seen = /* @__PURE__ */ new Set();
6858
+ const out = [];
6859
+ const candidates = [
6860
+ ...trackFrames.map((t) => ({
6861
+ trackId: t.trackId,
6862
+ timestamp: t.timestamp,
6863
+ bbox: t.bbox
6864
+ })),
6865
+ ...snapshots.map((s) => ({
6866
+ trackId: s.trackId,
6867
+ timestamp: s.timestamp,
6868
+ bbox: s.bbox
6869
+ })),
6870
+ ...(extraCandidates ?? []).map((c) => ({
6871
+ trackId: c.trackId,
6872
+ timestamp: c.timestamp,
6873
+ bbox: c.bbox
6874
+ }))
6875
+ ];
6876
+ for (const c of candidates) {
6877
+ if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
6878
+ seen.add(c.trackId);
6879
+ try {
6880
+ const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
6881
+ W: fw,
6882
+ H: fh
6883
+ }, cropPadding));
6884
+ out.push({
6885
+ trackId: c.trackId,
6886
+ timestamp: c.timestamp,
6887
+ jpeg: crop
6888
+ });
6889
+ } catch (err) {
6890
+ this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
6891
+ trackId: c.trackId,
6892
+ error: err instanceof Error ? err.message : String(err)
6893
+ } });
6894
+ }
6895
+ }
6896
+ return out;
6897
+ }
6898
+ /**
6786
6899
  * Periodic per-track media (§5). The boxed FULL frame is encoded once and
6787
6900
  * shared by the appended `snapshot` (timeline filmstrip) and the rolling
6788
6901
  * `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
@@ -6793,11 +6906,15 @@ var EventMediaDispatcher = class {
6793
6906
  * TrackStore wiring (null when `appendSnapshot` is false or the encode
6794
6907
  * failed). `thumbnailWritten` reports whether a best `thumbnail` actually
6795
6908
  * landed this frame (#27-A) so the caller can stop forcing retries.
6909
+ * `lastFrameWritten` reports whether the rolling `lastFrame` actually landed
6910
+ * (DEFECT B) so the caller advances its `lastFrameAt` clock ONLY on a real
6911
+ * write — a dropped roll leaves the clock put and retries next frame.
6796
6912
  */
6797
6913
  async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
6798
6914
  if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
6799
6915
  storedSnapshot: null,
6800
- thumbnailWritten: false
6916
+ thumbnailWritten: false,
6917
+ lastFrameWritten: false
6801
6918
  };
6802
6919
  let boxed = null;
6803
6920
  if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
@@ -6818,7 +6935,8 @@ var EventMediaDispatcher = class {
6818
6935
  bbox: sn.bbox
6819
6936
  };
6820
6937
  } catch {}
6821
- if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6938
+ let lastFrameWritten = false;
6939
+ if (sn.rollingLastFrame && boxed) lastFrameWritten = await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6822
6940
  let thumbnailWritten = false;
6823
6941
  if (sn.bestThumbnail) {
6824
6942
  const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
@@ -6829,7 +6947,8 @@ var EventMediaDispatcher = class {
6829
6947
  }
6830
6948
  return {
6831
6949
  storedSnapshot: stored,
6832
- thumbnailWritten
6950
+ thumbnailWritten,
6951
+ lastFrameWritten
6833
6952
  };
6834
6953
  }
6835
6954
  /**
@@ -7026,7 +7145,7 @@ var EventMediaDispatcher = class {
7026
7145
  }
7027
7146
  async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
7028
7147
  const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
7029
- if (!boxed) return;
7148
+ if (!boxed) return false;
7030
7149
  try {
7031
7150
  await this.deps.mediaStore.put({
7032
7151
  deviceId,
@@ -7036,6 +7155,7 @@ var EventMediaDispatcher = class {
7036
7155
  timestamp: tf.timestamp,
7037
7156
  data: boxed
7038
7157
  });
7158
+ return true;
7039
7159
  } catch (err) {
7040
7160
  this.deps.logger.warn("event media: track frame failed", {
7041
7161
  tags: { deviceId },
@@ -7045,40 +7165,11 @@ var EventMediaDispatcher = class {
7045
7165
  error: err instanceof Error ? err.message : String(err)
7046
7166
  }
7047
7167
  });
7168
+ return false;
7048
7169
  }
7049
7170
  }
7050
7171
  };
7051
7172
  //#endregion
7052
- //#region src/shared/frame/crop-extractor.ts
7053
- /**
7054
- * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
7055
- * Coordinates are clamped to frame bounds to avoid out-of-range errors.
7056
- */
7057
- async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
7058
- const rawLeft = Math.round(bbox.x * frameWidth);
7059
- const rawTop = Math.round(bbox.y * frameHeight);
7060
- const rawWidth = Math.round(bbox.w * frameWidth);
7061
- const rawHeight = Math.round(bbox.h * frameHeight);
7062
- const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
7063
- const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
7064
- const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
7065
- const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
7066
- return {
7067
- crop: await (0, sharp.default)(frameData, { raw: {
7068
- width: frameWidth,
7069
- height: frameHeight,
7070
- channels: 3
7071
- } }).extract({
7072
- left,
7073
- top,
7074
- width,
7075
- height
7076
- }).jpeg({ quality: 90 }).toBuffer(),
7077
- width,
7078
- height
7079
- };
7080
- }
7081
- //#endregion
7082
7173
  //#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
7083
7174
  function selectCropBbox(detection) {
7084
7175
  return detection.refinedBbox ?? detection.bbox;
@@ -8867,6 +8958,16 @@ function planPeriodicMedia(input) {
8867
8958
  bestThumbnail: input.isNewBest || !thumbnailLanded
8868
8959
  };
8869
8960
  }
8961
+ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
8962
+ /**
8963
+ * Classify a closing track's persistence outcome. Pure — see the module header
8964
+ * for the full contract.
8965
+ */
8966
+ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
8967
+ if (input.hasMedia) return "persist";
8968
+ if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
8969
+ return input.hasRasterFallback ? "raster-fallback" : "persist";
8970
+ }
8870
8971
  //#endregion
8871
8972
  //#region src/pipeline-analytics/best-thumbnail-guard.ts
8872
8973
  /**
@@ -12126,6 +12227,23 @@ function pickTrackFallbackMedia(files) {
12126
12227
  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];
12127
12228
  }
12128
12229
  /**
12230
+ * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
12231
+ * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
12232
+ * frame at all — the regression class created by retiring `crop`/`fullFrame`
12233
+ * production (2026-07-21), leaving degenerate tracks with only boxed
12234
+ * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
12235
+ * returning undefined. A boxed frame (with the target's box drawn) is a real
12236
+ * preview; an empty tile is not. Clean is always tried FIRST so the common case
12237
+ * still serves an unboxed tile. Undefined only when the owner has NO media.
12238
+ *
12239
+ * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
12240
+ * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
12241
+ * composition pure + unit-testable here.
12242
+ */
12243
+ function resolveCropForcedMedia(files, pickClean) {
12244
+ return pickClean(files) ?? pickTrackFallbackMedia(files);
12245
+ }
12246
+ /**
12129
12247
  * Resolve the default-path media for an event id: the event's own media when it
12130
12248
  * has any, else the owning track's fallback media. Returns `null` when neither
12131
12249
  * yields a blob (caller 404s → the surface shows an icon).
@@ -12457,6 +12575,32 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12457
12575
  * where no `snapshot` is appended, so it is never byte-identical to a stored
12458
12576
  * `snapshot` (kills the end-of-track duplicate). Cleared on track end. */
12459
12577
  lastFrameAtByTrack = /* @__PURE__ */ new Map();
12578
+ /** Tracks with a rolling-`lastFrame` capture CURRENTLY in flight (RC-1,
12579
+ * DEFECT B). The `lastFrameAt` clock now advances ONLY when the write lands
12580
+ * (dispatcher completion), not synchronously at plan time — so a dropped roll
12581
+ * (recycled/blank frame) leaves the clock put and re-rolls next frame. Without
12582
+ * an in-flight guard that re-roll would fire EVERY frame while the first
12583
+ * capture is still resolving (0.1–3s under the native path), stacking N
12584
+ * overlapping captures. While a track sits here `buildSnapshotTargets`
12585
+ * suppresses a new rolling-`lastFrame` request; the pending dispatch clears it
12586
+ * (and, if it landed, advances `lastFrameAtByTrack`). Cleared on track end +
12587
+ * reset. */
12588
+ lastFrameInFlight = /* @__PURE__ */ new Set();
12589
+ /** Confirmed-birth tracks whose `firstFrame` has NOT yet actually persisted
12590
+ * (DEFECT A). Seeded on a confirmed birth (alongside the birth firstFrame
12591
+ * target) and removed the moment the write lands. While a track sits here and
12592
+ * is matched this frame, `collectFirstFrameRetries` re-schedules a firstFrame
12593
+ * target so a birth capture dropped by a recycled/blank live frame is retried
12594
+ * on a later frame (earliest available view still beats none). Scoped to
12595
+ * confirmed births ONLY — a suppressed false-positive birth or resurrection
12596
+ * never enters, so it never gets a retro firstFrame. Cleared on track end +
12597
+ * reset. */
12598
+ firstFramePendingTracks = /* @__PURE__ */ new Set();
12599
+ /** Tracks with a `firstFrame` capture CURRENTLY in flight (RC-1, DEFECT A).
12600
+ * Mirrors `thumbnailInFlight`: while a firstFrame capture is resolving the
12601
+ * per-frame retry is suppressed so at most one capture is outstanding per
12602
+ * track. Cleared on dispatch settle (+ track end / reset). */
12603
+ firstFrameInFlight = /* @__PURE__ */ new Set();
12460
12604
  /** Track ids whose best `thumbnail` has ACTUALLY been persisted (#27-A). A
12461
12605
  * track absent here keeps forcing a best-thumbnail capture every frame until
12462
12606
  * one lands, so a short / high-churn track whose first capture was dropped
@@ -12481,6 +12625,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12481
12625
  * area) + emit time, so a material improvement is measured against the
12482
12626
  * last emit and debounced. Seeded at `start`, dropped at `end`. */
12483
12627
  trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
12628
+ /** Per-active-track close-policy state (spec §"Zero-media track policy"):
12629
+ * whether the track ever reached the tracker's hit-confirmation
12630
+ * (`trackAge >= minHits`) and the FIRST retained detection-raster subject
12631
+ * crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
12632
+ * suppress / raster-fallback / persist for a track that ends with no media.
12633
+ * `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
12634
+ * on every teardown path. */
12635
+ trackClosureState = /* @__PURE__ */ new Map();
12484
12636
  /** The shared crop extractor (native-res first, detection-frame fallback),
12485
12637
  * captured in the constructor so `processFrame` can crop object thumbnails in
12486
12638
  * the same live-frame window as the face/plate/event-media captures. The
@@ -12539,7 +12691,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12539
12691
  let storage = this.ctx.kernel.storage;
12540
12692
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
12541
12693
  if (mediaRoot) {
12542
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-C4bKtLou.js"));
12694
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DWX3J7jh.js"));
12543
12695
  storage = new FilesystemStorageProvider(mediaRoot);
12544
12696
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
12545
12697
  }
@@ -12972,6 +13124,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12972
13124
  this.bindingCache?.onBindingsChanged(data);
12973
13125
  if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
12974
13126
  this.trackStore?.clearDevice(data.deviceId);
13127
+ this.clearClosureStateForDevice(data.deviceId);
12975
13128
  this.stationaryRegistry?.forgetDevice(data.deviceId);
12976
13129
  this.overlayState.clearDevice(data.deviceId);
12977
13130
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -12990,6 +13143,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12990
13143
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.DeviceUnregistered }, (ev) => {
12991
13144
  const { deviceId } = ev.data;
12992
13145
  this.trackStore?.clearDevice(deviceId);
13146
+ this.clearClosureStateForDevice(deviceId);
12993
13147
  this.stationaryRegistry?.forgetDevice(deviceId);
12994
13148
  this.overlayState.clearDevice(deviceId);
12995
13149
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -13341,10 +13495,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13341
13495
  this.dropoutSkipsByKey.clear();
13342
13496
  this.bestFrameTracker.clear();
13343
13497
  this.lastFrameAtByTrack.clear();
13498
+ this.lastFrameInFlight.clear();
13499
+ this.firstFramePendingTracks.clear();
13500
+ this.firstFrameInFlight.clear();
13344
13501
  this.thumbnailLandedTracks.clear();
13345
13502
  this.thumbnailInFlight.clear();
13346
13503
  this.keyFrameInFlight.clear();
13347
13504
  this.trackLifecycleUpdateMem.clear();
13505
+ this.trackClosureState.clear();
13348
13506
  this.objectEmbeddingBestSelector.clear();
13349
13507
  this.levelStateByDevice.clear();
13350
13508
  this.settingsCacheByDevice.clear();
@@ -13463,6 +13621,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13463
13621
  });
13464
13622
  positionsCountById.set(t.trackId, upserted.positions.length);
13465
13623
  }
13624
+ if (result.rawTrackedDetections.length > 0) {
13625
+ const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
13626
+ for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
13627
+ }
13466
13628
  const log = this.ctx.logger.withTags({ deviceId });
13467
13629
  const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
13468
13630
  const firstFrameTargets = [];
@@ -13507,14 +13669,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13507
13669
  className: t.className,
13508
13670
  source
13509
13671
  } });
13510
- if (this.eventMediaDispatcher && frameHandle) {
13511
- firstFrameTargets.push({
13672
+ if (this.eventMediaDispatcher) {
13673
+ this.firstFramePendingTracks.add(id);
13674
+ this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13675
+ this.lastFrameAtByTrack.set(id, result.timestamp);
13676
+ if (frameHandle) firstFrameTargets.push({
13512
13677
  trackId: id,
13513
13678
  timestamp: result.timestamp,
13514
13679
  bbox: { ...t.bbox },
13515
13680
  ...t.label ? { label: t.label } : {}
13516
13681
  });
13517
- this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13518
13682
  }
13519
13683
  this.ctx.eventBus.emit({
13520
13684
  id: `pa-${(0, node_crypto.randomUUID)()}`,
@@ -13663,6 +13827,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13663
13827
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
13664
13828
  else plateCrops += 1;
13665
13829
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
13830
+ for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
13666
13831
  const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
13667
13832
  if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
13668
13833
  if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
@@ -13686,14 +13851,46 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13686
13851
  } });
13687
13852
  const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
13688
13853
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
13854
+ const firstFrameInFlightTrackIds = firstFrameTargets.map((t) => t.trackId);
13855
+ for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.add(trackId);
13856
+ const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
13857
+ for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
13858
+ const dispatchTimestamp = result.timestamp;
13859
+ const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
13860
+ for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
13861
+ for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
13862
+ const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
13863
+ const rasterFallbackCandidates = [];
13864
+ for (const t of result.tracked) {
13865
+ if (t.matchedThisFrame === false) continue;
13866
+ const closure = this.trackClosureState.get(t.trackId);
13867
+ if (!closure?.confirmed) continue;
13868
+ if (closure.rasterFallback) continue;
13869
+ if (targetedThisFrame.has(t.trackId)) continue;
13870
+ rasterFallbackWantedTrackIds.add(t.trackId);
13871
+ rasterFallbackCandidates.push({
13872
+ trackId: t.trackId,
13873
+ timestamp: result.timestamp,
13874
+ bbox: { ...t.bbox }
13875
+ });
13876
+ }
13689
13877
  this.eventMediaDispatcher.captureForFrame({
13690
13878
  deviceId,
13691
13879
  frameHandle,
13692
13880
  events: eventTargets,
13693
13881
  trackFrames: firstFrameTargets,
13694
13882
  snapshots: snapshotTargets,
13695
- cropPadding: mediaSettings.cropPadding
13883
+ cropPadding: mediaSettings.cropPadding,
13884
+ rasterFallbackWantedTrackIds,
13885
+ rasterFallbackCandidates
13696
13886
  }).then((res) => {
13887
+ for (const rf of res.rasterFallbacks) {
13888
+ const st = this.ensureTrackClosureState(deviceId, rf.trackId);
13889
+ if (!st.rasterFallback) st.rasterFallback = {
13890
+ jpeg: rf.jpeg,
13891
+ timestamp: rf.timestamp
13892
+ };
13893
+ }
13697
13894
  for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
13698
13895
  timestamp: s.timestamp,
13699
13896
  position: {
@@ -13705,8 +13902,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13705
13902
  mediaKey: s.mediaKey
13706
13903
  });
13707
13904
  for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
13905
+ for (const trackId of res.firstFrameTrackIds) this.firstFramePendingTracks.delete(trackId);
13906
+ for (const trackId of res.lastFrameTrackIds) this.lastFrameAtByTrack.set(trackId, dispatchTimestamp);
13708
13907
  }).catch(() => {}).finally(() => {
13709
13908
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.delete(trackId);
13909
+ for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.delete(trackId);
13910
+ for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.delete(trackId);
13710
13911
  });
13711
13912
  }
13712
13913
  }
@@ -13770,6 +13971,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13770
13971
  }
13771
13972
  overlayDetections = frame.detections;
13772
13973
  }
13974
+ const hasMovingTrack = result.tracked.some((t) => t.state === "moving" || t.state === "entered" || t.state === "left");
13773
13975
  this.ctx.eventBus.emit({
13774
13976
  id: `pa-${(0, node_crypto.randomUUID)()}`,
13775
13977
  timestamp: new Date(result.timestamp),
@@ -13784,7 +13986,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13784
13986
  timestamp: result.timestamp,
13785
13987
  frameWidth: result.frameWidth,
13786
13988
  frameHeight: result.frameHeight,
13787
- detections: overlayDetections
13989
+ detections: overlayDetections,
13990
+ hasMovingTrack
13788
13991
  }
13789
13992
  });
13790
13993
  }
@@ -14408,6 +14611,34 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14408
14611
  });
14409
14612
  this.emitTrackLifecycle(payload, timestamp);
14410
14613
  }
14614
+ /**
14615
+ * DEFECT A: build `firstFrame` RETRY targets for confirmed-birth tracks whose
14616
+ * birth capture never landed. A track qualifies when it is still pending
14617
+ * (`firstFramePendingTracks`), is OBSERVED this frame (`matchedThisFrame` — a
14618
+ * coasted/stale box would crop the empty scene), is NOT already targeted this
14619
+ * frame (its birth target), and has no capture in flight (RC-1). Each target is
14620
+ * stamped with the CURRENT frame timestamp so the media carries the real
14621
+ * capture instant, not the birth ts — the earliest AVAILABLE view still beats
14622
+ * no firstFrame at all.
14623
+ */
14624
+ collectFirstFrameRetries(tracked, timestamp, alreadyTargeted) {
14625
+ if (this.firstFramePendingTracks.size === 0) return [];
14626
+ const bornThisFrame = new Set(alreadyTargeted.map((t) => t.trackId));
14627
+ const retries = [];
14628
+ for (const t of tracked) {
14629
+ if (t.matchedThisFrame === false) continue;
14630
+ if (!this.firstFramePendingTracks.has(t.trackId)) continue;
14631
+ if (bornThisFrame.has(t.trackId)) continue;
14632
+ if (this.firstFrameInFlight.has(t.trackId)) continue;
14633
+ retries.push({
14634
+ trackId: t.trackId,
14635
+ timestamp,
14636
+ bbox: { ...t.bbox },
14637
+ ...t.label ? { label: t.label } : {}
14638
+ });
14639
+ }
14640
+ return retries;
14641
+ }
14411
14642
  buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
14412
14643
  const targets = [];
14413
14644
  for (const t of tracked) {
@@ -14441,19 +14672,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14441
14672
  now: timestamp,
14442
14673
  intervalMs: media.snapshotIntervalMs
14443
14674
  });
14444
- if (plan.rollingLastFrame) this.lastFrameAtByTrack.set(t.trackId, timestamp);
14675
+ const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
14445
14676
  if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
14446
14677
  const plausibleBox = isPlausibleThumbnailBox(t.bbox, frameWidth, frameHeight);
14447
14678
  const bestThumbnail = plan.bestThumbnail && plausibleBox && !this.thumbnailInFlight.has(t.trackId);
14448
14679
  const keyFrame = isNewBest && plausibleBox;
14449
- if (!plan.appendSnapshot && !plan.rollingLastFrame && !bestThumbnail && !keyFrame) continue;
14680
+ if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && !keyFrame) continue;
14450
14681
  targets.push({
14451
14682
  trackId: t.trackId,
14452
14683
  timestamp,
14453
14684
  bbox: { ...t.bbox },
14454
14685
  ...t.label ? { label: t.label } : {},
14455
14686
  appendSnapshot: plan.appendSnapshot,
14456
- rollingLastFrame: plan.rollingLastFrame,
14687
+ rollingLastFrame,
14457
14688
  bestThumbnail,
14458
14689
  keyFrame
14459
14690
  });
@@ -14704,6 +14935,44 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14704
14935
  const expired = await this.trackStore.expireStale(Date.now());
14705
14936
  for (const t of expired) {
14706
14937
  const duration = t.lastSeen - t.firstSeen;
14938
+ const closure = this.trackClosureState.get(t.trackId);
14939
+ const outcome = decideZeroMediaPolicy({
14940
+ durationMs: duration,
14941
+ hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
14942
+ confirmed: closure?.confirmed ?? false,
14943
+ hasRasterFallback: closure?.rasterFallback !== void 0
14944
+ });
14945
+ if (outcome === "suppress") {
14946
+ this.ctx.logger.info("track suppressed (zero-media false birth)", {
14947
+ tags: { deviceId: t.deviceId },
14948
+ meta: {
14949
+ trackId: t.trackId,
14950
+ className: t.className,
14951
+ durationMs: duration,
14952
+ positions: t.positions.length
14953
+ }
14954
+ });
14955
+ await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
14956
+ continue;
14957
+ }
14958
+ if (outcome === "raster-fallback" && closure?.rasterFallback) try {
14959
+ await this.mediaStore?.put({
14960
+ deviceId: t.deviceId,
14961
+ ownerKind: "track",
14962
+ ownerId: t.trackId,
14963
+ kind: "thumbnail",
14964
+ timestamp: closure.rasterFallback.timestamp,
14965
+ data: closure.rasterFallback.jpeg
14966
+ });
14967
+ } catch (err) {
14968
+ this.ctx.logger.debug("zero-media raster fallback put failed", {
14969
+ tags: { deviceId: t.deviceId },
14970
+ meta: {
14971
+ trackId: t.trackId,
14972
+ error: String(err)
14973
+ }
14974
+ });
14975
+ }
14707
14976
  this.ctx.logger.info("track ended", {
14708
14977
  tags: { deviceId: t.deviceId },
14709
14978
  meta: {
@@ -14761,6 +15030,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14761
15030
  this.bestFrameTracker.delete(t.trackId);
14762
15031
  this.objectEmbeddingBestSelector.delete(t.trackId);
14763
15032
  this.lastFrameAtByTrack.delete(t.trackId);
15033
+ this.lastFrameInFlight.delete(t.trackId);
15034
+ this.firstFramePendingTracks.delete(t.trackId);
15035
+ this.firstFrameInFlight.delete(t.trackId);
14764
15036
  this.thumbnailLandedTracks.delete(t.trackId);
14765
15037
  this.thumbnailInFlight.delete(t.trackId);
14766
15038
  this.keyFrameInFlight.delete(t.trackId);
@@ -14814,12 +15086,56 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14814
15086
  });
14815
15087
  this.emitTrackLifecycle(endPayload, t.lastSeen);
14816
15088
  this.trackLifecycleUpdateMem.delete(t.trackId);
15089
+ this.trackClosureState.delete(t.trackId);
14817
15090
  }
14818
15091
  } catch (err) {
14819
15092
  if (this.shuttingDown) return;
14820
15093
  this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
14821
15094
  }
14822
15095
  }
15096
+ /**
15097
+ * Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
15098
+ * down its live per-track state (mirroring the natural end MINUS importance
15099
+ * scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
15100
+ * just-persisted row + any events/media so nothing dangles. Best-effort.
15101
+ */
15102
+ async suppressZeroMediaTrack(deviceId, trackId) {
15103
+ const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
15104
+ const dropKeyFrame = () => {
15105
+ this.keyFrameKeyByTrackId.delete(trackId);
15106
+ };
15107
+ if (faceEnd) faceEnd.finally(dropKeyFrame);
15108
+ else dropKeyFrame();
15109
+ this.plateRecognizer?.onTrackEnd(deviceId, trackId);
15110
+ this.bestFrameTracker.delete(trackId);
15111
+ this.objectEmbeddingBestSelector.delete(trackId);
15112
+ this.lastFrameAtByTrack.delete(trackId);
15113
+ this.lastFrameInFlight.delete(trackId);
15114
+ this.firstFramePendingTracks.delete(trackId);
15115
+ this.firstFrameInFlight.delete(trackId);
15116
+ this.thumbnailLandedTracks.delete(trackId);
15117
+ this.thumbnailInFlight.delete(trackId);
15118
+ this.keyFrameInFlight.delete(trackId);
15119
+ this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15120
+ this.overlayState.onTrackEnded(deviceId, trackId);
15121
+ this.trackLifecycleUpdateMem.delete(trackId);
15122
+ this.trackClosureState.delete(trackId);
15123
+ if (this.eventStore && this.mediaStore && this.trackStore) try {
15124
+ await cascadeDeleteTrack({
15125
+ eventStore: this.eventStore,
15126
+ mediaStore: this.mediaStore,
15127
+ trackStore: this.trackStore
15128
+ }, trackId);
15129
+ } catch (err) {
15130
+ this.ctx.logger.debug("zero-media suppression cascade failed", {
15131
+ tags: { deviceId },
15132
+ meta: {
15133
+ trackId,
15134
+ error: String(err)
15135
+ }
15136
+ });
15137
+ }
15138
+ }
14823
15139
  async sweepRetention() {
14824
15140
  if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
14825
15141
  const now = Date.now();
@@ -15025,10 +15341,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15025
15341
  this.bestFrameTracker.delete(track.trackId);
15026
15342
  this.objectEmbeddingBestSelector.delete(track.trackId);
15027
15343
  this.lastFrameAtByTrack.delete(track.trackId);
15344
+ this.lastFrameInFlight.delete(track.trackId);
15345
+ this.firstFramePendingTracks.delete(track.trackId);
15346
+ this.firstFrameInFlight.delete(track.trackId);
15028
15347
  this.thumbnailLandedTracks.delete(track.trackId);
15029
15348
  this.thumbnailInFlight.delete(track.trackId);
15030
15349
  this.keyFrameInFlight.delete(track.trackId);
15031
15350
  this.trackLifecycleUpdateMem.delete(track.trackId);
15351
+ this.trackClosureState.delete(track.trackId);
15032
15352
  this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
15033
15353
  this.overlayState.onTrackEnded(deviceId, track.trackId);
15034
15354
  this.lastActiveTrackIds.get(key)?.delete(track.trackId);
@@ -15327,6 +15647,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15327
15647
  }
15328
15648
  async clearTracks(input) {
15329
15649
  this.trackStore?.clearDevice(input.deviceId);
15650
+ this.clearClosureStateForDevice(input.deviceId);
15330
15651
  this.stationaryRegistry?.clearDevice(input.deviceId);
15331
15652
  this.overlayState.clearDevice(input.deviceId);
15332
15653
  this.overlaySynthesisWarnAt.delete(input.deviceId);
@@ -15382,7 +15703,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15382
15703
  return input.kind ? all.filter((m) => m.kind === input.kind) : all;
15383
15704
  }
15384
15705
  async getTrackMedia(input) {
15385
- return this.mediaStore?.listByOwner("track", input.trackId) ?? [];
15706
+ const all = await (this.mediaStore?.listByOwner("track", input.trackId) ?? Promise.resolve([]));
15707
+ const kinds = input.kinds;
15708
+ return kinds && kinds.length > 0 ? all.filter((m) => kinds.includes(m.kind)) : all;
15386
15709
  }
15387
15710
  /**
15388
15711
  * Search object events by text using CLIP cosine similarity.
@@ -15539,6 +15862,27 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15539
15862
  clearLiveTrackState(deviceId, trackId) {
15540
15863
  this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15541
15864
  this.overlayState.onTrackEnded(deviceId, trackId);
15865
+ this.trackClosureState.delete(trackId);
15866
+ }
15867
+ /** Lazily create + return a track's close-policy state (zero-media policy). */
15868
+ ensureTrackClosureState(deviceId, trackId) {
15869
+ let st = this.trackClosureState.get(trackId);
15870
+ if (!st) {
15871
+ st = {
15872
+ deviceId,
15873
+ confirmed: false
15874
+ };
15875
+ this.trackClosureState.set(trackId, st);
15876
+ }
15877
+ return st;
15878
+ }
15879
+ /** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
15880
+ markTrackConfirmed(deviceId, trackId) {
15881
+ this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
15882
+ }
15883
+ /** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
15884
+ clearClosureStateForDevice(deviceId) {
15885
+ for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
15542
15886
  }
15543
15887
  async deleteTracks(input) {
15544
15888
  const cascade = this.buildTrackCascadeRegistry();
@@ -15833,11 +16177,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15833
16177
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
15834
16178
  if (preferKind !== void 0 && preferKind.length > 0) {
15835
16179
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15836
- const clean = pickCleanMedia([...eventFiles, ...trackFiles], preferKind);
15837
- if (!clean) return null;
16180
+ const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
16181
+ if (!chosen) return null;
15838
16182
  return {
15839
- bytes: Buffer.from(clean.base64, "base64"),
15840
- key: clean.key
16183
+ bytes: Buffer.from(chosen.base64, "base64"),
16184
+ key: chosen.key
15841
16185
  };
15842
16186
  }
15843
16187
  const chosen = await resolveDefaultEventMedia({