@camstack/addon-post-analysis 1.2.2 → 1.2.4

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-DSP-cVM0.js");
5
+ const require_dist = require("../dist-Ck79MtSp.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
@@ -4497,6 +4515,7 @@ var SINGLE_INSTANCE_KINDS = new Set([
4497
4515
  "keyFrame",
4498
4516
  "keyFrameSmall",
4499
4517
  "thumbnail",
4518
+ "thumbnailSmall",
4500
4519
  "firstFrame",
4501
4520
  "lastFrame"
4502
4521
  ]);
@@ -6477,6 +6496,106 @@ function squareSafeCropRegionNormalized(bbox, frame, padding) {
6477
6496
  };
6478
6497
  }
6479
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
6529
+ //#region src/shared/frame/square-subject-crop.ts
6530
+ /** Expansion factor applied to the bbox long side to frame the subject with a
6531
+ * little breathing room (operator choice: ×1.2). */
6532
+ var SQUARE_SUBJECT_CROP_EXPANSION = 1.2;
6533
+ /**
6534
+ * Compute a SQUARE crop region in PIXEL space around the subject bbox.
6535
+ *
6536
+ * Algorithm:
6537
+ * 1. center: cx = x + w/2, cy = y + h/2
6538
+ * 2. side = max(w, h) × SQUARE_SUBJECT_CROP_EXPANSION
6539
+ * 3. clamp side to fit the frame: side = min(side, W, H) — a square can never
6540
+ * exceed the frame's SHORT edge (this is the "subject bigger than the
6541
+ * frame's short side" case)
6542
+ * 4. center-and-clamp the origin so the square stays fully inside the frame
6543
+ * 5. round to integer pixels
6544
+ *
6545
+ * The returned region fully contains the bbox whenever the bbox itself fits in a
6546
+ * square of the frame's short side (always true for real detections).
6547
+ */
6548
+ function squareSubjectCropRegion(bbox, frame) {
6549
+ const { W, H } = frame;
6550
+ const cx = bbox.x + bbox.w / 2;
6551
+ const cy = bbox.y + bbox.h / 2;
6552
+ const longerSide = Math.max(bbox.w, bbox.h);
6553
+ const side = Math.min(longerSide * SQUARE_SUBJECT_CROP_EXPANSION, W, H);
6554
+ const rawX0 = cx - side / 2;
6555
+ const rawY0 = cy - side / 2;
6556
+ const x0 = Math.max(0, Math.min(rawX0, W - side));
6557
+ const y0 = Math.max(0, Math.min(rawY0, H - side));
6558
+ return {
6559
+ x: Math.round(x0),
6560
+ y: Math.round(y0),
6561
+ w: Math.round(side),
6562
+ h: Math.round(side)
6563
+ };
6564
+ }
6565
+ /**
6566
+ * The same SQUARE region as {@link squareSubjectCropRegion}, expressed in
6567
+ * NORMALIZED [0,1]×[0,1] coordinates. A normalized box maps DIRECTLY onto a
6568
+ * native-resolution surface of the SAME aspect ratio, so the region computed
6569
+ * from the detection-frame dimensions addresses the exact same ROI on the
6570
+ * runner's retained native frame. Reuses the pixel geometry verbatim (single
6571
+ * source of truth) and divides by the frame dimensions.
6572
+ */
6573
+ function squareSubjectCropRegionNormalized(bbox, frame) {
6574
+ const region = squareSubjectCropRegion(bbox, frame);
6575
+ return {
6576
+ x: region.x / frame.W,
6577
+ y: region.y / frame.H,
6578
+ w: region.w / frame.W,
6579
+ h: region.h / frame.H
6580
+ };
6581
+ }
6582
+ /**
6583
+ * Derive the `thumbnailSmall` JPEG from an already-encoded native subject-crop
6584
+ * JPEG. Downscales to at most {@link THUMBNAIL_SMALL_MAX_WIDTH} on the long side
6585
+ * (aspect preserved). If the native crop's long side is already ≤ the cap, the
6586
+ * ORIGINAL buffer is returned unchanged (store as-is, never upscale, no wasted
6587
+ * re-encode).
6588
+ */
6589
+ async function deriveThumbnailSmall(nativeJpeg) {
6590
+ const meta = await (0, sharp.default)(nativeJpeg).metadata();
6591
+ const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
6592
+ if (longSide > 0 && longSide <= 480) return nativeJpeg;
6593
+ return (0, sharp.default)(nativeJpeg).resize(480, 480, {
6594
+ fit: "inside",
6595
+ withoutEnlargement: true
6596
+ }).jpeg({ quality: 88 }).toBuffer();
6597
+ }
6598
+ //#endregion
6480
6599
  //#region src/shared/frame/box-drawer.ts
