@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.
@@ -1,4 +1,4 @@
1
- import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-gQ5DHTYd.mjs";
1
+ import { C as hydrateSchema, D as object, E as number, O as string, S as createEvent, T as boolean, _ as videoclipsCapability, a as OpsLogEntrySchema, b as BaseAddon, c as buildEventKindDescriptor, d as faceGalleryCapability, g as subKindsOf, h as plateGalleryCapability, i as MACRO_LABELS, k as EventCategory, l as cosineSimilarity, m as pipelineAnalyticsCapability, n as EVENT_KIND_BY_CAP, o as addonWidgetsSourceCapability, p as nodePin, r as EVENT_PAD_MS, s as audioMetricsCapability, t as DEFAULT_EVENT_COLOR, v as zoneAnalyticsCapability, w as array, x as DeviceType, y as errMsg } from "../dist-Db0CsDGK.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import sharp from "sharp";
4
4
  //#region src/pipeline-analytics/videoclips-provider.ts
@@ -3355,6 +3355,24 @@ async function cascadeDeleteTracks(registry, trackStore, trackIds) {
3355
3355
  for (const trackId of trackIds) await trackStore.deletePersisted(trackId);
3356
3356
  }
3357
3357
  /**
3358
+ * Full whole-track cascade for ONE track: events → their media + track media →
3359
+ * track row. Delegates to the generalized `cascadeDeleteTracks` so the shipped
3360
+ * `deleteTracks` provider path flows through the same engine while preserving
3361
+ * the exact legacy call order (event ids stay local to the delete). Adapts the
3362
+ * narrow legacy store shapes onto `TrackScopedStore` — a superset store (faces /
3363
+ * embeddings) is opted in by the CALLER's registry, not by this legacy shim.
3364
+ */
3365
+ async function cascadeDeleteTrack(stores, trackId) {
3366
+ await cascadeDeleteTracks([{ deleteByTracks: async (ids) => {
3367
+ for (const id of ids) {
3368
+ const eventIds = await stores.eventStore.deleteByTrack(id);
3369
+ if (eventIds.length > 0) await stores.mediaStore.deleteForEvents([...eventIds]);
3370
+ }
3371
+ } }, { deleteByTracks: async (ids) => {
3372
+ await stores.mediaStore.deleteForTracks([...ids]);
3373
+ } }], stores.trackStore, [trackId]);
3374
+ }
3375
+ /**
3358
3376
  * Widened batch whole-track deletion — the ONE engine behind the three §5 entry
3359
3377
  * points. Runs the FULL registry cascade per track (via `cascadeDeleteTracks`
3360
3378
  * with a single-id list so the track root is deleted last), fires cleanup on
@@ -6473,6 +6491,36 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
6473
6491
  };
6474
6492
  }
6475
6493
  //#endregion
6494
+ //#region src/shared/frame/crop-extractor.ts
6495
+ /**
6496
+ * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
6497
+ * Coordinates are clamped to frame bounds to avoid out-of-range errors.
6498
+ */
6499
+ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
6500
+ const rawLeft = Math.round(bbox.x * frameWidth);
6501
+ const rawTop = Math.round(bbox.y * frameHeight);
6502
+ const rawWidth = Math.round(bbox.w * frameWidth);
6503
+ const rawHeight = Math.round(bbox.h * frameHeight);
6504
+ const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
6505
+ const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
6506
+ const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
6507
+ const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
6508
+ return {
6509
+ crop: await sharp(frameData, { raw: {
6510
+ width: frameWidth,
6511
+ height: frameHeight,
6512
+ channels: 3
6513
+ } }).extract({
6514
+ left,
6515
+ top,
6516
+ width,
6517
+ height
6518
+ }).jpeg({ quality: 90 }).toBuffer(),
6519
+ width,
6520
+ height
6521
+ };
6522
+ }
6523
+ //#endregion
6476
6524
  //#region src/shared/frame/square-subject-crop.ts
6477
6525
  /** Expansion factor applied to the bbox long side to frame the subject with a
6478
6526
  * little breathing room (operator choice: ×1.2). */
@@ -6711,9 +6759,13 @@ var EventMediaDispatcher = class {
6711
6759
  const snapshots = input.snapshots ?? [];
6712
6760
  const empty = {
6713
6761
  storedSnapshots: [],
6714
- thumbnailTrackIds: []
6762
+ thumbnailTrackIds: [],
6763
+ firstFrameTrackIds: [],
6764
+ lastFrameTrackIds: [],
6765
+ rasterFallbacks: []
6715
6766
  };
6716
- if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
6767
+ const extraCandidateCount = input.rasterFallbackCandidates?.length ?? 0;
6768
+ if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0 && extraCandidateCount === 0) return empty;
6717
6769
  let decoded;
6718
6770
  try {
6719
6771
  decoded = await resolveFrame(frameHandle, { getRemoteFrame: this.deps.getRemoteFrame });
@@ -6763,21 +6815,82 @@ var EventMediaDispatcher = class {
6763
6815
  });
6764
6816
  return empty;
6765
6817
  }
6818
+ const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds, input.rasterFallbackCandidates);
6766
6819
  for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6767
- for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6820
+ const firstFrameTrackIds = [];
6821
+ for (const tf of trackFrames) if (await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf)) firstFrameTrackIds.push(tf.trackId);
6768
6822
  const storedSnapshots = [];
6769
6823
  const thumbnailTrackIds = [];
6824
+ const lastFrameTrackIds = [];
6770
6825
  for (const sn of snapshots) {
6771
6826
  const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
6772
6827
  if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
6773
6828
  if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
6829
+ if (res.lastFrameWritten) lastFrameTrackIds.push(sn.trackId);
6774
6830
  }
6775
6831
  return {
6776
6832
  storedSnapshots,
6777
- thumbnailTrackIds
6833
+ thumbnailTrackIds,
6834
+ firstFrameTrackIds,
6835
+ lastFrameTrackIds,
6836
+ rasterFallbacks
6778
6837
  };
6779
6838
  }
6780
6839
  /**
6840
+ * Cut ONE clean detection-raster subject crop per WANTED track observed this
6841
+ * frame — the "first available detection-raster frame" of the zero-media
6842
+ * fallback. Candidate bboxes come from this frame's firstFrame/snapshot targets
6843
+ * AND (widened) the explicit `extraCandidates` (confirmed tracks with no such
6844
+ * target). Cropped from the already-resolved `frameData` at its REAL resolution
6845
+ * via {@link extractCrop} (extract-only — NEVER upscaled). Deduped per trackId
6846
+ * (first candidate wins; target-derived candidates precede the extras). A
6847
+ * per-track encode failure is skipped (logged) — a missing fallback simply
6848
+ * leaves the track with no last-resort preview, never an error.
6849
+ */
6850
+ async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted, extraCandidates) {
6851
+ if (!wanted || wanted.size === 0) return [];
6852
+ const seen = /* @__PURE__ */ new Set();
6853
+ const out = [];
6854
+ const candidates = [
6855
+ ...trackFrames.map((t) => ({
6856
+ trackId: t.trackId,
6857
+ timestamp: t.timestamp,
6858
+ bbox: t.bbox
6859
+ })),
6860
+ ...snapshots.map((s) => ({
6861
+ trackId: s.trackId,
6862
+ timestamp: s.timestamp,
6863
+ bbox: s.bbox
6864
+ })),
6865
+ ...(extraCandidates ?? []).map((c) => ({
6866
+ trackId: c.trackId,
6867
+ timestamp: c.timestamp,
6868
+ bbox: c.bbox
6869
+ }))
6870
+ ];
6871
+ for (const c of candidates) {
6872
+ if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
6873
+ seen.add(c.trackId);
6874
+ try {
6875
+ const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
6876
+ W: fw,
6877
+ H: fh
6878
+ }, cropPadding));
6879
+ out.push({
6880
+ trackId: c.trackId,
6881
+ timestamp: c.timestamp,
6882
+ jpeg: crop
6883
+ });
6884
+ } catch (err) {
6885
+ this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
6886
+ trackId: c.trackId,
6887
+ error: err instanceof Error ? err.message : String(err)
6888
+ } });
6889
+ }
6890
+ }
6891
+ return out;
6892
+ }
6893
+ /**
6781
6894
  * Periodic per-track media (§5). The boxed FULL frame is encoded once and
6782
6895
  * shared by the appended `snapshot` (timeline filmstrip) and the rolling
6783
6896
  * `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
@@ -6788,11 +6901,15 @@ var EventMediaDispatcher = class {
6788
6901
  * TrackStore wiring (null when `appendSnapshot` is false or the encode
6789
6902
  * failed). `thumbnailWritten` reports whether a best `thumbnail` actually
6790
6903
  * landed this frame (#27-A) so the caller can stop forcing retries.
6904
+ * `lastFrameWritten` reports whether the rolling `lastFrame` actually landed
6905
+ * (DEFECT B) so the caller advances its `lastFrameAt` clock ONLY on a real
6906
+ * write — a dropped roll leaves the clock put and retries next frame.
6791
6907
  */
6792
6908
  async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
6793
6909
  if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
6794
6910
  storedSnapshot: null,
6795
- thumbnailWritten: false
6911
+ thumbnailWritten: false,
6912
+ lastFrameWritten: false
6796
6913
  };
6797
6914
  let boxed = null;
6798
6915
  if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
@@ -6813,7 +6930,8 @@ var EventMediaDispatcher = class {
6813
6930
  bbox: sn.bbox
6814
6931
  };