6481
6600
  var DEFAULT_COLOR = require_dist.DEFAULT_EVENT_COLOR;
6482
6601
  var DEFAULT_QUALITY = 80;
@@ -6645,7 +6764,8 @@ var EventMediaDispatcher = class {
6645
6764
  const snapshots = input.snapshots ?? [];
6646
6765
  const empty = {
6647
6766
  storedSnapshots: [],
6648
- thumbnailTrackIds: []
6767
+ thumbnailTrackIds: [],
6768
+ rasterFallbacks: []
6649
6769
  };
6650
6770
  if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
6651
6771
  let decoded;
@@ -6697,21 +6817,67 @@ var EventMediaDispatcher = class {
6697
6817
  });
6698
6818
  return empty;
6699
6819
  }
6820
+ const rasterFallbacks = await this.collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, input.cropPadding, input.rasterFallbackWantedTrackIds);
6700
6821
  for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6701
6822
  for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6702
6823
  const storedSnapshots = [];
6703
6824
  const thumbnailTrackIds = [];
6704
6825
  for (const sn of snapshots) {
6705
- const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn, input.cropPadding);
6826
+ const res = await this.writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn);
6706
6827
  if (res.storedSnapshot) storedSnapshots.push(res.storedSnapshot);
6707
6828
  if (res.thumbnailWritten) thumbnailTrackIds.push(sn.trackId);
6708
6829
  }
6709
6830
  return {
6710
6831
  storedSnapshots,
6711
- thumbnailTrackIds
6832
+ thumbnailTrackIds,
6833
+ rasterFallbacks
6712
6834
  };
6713
6835
  }
6714
6836
  /**
6837
+ * Cut ONE clean detection-raster subject crop per WANTED track that has a
6838
+ * target (firstFrame or snapshot) in this frame — the "first available
6839
+ * detection-raster frame" of the zero-media fallback. Cropped from the
6840
+ * already-resolved `frameData` at its REAL resolution via {@link extractCrop}
6841
+ * (extract-only — NEVER upscaled). Deduped per trackId (first target wins).
6842
+ * A per-track encode failure is skipped (logged) — a missing fallback simply
6843
+ * leaves the track with no last-resort preview, never an error.
6844
+ */
6845
+ async collectRasterFallbacks(frameData, fw, fh, trackFrames, snapshots, cropPadding, wanted) {
6846
+ if (!wanted || wanted.size === 0) return [];
6847
+ const seen = /* @__PURE__ */ new Set();
6848
+ const out = [];
6849
+ const candidates = [...trackFrames.map((t) => ({
6850
+ trackId: t.trackId,
6851
+ timestamp: t.timestamp,
6852
+ bbox: t.bbox
6853
+ })), ...snapshots.map((s) => ({
6854
+ trackId: s.trackId,
6855
+ timestamp: s.timestamp,
6856
+ bbox: s.bbox
6857
+ }))];
6858
+ for (const c of candidates) {
6859
+ if (!wanted.has(c.trackId) || seen.has(c.trackId)) continue;
6860
+ seen.add(c.trackId);
6861
+ try {
6862
+ const { crop } = await extractCrop(frameData, fw, fh, squareSafeCropRegionNormalized(c.bbox, {
6863
+ W: fw,
6864
+ H: fh
6865
+ }, cropPadding));
6866
+ out.push({
6867
+ trackId: c.trackId,
6868
+ timestamp: c.timestamp,
6869
+ jpeg: crop
6870
+ });
6871
+ } catch (err) {
6872
+ this.deps.logger.debug("event media: raster fallback crop failed", { meta: {
6873
+ trackId: c.trackId,
6874
+ error: err instanceof Error ? err.message : String(err)
6875
+ } });
6876
+ }
6877
+ }
6878
+ return out;
6879
+ }
6880
+ /**
6715
6881
  * Periodic per-track media (§5). The boxed FULL frame is encoded once and
6716
6882
  * shared by the appended `snapshot` (timeline filmstrip) and the rolling
6717
6883
  * `lastFrame` (overwrite). The best `thumbnail` is DIFFERENT: a clean
@@ -6723,7 +6889,7 @@ var EventMediaDispatcher = class {
6723
6889
  * failed). `thumbnailWritten` reports whether a best `thumbnail` actually
6724
6890
  * landed this frame (#27-A) so the caller can stop forcing retries.
6725
6891
  */