6815
6932
  } catch {}
6816
- if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6933
+ let lastFrameWritten = false;
6934
+ if (sn.rollingLastFrame && boxed) lastFrameWritten = await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6817
6935
  let thumbnailWritten = false;
6818
6936
  if (sn.bestThumbnail) {
6819
6937
  const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
@@ -6824,7 +6942,8 @@ var EventMediaDispatcher = class {
6824
6942
  }
6825
6943
  return {
6826
6944
  storedSnapshot: stored,
6827
- thumbnailWritten
6945
+ thumbnailWritten,
6946
+ lastFrameWritten
6828
6947
  };
6829
6948
  }
6830
6949
  /**
@@ -7021,7 +7140,7 @@ var EventMediaDispatcher = class {
7021
7140
  }
7022
7141
  async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
7023
7142
  const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
7024
- if (!boxed) return;
7143
+ if (!boxed) return false;
7025
7144
  try {
7026
7145
  await this.deps.mediaStore.put({
7027
7146
  deviceId,
@@ -7031,6 +7150,7 @@ var EventMediaDispatcher = class {
7031
7150
  timestamp: tf.timestamp,
7032
7151
  data: boxed
7033
7152
  });
7153
+ return true;
7034
7154
  } catch (err) {
7035
7155
  this.deps.logger.warn("event media: track frame failed", {
7036
7156
  tags: { deviceId },
@@ -7040,40 +7160,11 @@ var EventMediaDispatcher = class {
7040
7160
  error: err instanceof Error ? err.message : String(err)
7041
7161
  }
7042
7162
  });
7163
+ return false;
7043
7164
  }
7044
7165
  }
7045
7166
  };
7046
7167
  //#endregion
7047
- //#region src/shared/frame/crop-extractor.ts
7048
- /**
7049
- * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
7050
- * Coordinates are clamped to frame bounds to avoid out-of-range errors.
7051
- */
7052
- async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
7053
- const rawLeft = Math.round(bbox.x * frameWidth);
7054
- const rawTop = Math.round(bbox.y * frameHeight);
7055
- const rawWidth = Math.round(bbox.w * frameWidth);
7056
- const rawHeight = Math.round(bbox.h * frameHeight);
7057
- const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
7058
- const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
7059
- const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
7060
- const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
7061
- return {
7062
- crop: await sharp(frameData, { raw: {
7063
- width: frameWidth,
7064
- height: frameHeight,
7065
- channels: 3
7066
- } }).extract({
7067
- left,
7068
- top,
7069
- width,
7070
- height
7071
- }).jpeg({ quality: 90 }).toBuffer(),
7072
- width,
7073
- height
7074
- };
7075
- }
7076
- //#endregion
7077
7168
  //#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
7078
7169
  function selectCropBbox(detection) {
7079
7170
  return detection.refinedBbox ?? detection.bbox;
@@ -8862,6 +8953,16 @@ function planPeriodicMedia(input) {
8862
8953
  bestThumbnail: input.isNewBest || !thumbnailLanded
8863
8954
  };
8864
8955
  }
8956
+ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
8957
+ /**
8958
+ * Classify a closing track's persistence outcome. Pure — see the module header
8959
+ * for the full contract.
8960
+ */
8961
+ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
8962
+ if (input.hasMedia) return "persist";
8963
+ if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
8964
+ return input.hasRasterFallback ? "raster-fallback" : "persist";
8965
+ }
8865
8966
  //#endregion
8866
8967
  //#region src/pipeline-analytics/best-thumbnail-guard.ts
8867
8968
  /**
@@ -12121,6 +12222,23 @@ function pickTrackFallbackMedia(files) {
12121
12222
  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];
12122
12223
  }
12123
12224
  /**
12225
+ * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
12226
+ * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
12227
+ * frame at all — the regression class created by retiring `crop`/`fullFrame`
12228
+ * production (2026-07-21), leaving degenerate tracks with only boxed
12229
+ * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
12230
+ * returning undefined. A boxed frame (with the target's box drawn) is a real
12231
+ * preview; an empty tile is not. Clean is always tried FIRST so the common case
12232
+ * still serves an unboxed tile. Undefined only when the owner has NO media.
12233
+ *
12234
+ * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
12235
+ * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
12236
+ * composition pure + unit-testable here.
12237
+ */
12238
+ function resolveCropForcedMedia(files, pickClean) {
12239
+ return pickClean(files) ?? pickTrackFallbackMedia(files);
12240
+ }
12241
+ /**
12124
12242
  * Resolve the default-path media for an event id: the event's own media when it
12125
12243
  * has any, else the owning track's fallback media. Returns `null` when neither
12126
12244
  * yields a blob (caller 404s → the surface shows an icon).
@@ -12452,6 +12570,32 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12452
12570
  * where no `snapshot` is appended, so it is never byte-identical to a stored
12453
12571
  * `snapshot` (kills the end-of-track duplicate). Cleared on track end. */
12454
12572
  lastFrameAtByTrack = /* @__PURE__ */ new Map();
12573
+ /** Tracks with a rolling-`lastFrame` capture CURRENTLY in flight (RC-1,
12574
+ * DEFECT B). The `lastFrameAt` clock now advances ONLY when the write lands
12575
+ * (dispatcher completion), not synchronously at plan time — so a dropped roll
12576
+ * (recycled/blank frame) leaves the clock put and re-rolls next frame. Without
12577
+ * an in-flight guard that re-roll would fire EVERY frame while the first
12578
+ * capture is still resolving (0.1–3s under the native path), stacking N
12579
+ * overlapping captures. While a track sits here `buildSnapshotTargets`
12580
+ * suppresses a new rolling-`lastFrame` request; the pending dispatch clears it
12581
+ * (and, if it landed, advances `lastFrameAtByTrack`). Cleared on track end +
12582
+ * reset. */
12583
+ lastFrameInFlight = /* @__PURE__ */ new Set();
12584
+ /** Confirmed-birth tracks whose `firstFrame` has NOT yet actually persisted
12585
+ * (DEFECT A). Seeded on a confirmed birth (alongside the birth firstFrame
12586
+ * target) and removed the moment the write lands. While a track sits here and
12587
+ * is matched this frame, `collectFirstFrameRetries` re-schedules a firstFrame
12588
+ * target so a birth capture dropped by a recycled/blank live frame is retried
12589
+ * on a later frame (earliest available view still beats none). Scoped to
12590
+ * confirmed births ONLY — a suppressed false-positive birth or resurrection
12591
+ * never enters, so it never gets a retro firstFrame. Cleared on track end +
12592
+ * reset. */
12593
+ firstFramePendingTracks = /* @__PURE__ */ new Set();
12594
+ /** Tracks with a `firstFrame` capture CURRENTLY in flight (RC-1, DEFECT A).
12595
+ * Mirrors `thumbnailInFlight`: while a firstFrame capture is resolving the
12596
+ * per-frame retry is suppressed so at most one capture is outstanding per
12597
+ * track. Cleared on dispatch settle (+ track end / reset). */
12598
+ firstFrameInFlight = /* @__PURE__ */ new Set();
12455
12599
  /** Track ids whose best `thumbnail` has ACTUALLY been persisted (#27-A). A
12456
12600
  * track absent here keeps forcing a best-thumbnail capture every frame until
12457
12601
  * one lands, so a short / high-churn track whose first capture was dropped
@@ -12476,6 +12620,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12476
12620
  * area) + emit time, so a material improvement is measured against the
12477
12621
  * last emit and debounced. Seeded at `start`, dropped at `end`. */
12478
12622
  trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