6726
- async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn, cropPadding) {
6892
+ async writeTrackSnapshot(deviceId, frameHandle, frameData, fw, fh, sn) {
6727
6893
  if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return {
6728
6894
  storedSnapshot: null,
6729
6895
  thumbnailWritten: false
@@ -6750,8 +6916,11 @@ var EventMediaDispatcher = class {
6750
6916
  if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6751
6917
  let thumbnailWritten = false;
6752
6918
  if (sn.bestThumbnail) {
6753
- const crop = await this.cropSubjectRegion(frameHandle, fw, fh, sn.bbox, cropPadding);
6754
- if (crop) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
6919
+ const variants = await this.cropSubjectVariants(frameHandle, fw, fh, sn.bbox);
6920
+ if (variants) {
6921
+ thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, variants.thumbnail);
6922
+ await this.replaceKind(deviceId, sn.trackId, "thumbnailSmall", sn.timestamp, variants.thumbnailSmall);
6923
+ }
6755
6924
  }
6756
6925
  return {
6757
6926
  storedSnapshot: stored,
@@ -6801,6 +6970,54 @@ var EventMediaDispatcher = class {
6801
6970
  }
6802
6971
  }
6803
6972
  /**
6973
+ * The best-shot subject crop as its TWO persisted variants (best-crop
6974
+ * fast-load, 2026-07-21). ONE native fetch: the SQUARE-framed subject ROI
6975
+ * (side = max(w,h)×1.2, clamped to the frame — {@link squareSubjectCropRegionNormalized})
6976
+ * is requested from the runner's retained native surface with NO `maxWidth`
6977
+ * (uncapped TRUE native, decision #3) → the `thumbnail`. The `thumbnailSmall`
6978
+ * is DERIVED by downscaling that SAME JPEG to the 480 long-side cap — never a
6979
+ * second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
6980
+ * returns the native buffer as-is when it is already ≤ 480).
6981
+ *
6982
+ * NATIVE-OR-NOTHING: any miss/error (or a runner without the method) returns
6983
+ * `null` after a loud `logger.warn`; the caller SKIPS the write and the
6984
+ * per-frame retry lands a real native crop later. Never a local resize
6985
+ * upscale of a ≤640 tile (a blurred lie).
6986
+ */
6987
+ async cropSubjectVariants(frameHandle, fw, fh, bbox) {
6988
+ if (!this.deps.getNativeCropJpeg) {
6989
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
6990
+ shmId: frameHandle.shmId,
6991
+ reason: "no-native-cap"
6992
+ } });
6993
+ return null;
6994
+ }
6995
+ try {
6996
+ const norm = squareSubjectCropRegionNormalized(bbox, {
6997
+ W: fw,
6998
+ H: fh
6999
+ });
7000
+ const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
7001
+ if (!native) {
7002
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
7003
+ shmId: frameHandle.shmId,
7004
+ reason: "native-miss"
7005
+ } });
7006
+ return null;
7007
+ }
7008
+ return {
7009
+ thumbnail: native,
7010
+ thumbnailSmall: await deriveThumbnailSmall(native)
7011
+ };
7012
+ } catch (err) {
7013
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
7014
+ shmId: frameHandle.shmId,
7015
+ error: err instanceof Error ? err.message : String(err)
7016
+ } });
7017
+ return null;
7018
+ }
7019
+ }
7020
+ /**
6804
7021
  * The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
6805
7022
  * OWN box burned onto the NATIVE full frame downscaled to
6806
7023
  * {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
@@ -6927,36 +7144,6 @@ var EventMediaDispatcher = class {
6927
7144
  }
6928
7145
  };
6929
7146
  //#endregion
6930
- //#region src/shared/frame/crop-extractor.ts
6931
- /**
6932
- * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.
6933
- * Coordinates are clamped to frame bounds to avoid out-of-range errors.
6934
- */
6935
- async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
6936
- const rawLeft = Math.round(bbox.x * frameWidth);
6937
- const rawTop = Math.round(bbox.y * frameHeight);
6938
- const rawWidth = Math.round(bbox.w * frameWidth);
6939
- const rawHeight = Math.round(bbox.h * frameHeight);
6940
- const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
6941
- const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
6942
- const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
6943
- const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
6944
- return {
6945
- crop: await (0, sharp.default)(frameData, { raw: {
6946
- width: frameWidth,
6947
- height: frameHeight,
6948
- channels: 3
6949
- } }).extract({
6950
- left,
6951
- top,
6952
- width,
6953
- height
6954
- }).jpeg({ quality: 90 }).toBuffer(),
6955
- width,
6956
- height
6957
- };
6958
- }
6959
- //#endregion
6960
7147
  //#region src/pipeline-analytics/embedding/embedding-dispatcher.ts