12623
+ /** Per-active-track close-policy state (spec §"Zero-media track policy"):
12624
+ * whether the track ever reached the tracker's hit-confirmation
12625
+ * (`trackAge >= minHits`) and the FIRST retained detection-raster subject
12626
+ * crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
12627
+ * suppress / raster-fallback / persist for a track that ends with no media.
12628
+ * `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
12629
+ * on every teardown path. */
12630
+ trackClosureState = /* @__PURE__ */ new Map();
12479
12631
  /** The shared crop extractor (native-res first, detection-frame fallback),
12480
12632
  * captured in the constructor so `processFrame` can crop object thumbnails in
12481
12633
  * the same live-frame window as the face/plate/event-media captures. The
@@ -12967,6 +13119,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12967
13119
  this.bindingCache?.onBindingsChanged(data);
12968
13120
  if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
12969
13121
  this.trackStore?.clearDevice(data.deviceId);
13122
+ this.clearClosureStateForDevice(data.deviceId);
12970
13123
  this.stationaryRegistry?.forgetDevice(data.deviceId);
12971
13124
  this.overlayState.clearDevice(data.deviceId);
12972
13125
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -12985,6 +13138,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12985
13138
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (ev) => {
12986
13139
  const { deviceId } = ev.data;
12987
13140
  this.trackStore?.clearDevice(deviceId);
13141
+ this.clearClosureStateForDevice(deviceId);
12988
13142
  this.stationaryRegistry?.forgetDevice(deviceId);
12989
13143
  this.overlayState.clearDevice(deviceId);
12990
13144
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -13336,10 +13490,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13336
13490
  this.dropoutSkipsByKey.clear();
13337
13491
  this.bestFrameTracker.clear();
13338
13492
  this.lastFrameAtByTrack.clear();
13493
+ this.lastFrameInFlight.clear();
13494
+ this.firstFramePendingTracks.clear();
13495
+ this.firstFrameInFlight.clear();
13339
13496
  this.thumbnailLandedTracks.clear();
13340
13497
  this.thumbnailInFlight.clear();
13341
13498
  this.keyFrameInFlight.clear();
13342
13499
  this.trackLifecycleUpdateMem.clear();
13500
+ this.trackClosureState.clear();
13343
13501
  this.objectEmbeddingBestSelector.clear();
13344
13502
  this.levelStateByDevice.clear();
13345
13503
  this.settingsCacheByDevice.clear();
@@ -13458,6 +13616,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13458
13616
  });
13459
13617
  positionsCountById.set(t.trackId, upserted.positions.length);
13460
13618
  }
13619
+ if (result.rawTrackedDetections.length > 0) {
13620
+ const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
13621
+ for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
13622
+ }
13461
13623
  const log = this.ctx.logger.withTags({ deviceId });
13462
13624
  const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
13463
13625
  const firstFrameTargets = [];
@@ -13502,14 +13664,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13502
13664
  className: t.className,
13503
13665
  source
13504
13666
  } });
13505
- if (this.eventMediaDispatcher && frameHandle) {
13506
- firstFrameTargets.push({
13667
+ if (this.eventMediaDispatcher) {
13668
+ this.firstFramePendingTracks.add(id);
13669
+ this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13670
+ this.lastFrameAtByTrack.set(id, result.timestamp);
13671
+ if (frameHandle) firstFrameTargets.push({
13507
13672
  trackId: id,
13508
13673
  timestamp: result.timestamp,
13509
13674
  bbox: { ...t.bbox },
13510
13675
  ...t.label ? { label: t.label } : {}
13511
13676
  });
13512
- this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13513
13677
  }
13514
13678
  this.ctx.eventBus.emit({
13515
13679
  id: `pa-${randomUUID()}`,
@@ -13658,6 +13822,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13658
13822
  for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
13659
13823
  else plateCrops += 1;
13660
13824
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
13825
+ for (const retry of this.collectFirstFrameRetries(result.tracked, result.timestamp, firstFrameTargets)) firstFrameTargets.push(retry);
13661
13826
  const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
13662
13827
  if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
13663
13828
  if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
@@ -13681,14 +13846,46 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13681
13846
  } });
13682
13847
  const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
13683
13848
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
13849
+ const firstFrameInFlightTrackIds = firstFrameTargets.map((t) => t.trackId);
13850
+ for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.add(trackId);
13851
+ const lastFrameInFlightTrackIds = snapshotTargets.filter((t) => t.rollingLastFrame).map((t) => t.trackId);
13852
+ for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.add(trackId);
13853
+ const dispatchTimestamp = result.timestamp;
13854
+ const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
13855
+ for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
13856
+ for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
13857
+ const targetedThisFrame = new Set([...firstFrameTargets.map((t) => t.trackId), ...snapshotTargets.map((s) => s.trackId)]);
13858
+ const rasterFallbackCandidates = [];
13859
+ for (const t of result.tracked) {
13860
+ if (t.matchedThisFrame === false) continue;
13861
+ const closure = this.trackClosureState.get(t.trackId);
13862
+ if (!closure?.confirmed) continue;
13863
+ if (closure.rasterFallback) continue;
13864
+ if (targetedThisFrame.has(t.trackId)) continue;
13865
+ rasterFallbackWantedTrackIds.add(t.trackId);
13866
+ rasterFallbackCandidates.push({
13867
+ trackId: t.trackId,
13868
+ timestamp: result.timestamp,
13869
+ bbox: { ...t.bbox }
13870
+ });
13871
+ }
13684
13872
  this.eventMediaDispatcher.captureForFrame({
13685
13873
  deviceId,
13686
13874
  frameHandle,
13687
13875
  events: eventTargets,
13688
13876
  trackFrames: firstFrameTargets,
13689
13877
  snapshots: snapshotTargets,
13690
- cropPadding: mediaSettings.cropPadding
13878
+ cropPadding: mediaSettings.cropPadding,
13879
+ rasterFallbackWantedTrackIds,
13880
+ rasterFallbackCandidates
13691
13881
  }).then((res) => {
13882
+ for (const rf of res.rasterFallbacks) {
13883
+ const st = this.ensureTrackClosureState(deviceId, rf.trackId);
13884
+ if (!st.rasterFallback) st.rasterFallback = {
13885
+ jpeg: rf.jpeg,
13886
+ timestamp: rf.timestamp
13887
+ };
13888
+ }
13692
13889
  for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
13693
13890
  timestamp: s.timestamp,
13694
13891
  position: {
@@ -13700,8 +13897,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13700
13897
  mediaKey: s.mediaKey
13701
13898
  });
13702
13899
  for (const trackId of res.thumbnailTrackIds) this.thumbnailLandedTracks.add(trackId);
13900
+ for (const trackId of res.firstFrameTrackIds) this.firstFramePendingTracks.delete(trackId);
13901
+ for (const trackId of res.lastFrameTrackIds) this.lastFrameAtByTrack.set(trackId, dispatchTimestamp);
13703
13902
  }).catch(() => {}).finally(() => {
13704
13903
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.delete(trackId);
13904
+ for (const trackId of firstFrameInFlightTrackIds) this.firstFrameInFlight.delete(trackId);
13905
+ for (const trackId of lastFrameInFlightTrackIds) this.lastFrameInFlight.delete(trackId);
13705
13906
  });
13706
13907
  }
13707
13908
  }
@@ -13765,6 +13966,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13765
13966
  }
13766
13967
  overlayDetections = frame.detections;
13767
13968
  }
13969
+ const hasMovingTrack = result.tracked.some((t) => t.state === "moving" || t.state === "entered" || t.state === "left");
13768
13970
  this.ctx.eventBus.emit({
13769
13971
  id: `pa-${randomUUID()}`,
13770
13972
  timestamp: new Date(result.timestamp),
@@ -13779,7 +13981,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13779
13981
  timestamp: result.timestamp,
13780
13982
  frameWidth: result.frameWidth,
13781
13983
  frameHeight: result.frameHeight,
13782
- detections: overlayDetections
13984
+ detections: overlayDetections,
13985
+ hasMovingTrack
13783
13986
  }
13784
13987
  });
13785
13988
  }
@@ -14403,6 +14606,34 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14403
14606
  });
14404
14607
  this.emitTrackLifecycle(payload, timestamp);
14405
14608
  }
14609
+ /**
14610
+ * DEFECT A: build `firstFrame` RETRY targets for confirmed-birth tracks whose
14611
+ * birth capture never landed. A track qualifies when it is still pending
14612
+ * (`firstFramePendingTracks`), is OBSERVED this frame (`matchedThisFrame` — a
14613
+ * coasted/stale box would crop the empty scene), is NOT already targeted this
14614
+ * frame (its birth target), and has no capture in flight (RC-1). Each target is
14615
+ * stamped with the CURRENT frame timestamp so the media carries the real
14616
+ * capture instant, not the birth ts — the earliest AVAILABLE view still beats
14617
+ * no firstFrame at all.
14618
+ */
14619
+ collectFirstFrameRetries(tracked, timestamp, alreadyTargeted) {
14620
+ if (this.firstFramePendingTracks.size === 0) return [];
14621
+ const bornThisFrame = new Set(alreadyTargeted.map((t) => t.trackId));
14622
+ const retries = [];
14623
+ for (const t of tracked) {
14624
+ if (t.matchedThisFrame === false) continue;
14625
+ if (!this.firstFramePendingTracks.has(t.trackId)) continue;
14626
+ if (bornThisFrame.has(t.trackId)) continue;
14627
+ if (this.firstFrameInFlight.has(t.trackId)) continue;
14628
+ retries.push({
14629
+ trackId: t.trackId,
14630
+ timestamp,
14631
+ bbox: { ...t.bbox },
14632
+ ...t.label ? { label: t.label } : {}
14633
+ });
14634
+ }
14635
+ return retries;
14636
+ }
14406
14637
  buildSnapshotTargets(deviceId, tracked, timestamp, media, frameWidth, frameHeight) {
14407
14638
  const targets = [];
14408
14639
  for (const t of tracked) {
@@ -14436,19 +14667,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14436
14667
  now: timestamp,
14437
14668
  intervalMs: media.snapshotIntervalMs
14438
14669
  });