6961
7148
  function selectCropBbox(detection) {
6962
7149
  return detection.refinedBbox ?? detection.bbox;
@@ -8745,6 +8932,16 @@ function planPeriodicMedia(input) {
8745
8932
  bestThumbnail: input.isNewBest || !thumbnailLanded
8746
8933
  };
8747
8934
  }
8935
+ var DEFAULT_ZERO_MEDIA_POLICY_CONFIG = { suppressMaxDurationMs: 1e3 };
8936
+ /**
8937
+ * Classify a closing track's persistence outcome. Pure — see the module header
8938
+ * for the full contract.
8939
+ */
8940
+ function decideZeroMediaPolicy(input, config = DEFAULT_ZERO_MEDIA_POLICY_CONFIG) {
8941
+ if (input.hasMedia) return "persist";
8942
+ if (input.durationMs < config.suppressMaxDurationMs && !input.confirmed) return "suppress";
8943
+ return input.hasRasterFallback ? "raster-fallback" : "persist";
8944
+ }
8748
8945
  //#endregion
8749
8946
  //#region src/pipeline-analytics/best-thumbnail-guard.ts
8750
8947
  /**
@@ -11997,10 +12194,28 @@ function pickEventOwnedMedia(files) {
11997
12194
  return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
11998
12195
  }
11999
12196
  /** Pick a track's fallback media in the shared cadence-preference order the
12000
- * KeyEvent path uses: best `thumbnail` rolling `lastFrame` → `firstFrame` →
12001
- * newest `snapshot` → any track blob. Undefined when the track owns no media. */
12197
+ * KeyEvent path uses: `thumbnailSmall` (480 fast-load)best `thumbnail` →
12198
+ * rolling `lastFrame` → `firstFrame` newest `snapshot` any track blob.
12199
+ * Undefined when the track owns no media. */
12002
12200
  function pickTrackFallbackMedia(files) {
12003
- return 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];
12201
+ 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];
12202
+ }
12203
+ /**
12204
+ * Crop-forced resolution (the `?kind=crop` reel/timeline path). Prefer a CLEAN
12205
+ * (never-boxed) frame via the injected `pickClean`; when the track owns NO clean
12206
+ * frame at all — the regression class created by retiring `crop`/`fullFrame`
12207
+ * production (2026-07-21), leaving degenerate tracks with only boxed
12208
+ * `lastFrame`/`snapshot`/`firstFrame` — degrade to ANY track media rather than
12209
+ * returning undefined. A boxed frame (with the target's box drawn) is a real
12210
+ * preview; an empty tile is not. Clean is always tried FIRST so the common case
12211
+ * still serves an unboxed tile. Undefined only when the owner has NO media.
12212
+ *
12213
+ * `pickClean` is injected because the clean-kind picker (`pickCleanMedia`) lives
12214
+ * in the addon `index.ts` over the concrete `MediaFileKind` set; this keeps the
12215
+ * composition pure + unit-testable here.
12216
+ */
12217
+ function resolveCropForcedMedia(files, pickClean) {
12218
+ return pickClean(files) ?? pickTrackFallbackMedia(files);
12004
12219
  }
12005
12220
  /**
12006
12221
  * Resolve the default-path media for an event id: the event's own media when it
@@ -12358,6 +12573,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12358
12573
  * area) + emit time, so a material improvement is measured against the
12359
12574
  * last emit and debounced. Seeded at `start`, dropped at `end`. */
12360
12575
  trackLifecycleUpdateMem = /* @__PURE__ */ new Map();
12576
+ /** Per-active-track close-policy state (spec §"Zero-media track policy"):
12577
+ * whether the track ever reached the tracker's hit-confirmation
12578
+ * (`trackAge >= minHits`) and the FIRST retained detection-raster subject
12579
+ * crop. Both are read ONLY at close (`sweepExpiredTracks`) to decide
12580
+ * suppress / raster-fallback / persist for a track that ends with no media.
12581
+ * `deviceId` lets `clearDevice` prune without an active-track lookup. Dropped
12582
+ * on every teardown path. */
12583
+ trackClosureState = /* @__PURE__ */ new Map();
12361
12584
  /** The shared crop extractor (native-res first, detection-frame fallback),
12362
12585
  * captured in the constructor so `processFrame` can crop object thumbnails in
12363
12586
  * the same live-frame window as the face/plate/event-media captures. The
@@ -12416,7 +12639,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12416
12639
  let storage = this.ctx.kernel.storage;
12417
12640
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
12418
12641
  if (mediaRoot) {
12419
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DtSEvIXU.js"));
12642
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-C4bKtLou.js"));
12420
12643
  storage = new FilesystemStorageProvider(mediaRoot);
12421
12644
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
12422
12645
  }
@@ -12849,6 +13072,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12849
13072
  this.bindingCache?.onBindingsChanged(data);
12850
13073
  if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
12851
13074
  this.trackStore?.clearDevice(data.deviceId);
13075
+ this.clearClosureStateForDevice(data.deviceId);
12852
13076
  this.stationaryRegistry?.forgetDevice(data.deviceId);
12853
13077
  this.overlayState.clearDevice(data.deviceId);
12854
13078
  this.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -12867,6 +13091,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12867
13091
  this.unsubDeviceUnreg = this.ctx.eventBus.subscribe({ category: require_dist.EventCategory.DeviceUnregistered }, (ev) => {
12868
13092
  const { deviceId } = ev.data;
12869
13093
  this.trackStore?.clearDevice(deviceId);
13094
+ this.clearClosureStateForDevice(deviceId);
12870
13095
  this.stationaryRegistry?.forgetDevice(deviceId);
12871
13096
  this.overlayState.clearDevice(deviceId);
12872
13097
  this.overlaySynthesisWarnAt.delete(deviceId);
@@ -13222,6 +13447,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13222
13447
  this.thumbnailInFlight.clear();
13223
13448
  this.keyFrameInFlight.clear();
13224
13449
  this.trackLifecycleUpdateMem.clear();
13450
+ this.trackClosureState.clear();
13225
13451
  this.objectEmbeddingBestSelector.clear();
13226
13452
  this.levelStateByDevice.clear();
13227
13453
  this.settingsCacheByDevice.clear();
@@ -13340,6 +13566,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13340
13566
  });
13341
13567
  positionsCountById.set(t.trackId, upserted.positions.length);
13342
13568
  }
13569
+ if (result.rawTrackedDetections.length > 0) {
13570
+ const { minHits } = await this.resolveDeviceDetectionSensitivitySettings(deviceId);
13571
+ for (const rt of result.rawTrackedDetections) if (rt.trackAge >= minHits) this.markTrackConfirmed(deviceId, rt.trackId);
13572
+ }
13343
13573
  const log = this.ctx.logger.withTags({ deviceId });
13344
13574
  const prevIds = this.lastActiveTrackIds.get(key) ?? /* @__PURE__ */ new Set();