14439
- if (plan.rollingLastFrame) this.lastFrameAtByTrack.set(t.trackId, timestamp);
14670
+ const rollingLastFrame = plan.rollingLastFrame && !this.lastFrameInFlight.has(t.trackId);
14440
14671
  if (plan.appendSnapshot) this.trackStore.markSnapshotPending(t.trackId, timestamp, t.bbox);
14441
14672
  const plausibleBox = isPlausibleThumbnailBox(t.bbox, frameWidth, frameHeight);
14442
14673
  const bestThumbnail = plan.bestThumbnail && plausibleBox && !this.thumbnailInFlight.has(t.trackId);
14443
14674
  const keyFrame = isNewBest && plausibleBox;
14444
- if (!plan.appendSnapshot && !plan.rollingLastFrame && !bestThumbnail && !keyFrame) continue;
14675
+ if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && !keyFrame) continue;
14445
14676
  targets.push({
14446
14677
  trackId: t.trackId,
14447
14678
  timestamp,
14448
14679
  bbox: { ...t.bbox },
14449
14680
  ...t.label ? { label: t.label } : {},
14450
14681
  appendSnapshot: plan.appendSnapshot,
14451
- rollingLastFrame: plan.rollingLastFrame,
14682
+ rollingLastFrame,
14452
14683
  bestThumbnail,
14453
14684
  keyFrame
14454
14685
  });
@@ -14699,6 +14930,44 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14699
14930
  const expired = await this.trackStore.expireStale(Date.now());
14700
14931
  for (const t of expired) {
14701
14932
  const duration = t.lastSeen - t.firstSeen;
14933
+ const closure = this.trackClosureState.get(t.trackId);
14934
+ const outcome = decideZeroMediaPolicy({
14935
+ durationMs: duration,
14936
+ hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
14937
+ confirmed: closure?.confirmed ?? false,
14938
+ hasRasterFallback: closure?.rasterFallback !== void 0
14939
+ });
14940
+ if (outcome === "suppress") {
14941
+ this.ctx.logger.info("track suppressed (zero-media false birth)", {
14942
+ tags: { deviceId: t.deviceId },
14943
+ meta: {
14944
+ trackId: t.trackId,
14945
+ className: t.className,
14946
+ durationMs: duration,
14947
+ positions: t.positions.length
14948
+ }
14949
+ });
14950
+ await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
14951
+ continue;
14952
+ }
14953
+ if (outcome === "raster-fallback" && closure?.rasterFallback) try {
14954
+ await this.mediaStore?.put({
14955
+ deviceId: t.deviceId,
14956
+ ownerKind: "track",
14957
+ ownerId: t.trackId,
14958
+ kind: "thumbnail",
14959
+ timestamp: closure.rasterFallback.timestamp,
14960
+ data: closure.rasterFallback.jpeg
14961
+ });
14962
+ } catch (err) {
14963
+ this.ctx.logger.debug("zero-media raster fallback put failed", {
14964
+ tags: { deviceId: t.deviceId },
14965
+ meta: {
14966
+ trackId: t.trackId,
14967
+ error: String(err)
14968
+ }
14969
+ });
14970
+ }
14702
14971
  this.ctx.logger.info("track ended", {
14703
14972
  tags: { deviceId: t.deviceId },
14704
14973
  meta: {
@@ -14756,6 +15025,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14756
15025
  this.bestFrameTracker.delete(t.trackId);
14757
15026
  this.objectEmbeddingBestSelector.delete(t.trackId);
14758
15027
  this.lastFrameAtByTrack.delete(t.trackId);
15028
+ this.lastFrameInFlight.delete(t.trackId);
15029
+ this.firstFramePendingTracks.delete(t.trackId);
15030
+ this.firstFrameInFlight.delete(t.trackId);
14759
15031
  this.thumbnailLandedTracks.delete(t.trackId);
14760
15032
  this.thumbnailInFlight.delete(t.trackId);
14761
15033
  this.keyFrameInFlight.delete(t.trackId);
@@ -14809,12 +15081,56 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14809
15081
  });
14810
15082
  this.emitTrackLifecycle(endPayload, t.lastSeen);
14811
15083
  this.trackLifecycleUpdateMem.delete(t.trackId);
15084
+ this.trackClosureState.delete(t.trackId);
14812
15085
  }
14813
15086
  } catch (err) {
14814
15087
  if (this.shuttingDown) return;
14815
15088
  this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
14816
15089
  }
14817
15090
  }