13345
13575
  const firstFrameTargets = [];
@@ -13392,6 +13622,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13392
13622
  ...t.label ? { label: t.label } : {}
13393
13623
  });
13394
13624
  this.trackStore.seedSnapshotClock(id, result.timestamp, t.bbox);
13625
+ this.lastFrameAtByTrack.set(id, result.timestamp);
13395
13626
  }
13396
13627
  this.ctx.eventBus.emit({
13397
13628
  id: `pa-${(0, node_crypto.randomUUID)()}`,
@@ -13563,14 +13794,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13563
13794
  } });
13564
13795
  const thumbInFlightTrackIds = snapshotTargets.filter((t) => t.bestThumbnail).map((t) => t.trackId);
13565
13796
  for (const trackId of thumbInFlightTrackIds) this.thumbnailInFlight.add(trackId);
13797
+ const rasterFallbackWantedTrackIds = /* @__PURE__ */ new Set();
13798
+ for (const t of firstFrameTargets) if (!this.trackClosureState.get(t.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(t.trackId);
13799
+ for (const s of snapshotTargets) if (!this.trackClosureState.get(s.trackId)?.rasterFallback) rasterFallbackWantedTrackIds.add(s.trackId);
13566
13800
  this.eventMediaDispatcher.captureForFrame({
13567
13801
  deviceId,
13568
13802
  frameHandle,
13569
13803
  events: eventTargets,
13570
13804
  trackFrames: firstFrameTargets,
13571
13805
  snapshots: snapshotTargets,
13572
- cropPadding: mediaSettings.cropPadding
13806
+ cropPadding: mediaSettings.cropPadding,
13807
+ rasterFallbackWantedTrackIds
13573
13808
  }).then((res) => {
13809
+ for (const rf of res.rasterFallbacks) {
13810
+ const st = this.ensureTrackClosureState(deviceId, rf.trackId);
13811
+ if (!st.rasterFallback) st.rasterFallback = {
13812
+ jpeg: rf.jpeg,
13813
+ timestamp: rf.timestamp
13814
+ };
13815
+ }
13574
13816
  for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
13575
13817
  timestamp: s.timestamp,
13576
13818
  position: {
@@ -14581,6 +14823,44 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14581
14823
  const expired = await this.trackStore.expireStale(Date.now());
14582
14824
  for (const t of expired) {
14583
14825
  const duration = t.lastSeen - t.firstSeen;
14826
+ const closure = this.trackClosureState.get(t.trackId);
14827
+ const outcome = decideZeroMediaPolicy({
14828
+ durationMs: duration,
14829
+ hasMedia: (await this.mediaStore?.listByOwner("track", t.trackId) ?? []).length > 0,
14830
+ confirmed: closure?.confirmed ?? false,
14831
+ hasRasterFallback: closure?.rasterFallback !== void 0
14832
+ });
14833
+ if (outcome === "suppress") {
14834
+ this.ctx.logger.info("track suppressed (zero-media false birth)", {
14835
+ tags: { deviceId: t.deviceId },
14836
+ meta: {
14837
+ trackId: t.trackId,
14838
+ className: t.className,
14839
+ durationMs: duration,
14840
+ positions: t.positions.length
14841
+ }
14842
+ });
14843
+ await this.suppressZeroMediaTrack(t.deviceId, t.trackId);
14844
+ continue;
14845
+ }
14846
+ if (outcome === "raster-fallback" && closure?.rasterFallback) try {
14847
+ await this.mediaStore?.put({
14848
+ deviceId: t.deviceId,
14849
+ ownerKind: "track",
14850
+ ownerId: t.trackId,
14851
+ kind: "thumbnail",
14852
+ timestamp: closure.rasterFallback.timestamp,
14853
+ data: closure.rasterFallback.jpeg
14854
+ });
14855
+ } catch (err) {
14856
+ this.ctx.logger.debug("zero-media raster fallback put failed", {
14857
+ tags: { deviceId: t.deviceId },
14858
+ meta: {
14859
+ trackId: t.trackId,
14860
+ error: String(err)
14861
+ }
14862
+ });
14863
+ }
14584
14864
  this.ctx.logger.info("track ended", {
14585
14865
  tags: { deviceId: t.deviceId },
14586
14866
  meta: {
@@ -14691,12 +14971,53 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14691
14971
  });
14692
14972
  this.emitTrackLifecycle(endPayload, t.lastSeen);
14693
14973
  this.trackLifecycleUpdateMem.delete(t.trackId);
14974
+ this.trackClosureState.delete(t.trackId);
14694
14975
  }
14695
14976
  } catch (err) {
14696
14977
  if (this.shuttingDown) return;
14697
14978
  this.ctx.logger.debug("sweepExpiredTracks failed", { meta: { error: String(err) } });
14698
14979
  }
14699
14980
  }
14981
+ /**
14982
+ * Undo a zero-media false-birth track (spec §"Zero-media track policy"): tear
14983
+ * down its live per-track state (mirroring the natural end MINUS importance
14984
+ * scoring + the lifecycle `end`/`TrackEnded` events) and cascade-delete the
14985
+ * just-persisted row + any events/media so nothing dangles. Best-effort.
14986
+ */
14987
+ async suppressZeroMediaTrack(deviceId, trackId) {
14988
+ const faceEnd = this.faceRecognizer?.onTrackEnd(deviceId, trackId);
14989
+ const dropKeyFrame = () => {
14990
+ this.keyFrameKeyByTrackId.delete(trackId);
14991
+ };
14992
+ if (faceEnd) faceEnd.finally(dropKeyFrame);
14993
+ else dropKeyFrame();
14994
+ this.plateRecognizer?.onTrackEnd(deviceId, trackId);
14995
+ this.bestFrameTracker.delete(trackId);
14996
+ this.objectEmbeddingBestSelector.delete(trackId);
14997
+ this.lastFrameAtByTrack.delete(trackId);
14998
+ this.thumbnailLandedTracks.delete(trackId);
14999
+ this.thumbnailInFlight.delete(trackId);
15000
+ this.keyFrameInFlight.delete(trackId);
15001
+ this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15002
+ this.overlayState.onTrackEnded(deviceId, trackId);
15003
+ this.trackLifecycleUpdateMem.delete(trackId);
15004
+ this.trackClosureState.delete(trackId);
15005
+ if (this.eventStore && this.mediaStore && this.trackStore) try {
15006
+ await cascadeDeleteTrack({
15007
+ eventStore: this.eventStore,
15008
+ mediaStore: this.mediaStore,
15009
+ trackStore: this.trackStore
15010
+ }, trackId);
15011
+ } catch (err) {
15012
+ this.ctx.logger.debug("zero-media suppression cascade failed", {
15013
+ tags: { deviceId },
15014
+ meta: {
15015
+ trackId,
15016
+ error: String(err)
15017
+ }
15018
+ });
15019
+ }
15020
+ }
14700
15021
  async sweepRetention() {
14701
15022
  if (this.shuttingDown || !this.eventStore || !this.mediaStore) return;
14702
15023
  const now = Date.now();
@@ -14906,6 +15227,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14906
15227
  this.thumbnailInFlight.delete(track.trackId);
14907
15228
  this.keyFrameInFlight.delete(track.trackId);
14908
15229
  this.trackLifecycleUpdateMem.delete(track.trackId);
15230
+ this.trackClosureState.delete(track.trackId);
14909
15231
  this.detailDispatcher?.onTrackEnded(deviceId, track.trackId);
14910
15232
  this.overlayState.onTrackEnded(deviceId, track.trackId);
14911
15233
  this.lastActiveTrackIds.get(key)?.delete(track.trackId);
@@ -15204,6 +15526,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15204
15526
  }
15205
15527
  async clearTracks(input) {
15206
15528
  this.trackStore?.clearDevice(input.deviceId);
15529
+ this.clearClosureStateForDevice(input.deviceId);
15207
15530
  this.stationaryRegistry?.clearDevice(input.deviceId);
15208
15531
  this.overlayState.clearDevice(input.deviceId);
15209
15532
  this.overlaySynthesisWarnAt.delete(input.deviceId);
@@ -15416,6 +15739,27 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15416
15739
  clearLiveTrackState(deviceId, trackId) {
15417
15740
  this.detailDispatcher?.onTrackEnded(deviceId, trackId);
15418
15741
  this.overlayState.onTrackEnded(deviceId, trackId);
15742
+ this.trackClosureState.delete(trackId);
15743
+ }
15744
+ /** Lazily create + return a track's close-policy state (zero-media policy). */
15745
+ ensureTrackClosureState(deviceId, trackId) {
15746
+ let st = this.trackClosureState.get(trackId);
15747
+ if (!st) {
15748
+ st = {
15749
+ deviceId,
15750
+ confirmed: false
15751
+ };
15752
+ this.trackClosureState.set(trackId, st);
15753
+ }
15754
+ return st;
15755
+ }
15756
+ /** Sticky-mark a track as confirmed once it reaches the tracker's hit gate. */
15757
+ markTrackConfirmed(deviceId, trackId) {
15758
+ this.ensureTrackClosureState(deviceId, trackId).confirmed = true;
15759
+ }
15760
+ /** Drop every close-policy entry for a device (mirrors `trackStore.clearDevice`). */
15761
+ clearClosureStateForDevice(deviceId) {
15762
+ for (const [trackId, st] of this.trackClosureState) if (st.deviceId === deviceId) this.trackClosureState.delete(trackId);
15419
15763
  }
15420
15764
  async deleteTracks(input) {
15421
15765
  const cascade = this.buildTrackCascadeRegistry();
@@ -15710,11 +16054,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15710
16054
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
15711
16055
  if (preferKind !== void 0 && preferKind.length > 0) {
15712
16056
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15713
- const clean = pickCleanMedia([...eventFiles, ...trackFiles], preferKind);
15714
- if (!clean) return null;
16057
+ const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
16058
+ if (!chosen) return null;
15715
16059
  return {
15716
- bytes: Buffer.from(clean.base64, "base64"),
15717
- key: clean.key
16060
+ bytes: Buffer.from(chosen.base64, "base64"),
16061
+ key: chosen.key
15718
16062
  };
15719
16063
  }
15720
16064
  const chosen = await resolveDefaultEventMedia({