15091
+ /**
15092
+ * Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
15093
+ * down its live per-track state (mirroring the natural end MINUS importance
15094
+ * scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
15095
+ * just-persisted row + any events/media so nothing dangles. Best-effort.
15096
+ */
15097
+ async suppressZeroMediaTrack(deviceId, trackId) {
15098
+ const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
15099
+ const dropKeyFrame = () => {
15100
+ this.keyFrameKeyByTrackId.delete(trackId);
15101
+ };
15102
+ if (faceEnd) faceEnd.finally(dropKeyFrame);
15103
+ else dropKeyFrame();
15104
+ this.plateRecognizer?.onTrackEnd(deviceId, trackId);
15105
+ this.bestFrameTracker.delete(trackId);
15106
+ this.objectEmbeddingBestSelector.delete(trackId);
15107
+ this.lastFrameAtByTrack.delete(trackId);
15108
+ this.lastFrameInFlight.delete(trackId);
15109
+ this.firstFramePendingTracks.delete(trackId);
15110
+ this.firstFrameInFlight.delete(trackId);
15111
+ this.thumbnailLandedTracks.delete(trackId);
15112
+ this.thumbnailInFlight.delete(trackId);
15113
+ this.keyFrameInFlight.delete(trackId);
15114
+ this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15115
+ this.overlayState.onTrackEnded(deviceId, trackId);
15116
+ this.trackLifecycleUpdateMem.delete(trackId);
15117
+ this.trackClosureState.delete(trackId);
15118
+ if (this.eventStore && this.mediaStore && this.trackStore) try {
15119
+ await cascadeDeleteTrack({
15120
+ eventStore: this.eventStore,
15121
+ mediaStore: this.mediaStore,
15122
+ trackStore: this.trackStore
15123
+ }, trackId);
15124
+ } catch (err) {
15125
+ this.ctx.logger.debug("zero-media suppression cascade failed", {
15126
+ tags: { deviceId },
15127
+ meta: {
15128
+ trackId,
15129
+ error: String(err)
15130
+ }
15131
+ });
15132
+ }
15133
+ }
14818
15134
  async sweepRetention() {
14819
15135
  if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
14820
15136
  const now = Date.now();
@@ -15020,10 +15336,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15020
15336
  this.bestFrameTracker.delete(track.trackId);
15021
15337
  this.objectEmbeddingBestSelector.delete(track.trackId);
15022
15338
  this.lastFrameAtByTrack.delete(track.trackId);
15339
+ this.lastFrameInFlight.delete(track.trackId);
15340
+ this.firstFramePendingTracks.delete(track.trackId);
15341
+ this.firstFrameInFlight.delete(track.trackId);
15023
15342
  this.thumbnailLandedTracks.delete(track.trackId);
15024
15343
  this.thumbnailInFlight.delete(track.trackId);
15025
15344
  this.keyFrameInFlight.delete(track.trackId);
15026
15345
  this.trackLifecycleUpdateMem.delete(track.trackId);
15346
+ this.trackClosureState.delete(track.trackId);
15027
15347
  this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
15028
15348
  this.overlayState.onTrackEnded(deviceId, track.trackId);
15029
15349
  this.lastActiveTrackIds.get(key)?.delete(track.trackId);
@@ -15322,6 +15642,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15322
15642
  }
15323
15643
  async clearTracks(input) {
15324
15644
  this.trackStore?.clearDevice(input.deviceId);
15645
+ this.clearClosureStateForDevice(input.deviceId);
15325
15646
  this.stationaryRegistry?.clearDevice(input.deviceId);
15326
15647
  this.overlayState.clearDevice(input.deviceId);
15327
15648
  this.overlaySynthesisWarnAt.delete(input.deviceId);
@@ -15377,7 +15698,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15377
15698
  return input.kind ? all.filter((m) => m.kind === input.kind) : all;
15378
15699
  }
15379
15700
  async getTrackMedia(input) {
15380
- return this.mediaStore?.listByOwner("track", input.trackId) ?? [];
15701
+ const all = await (this.mediaStore?.listByOwner("track", input.trackId) ?? Promise.resolve([]));
15702
+ const kinds = input.kinds;
15703
+ return kinds && kinds.length > 0 ? all.filter((m) => kinds.includes(m.kind)) : all;
15381
15704
  }
15382
15705
  /**
15383
15706
  * Search object events by text using CLIP cosine similarity.
@@ -15534,6 +15857,27 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15534
15857
  clearLiveTrackState(deviceId, trackId) {
15535
15858
  this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15536
15859
  this.overlayState.onTrackEnded(deviceId, trackId);
15860
+ this.trackClosureState.delete(trackId);
15861
+ }
15862
+ /** Lazily create + return a track's close-policy state (zero-media policy). */
15863
+ ensureTrackClosureState(deviceId, trackId) {
15864
+ let st = this.trackClosureState.get(trackId);
15865
+ if (!st) {
15866
+ st = {
15867
+ deviceId,
15868
+ confirmed: false
15869
+ };
15870
+ this.trackClosureState.set(trackId, st);
15871
+ }
15872
+ return st;
15873
+ }
15874
+ /** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
15875
+ markTrackConfirmed(deviceId, trackId) {
15876
+ this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
15877
+ }
15878
+ /** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
15879
+ clearClosureStateForDevice(deviceId) {
15880
+ for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
15537
15881
  }
15538
15882
  async deleteTracks(input) {
15539
15883
  const cascade = this.buildTrackCascadeRegistry();
@@ -15828,11 +16172,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15828
16172
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
15829
16173
  if (preferKind !== void 0 && preferKind.length > 0) {
15830
16174
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15831
- const clean = pickCleanMedia([...eventFiles, ...trackFiles], preferKind);
15832
- if (!clean) return null;
16175
+ const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
16176
+ if (!chosen) return null;
15833
16177
  return {
15834
- bytes: Buffer.from(clean.base64, "base64"),
15835
- key: clean.key
16178
+ bytes: Buffer.from(chosen.base64, "base64"),
16179
+ key: chosen.key
15836
16180
  };
15837
16181
  }
15838
16182
  const chosen = await resolveDefaultEventMedia({