@camstack/addon-post-analysis 1.2.0 → 1.2.2

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-MgnHaI0L.js");
5
+ const require_dist = require("../dist-DSP-cVM0.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let sharp = require("sharp");
8
8
  sharp = require_dist.__toESM(sharp);
@@ -3655,6 +3655,10 @@ var TRACKS_COLUMNS = [
3655
3655
  name: "source",
3656
3656
  type: "TEXT"
3657
3657
  },
3658
+ {
3659
+ name: "producingDeviceName",
3660
+ type: "TEXT"
3661
+ },
3658
3662
  {
3659
3663
  name: "firstSeen",
3660
3664
  type: "INTEGER",
@@ -4362,6 +4366,7 @@ var TrackStore = class {
4362
4366
  className: t.className,
4363
4367
  ...t.label !== void 0 ? { label: t.label } : {},
4364
4368
  source: t.source ?? "sensor",
4369
+ ...t.producingDeviceName !== void 0 ? { producingDeviceName: t.producingDeviceName } : {},
4365
4370
  firstSeen: t.firstSeen,
4366
4371
  lastSeen: t.lastSeen,
4367
4372
  positions: [...t.positions],
@@ -4423,6 +4428,7 @@ var TrackStore = class {
4423
4428
  const classes = data["classes"];
4424
4429
  const label = data["label"];
4425
4430
  const source = data["source"];
4431
+ const producingDeviceName = data["producingDeviceName"];
4426
4432
  const importance = data["importance"];
4427
4433
  const bestEventId = data["bestEventId"];
4428
4434
  const importanceReason = data["importanceReason"];
@@ -4443,6 +4449,7 @@ var TrackStore = class {
4443
4449
  className: String(data["className"]),
4444
4450
  ...typeof label === "string" ? { label } : {},
4445
4451
  ...source === "sensor" || source === "pipeline" ? { source } : {},
4452
+ ...typeof producingDeviceName === "string" ? { producingDeviceName } : {},
4446
4453
  firstSeen: Number(data["firstSeen"]),
4447
4454
  lastSeen: Number(data["lastSeen"]),
4448
4455
  positions,
@@ -4488,6 +4495,7 @@ var PLATE_MEDIA_OWNER_PREFIX = "plate-";
4488
4495
  */
4489
4496
  var SINGLE_INSTANCE_KINDS = new Set([
4490
4497
  "keyFrame",
4498
+ "keyFrameSmall",
4491
4499
  "thumbnail",
4492
4500
  "firstFrame",
4493
4501
  "lastFrame"
@@ -5244,6 +5252,32 @@ var EventStore = class {
5244
5252
  });
5245
5253
  }
5246
5254
  /**
5255
+ * Resolve the OWNING track id for an object event id. Powers the event-media
5256
+ * resolver's track fallback: a new-style object event owns no crop, so its
5257
+ * `mediaUrl` degrades to the track's cadence media, addressed by this id.
5258
+ * Returns `null` when the id is not a persisted object event (e.g. a KeyEvent
5259
+ * whose id is already a track id, or a motion/audio event) — best-effort.
5260
+ */
5261
+ async getTrackIdForEvent(eventId) {
5262
+ try {
5263
+ const row = await this.store.get.query({
5264
+ collection: OBJECT_EVENTS_COLLECTION,
5265
+ key: eventId
5266
+ });
5267
+ if (row !== null && typeof row === "object" && "trackId" in row) {
5268
+ const trackId = row["trackId"];
5269
+ if (typeof trackId === "string" && trackId.length > 0) return trackId;
5270
+ }
5271
+ return null;
5272
+ } catch (err) {
5273
+ this.logger.debug("getTrackIdForEvent failed", { meta: {
5274
+ eventId,
5275
+ error: String(err)
5276
+ } });
5277
+ return null;
5278
+ }
5279
+ }
5280
+ /**
5247
5281
  * Forward-only: set `label` on every already-emitted object event of a track.
5248
5282
  * Returns the number of events updated. Best-effort per row. Used when face
5249
5283
  * recognition assigns a name to a track after its events were emitted.
@@ -6150,6 +6184,7 @@ var LinkedCamerasCache = class {
6150
6184
  for (const cameraId of cameraIds) try {
6151
6185
  const { devices } = await this.deps.linkedDevices.getLinkedDevices({ deviceId: cameraId });
6152
6186
  for (const d of devices) {
6187
+ if (d.producesTrackedEvents === false) continue;
6153
6188
  const list = next.get(d.deviceId);
6154
6189
  if (list === void 0) next.set(d.deviceId, [cameraId]);
6155
6190
  else if (!list.includes(cameraId)) list.push(cameraId);
@@ -6216,10 +6251,13 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
6216
6251
  *
6217
6252
  * A linked sensor/control state change is projected into the UNIFIED track
6218
6253
  * store as a SYNTHETIC track: `className` = the event kind (`contact` /
6219
- * `lock` / `switch` …), `source: 'sensor'`, media = an on-demand snapshot of
6254
+ * `lock` / `switch` …), `source: 'sensor'`, media = an on-demand best shot of
6220
6255
  * the linked camera in the SAME `getTrackMedia` shape the UIs consume (no
6221
- * bbox → full frame), `positions: []`. It joins the timeline via the existing
6222
- * time-based clustering no new join mechanism.
6256
+ * bbox → full frame), persisted under `kind: 'keyFrameSmall'` the kind the
6257
+ * viewer representative-image selectors (hero/clean/reel) actually consume; a
6258
+ * `'snapshot'`-kind blob is ignored by those chains, which is what stuck the
6259
+ * pa-synth reel on "loading". `positions: []`. It joins the timeline via the
6260
+ * existing time-based clustering — no new join mechanism.
6223
6261
  *
6224
6262
  * The raw `SensorEvent` remains the durable record; this track is its
6225
6263
  * projection. Snapshots are debounced per `(camera, kind)` so a chattery
@@ -6227,6 +6265,28 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
6227
6265
  * `source:'sensor'` tracks (they carry no trajectory).
6228
6266
  */
6229
6267
  var DEFAULT_DEBOUNCE_MS = 1e4;
6268
+ /** A hanging snapshot cap must not stall the sensor state-change path. */
6269
+ var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
6270
+ /** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
6271
+ var SnapshotTimeoutError = class extends Error {
6272
+ constructor(ms) {
6273
+ super(`snapshot cap timed out after ${ms}ms`);
6274
+ this.name = "SnapshotTimeoutError";
6275
+ }
6276
+ };
6277
+ /** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
6278
+ function withTimeout$1(promise, ms) {
6279
+ return new Promise((resolve, reject) => {
6280
+ const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
6281
+ promise.then((value) => {
6282
+ clearTimeout(timer);
6283
+ resolve(value);
6284
+ }, (err) => {
6285
+ clearTimeout(timer);
6286
+ reject(err instanceof Error ? err : new Error(String(err)));
6287
+ });
6288
+ });
6289
+ }
6230
6290
  /** Full-frame, position-less placeholder (spatial consumers skip these). */
6231
6291
  function zeroPosition(timestamp) {
6232
6292
  return {
@@ -6244,6 +6304,7 @@ function zeroPosition(timestamp) {
6244
6304
  var SyntheticSensorTrackMaterializer = class {
6245
6305
  deps;
6246
6306
  debounceMs;
6307
+ snapshotTimeoutMs;
6247
6308
  makeId;
6248
6309
  now;
6249
6310
  /** Last snapshot time per `${cameraId}:${kind}`. */
@@ -6251,10 +6312,29 @@ var SyntheticSensorTrackMaterializer = class {
6251
6312
  constructor(deps) {
6252
6313
  this.deps = deps;
6253
6314
  this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
6315
+ this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS;
6254
6316
  this.makeId = deps.makeId ?? (() => `pa-synth-${(0, node_crypto.randomUUID)()}`);
6255
6317
  this.now = deps.now ?? Date.now;
6256
6318
  }
6257
6319
  /**
6320
+ * Fetch the linked camera's snapshot under a bounded timeout. A hanging
6321
+ * snapshot cap is skipped (returns null + warns) so the track still lands;
6322
+ * a null / failed fetch is likewise reported and skipped.
6323
+ */
6324
+ async fetchSnapshot(cameraId) {
6325
+ try {
6326
+ const snap = await withTimeout$1(this.deps.snapshot.getSnapshot({
6327
+ deviceId: cameraId,
6328
+ force: true
6329
+ }), this.snapshotTimeoutMs);
6330
+ if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
6331
+ return snap;
6332
+ } catch (err) {
6333
+ this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err);
6334
+ return null;
6335
+ }
6336
+ }
6337
+ /**
6258
6338
  * Materialize a synthetic track for a sensor/control event on `cameraId`.
6259
6339
  * Returns the persisted track, or null when debounced. Snapshot failure
6260
6340
  * still lands a track (with no media) — it never blocks the sensor record.
@@ -6267,28 +6347,23 @@ var SyntheticSensorTrackMaterializer = class {
6267
6347
  const trackId = this.makeId();
6268
6348
  const ts = input.timestamp;
6269
6349
  let mediaKey = null;
6270
- try {
6271
- const snap = await this.deps.snapshot.getSnapshot({
6350
+ const snap = await this.fetchSnapshot(input.cameraId);
6351
+ if (snap !== null) try {
6352
+ const raw = Buffer.from(snap.base64, "base64");
6353
+ let data = raw;
6354
+ try {
6355
+ data = await downscaleFullFrameJpeg(raw, 960, 540);
6356
+ } catch (err) {
6357
+ this.deps.onError?.("downscaleSnapshot", err);
6358
+ }
6359
+ mediaKey = await this.deps.media.put({
6272
6360
  deviceId: input.cameraId,
6273
- force: true
6361
+ ownerKind: "track",
6362
+ ownerId: trackId,
6363
+ kind: "keyFrameSmall",
6364
+ timestamp: ts,
6365
+ data
6274
6366
  });
6275
- if (snap !== null) {
6276
- const raw = Buffer.from(snap.base64, "base64");
6277
- let data = raw;
6278
- try {
6279
- data = await downscaleFullFrameJpeg(raw);
6280
- } catch (err) {
6281
- this.deps.onError?.("downscaleSnapshot", err);
6282
- }
6283
- mediaKey = await this.deps.media.put({
6284
- deviceId: input.cameraId,
6285
- ownerKind: "track",
6286
- ownerId: trackId,
6287
- kind: "snapshot",
6288
- timestamp: ts,
6289
- data
6290
- });
6291
- } else this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
6292
6367
  } catch (err) {
6293
6368
  this.deps.onError?.("snapshotMedia", err);
6294
6369
  }
@@ -6297,6 +6372,7 @@ var SyntheticSensorTrackMaterializer = class {
6297
6372
  deviceId: input.cameraId,
6298
6373
  className: input.kind,
6299
6374
  source: "sensor",
6375
+ ...input.producingDeviceName !== void 0 ? { producingDeviceName: input.producingDeviceName } : {},
6300
6376
  firstSeen: ts,
6301
6377
  lastSeen: ts,
6302
6378
  positions: [],
@@ -6329,6 +6405,22 @@ async function resolveFrame(handle, deps) {
6329
6405
  return deps.getRemoteFrame(handle);
6330
6406
  }
6331
6407
  //#endregion
6408
+ //#region src/shared/frame/shared-frame-resolver.ts
6409
+ function frameHandleKey(h) {
6410
+ return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
6411
+ }
6412
+ function createSharedFrameResolver(resolve) {
6413
+ let cache = null;
6414
+ return (handle) => {
6415
+ const key = frameHandleKey(handle);
6416
+ if (cache === null || cache.key !== key) cache = {
6417
+ key,
6418
+ value: resolve(handle)
6419
+ };
6420
+ return cache.value;
6421
+ };
6422
+ }
6423
+ //#endregion
6332
6424
  //#region src/shared/frame/square-safe-crop.ts
6333
6425
  /**
6334
6426
  * Compute a square-safe 16:9 crop region in pixel space.
@@ -6457,27 +6549,30 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
6457
6549
  }
6458
6550
  //#endregion
6459
6551
  //#region src/pipeline-analytics/services/event-media-dispatcher.ts
6460
- /** All event media is stored at NATIVE resolution (no downscale), high quality.
6461
- * Small downscaled `thumbnail`s are intentionally left out for now — when we
6462
- * reintroduce them they'll be a separate small kind. */
6463
- var MEDIA_QUALITY = 88;
6464
- /** Output dimensions for the LOCAL-FALLBACK square-safe 16:9 crops
6465
- * (crop/faceCrop/plateCrop/thumbnail) cut from the resolved ≤640 frame. */
6466
- var CROP_WIDTH = 640;
6467
- var CROP_HEIGHT = 360;
6468
- var CROP_QUALITY = 80;
6469
6552
  /**
6470
- * Native-surface crop width cap for subject crops (`crop`/`thumbnail`/
6471
- * `faceCrop`/`plateCrop`). ~960px yields a sharp gallery/reel tile AND a
6472
- * ≥224px classifier input straight from the runner's native surface, WITHOUT
6473
- * hauling a full 1920px frame per subject (that width is reserved for the
6474
- * full-frame `keyFrame`). It is an UPPER bound only: a native miss falls back to
6475
- * the ≤640 local crop, so quality never regresses below today's behaviour.
6553
+ * Long-side cap (px) for the boxed timeline tiles — `firstFrame`, `snapshot`,
6554
+ * `lastFrame`. These are the low-res "timeline filmstrip" images: a native full
6555
+ * frame downscaled to 960 with ONLY the track's own box burned in. A native miss
6556
+ * falls back to the ≤640 detection frame (an honest lower-res frame, NEVER an
6557
+ * upscale).
6558
+ */
6559
+ var SNAPSHOT_MAX_WIDTH = 960;
6560
+ /**
6561
+ * Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
6562
+ * ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
6563
+ * surface). Both frames show the SAME scene at the same aspect ratio, so a
6564
+ * uniform per-axis scale places the box correctly on the higher-res frame.
6476
6565
  */
6477
- var NATIVE_SUBJECT_CROP_MAX_WIDTH = 960;
6478
- function caption(className, confidence, label) {
6479
- const base = label && label !== className ? `${className} ${label}` : className;
6480
- return typeof confidence === "number" ? `${base} ${Math.round(confidence * 100)}%` : base;
6566
+ function scaleBox(bbox, fromW, fromH, toW, toH, label) {
6567
+ const sx = toW / fromW;
6568
+ const sy = toH / fromH;
6569
+ return {
6570
+ x: bbox.x * sx,
6571
+ y: bbox.y * sy,
6572
+ w: bbox.w * sx,
6573
+ h: bbox.h * sy,
6574
+ ...label ? { label } : {}
6575
+ };
6481
6576
  }
6482
6577
  /**
6483
6578
  * True when a packed-RGB frame is (near-)uniform in every channel — the
@@ -6509,20 +6604,41 @@ function isUniformRgbFrame(data, width, height) {
6509
6604
  }
6510
6605
  /**
6511
6606
  * Generates object-event + track media FROM the decoded detection-pipeline
6512
- * frame (the `frameHandle`/shm frame the detector ran on), NEVER the device
6513
- * snapshot cap. Per object event it writes: a `crop` (square-safe 16:9
6514
- * region, 640×360, q80), and — at ORIGINAL resolution — a clear `fullFrame`
6515
- * (no boxes) + a `fullFrameBoxed` (this event's box drawn) for the detail
6516
- * view. Per new track a boxed `firstFrame`. Child bboxes produce
6517
- * `faceCrop`/`plateCrop` with the same square-safe algorithm.
6518
- * The frame is resolved ONCE per call and reused across all
6519
- * targets — frames recycle in milliseconds, so the caller must invoke this
6520
- * immediately after producing the events while the handle is still live.
6607
+ * frame (the `frameHandle`/native surface the detector ran on), NEVER the device
6608
+ * snapshot cap.
6609
+ *
6610
+ * Per object event it writes ONLY the enrichment child crops
6611
+ * (`faceCrop`/`plateCrop`) the retired `crop`/`fullFrame`/`fullFrameBoxed`
6612
+ * kinds are no longer produced (2026-07-21; old stored tracks keep reading them,
6613
+ * the enum is unchanged). The representative event image is now the track
6614
+ * `thumbnail`/`keyFrame`.
6615
+ *
6616
+ * Per track it writes the boxed timeline tiles — a `firstFrame` on track start,
6617
+ * plus periodic `snapshot`/`lastFrame` — each cut from the runner's NATIVE full
6618
+ * frame downscaled to {@link SNAPSHOT_MAX_WIDTH} (960) with ONLY the track's own
6619
+ * box burned in, falling back to the ≤640 detection frame (honest lower res,
6620
+ * never an upscale) on a native miss. The best `thumbnail` stays a clean,
6621
+ * uncapped native subject crop (native-or-nothing).
6622
+ *
6623
+ * The detection frame is resolved ONCE per call (blank-frame guard); native full
6624
+ * frames + subject crops are fetched per target by handle. Frames recycle in
6625
+ * milliseconds, so the caller must invoke this immediately after producing the
6626
+ * events while the handle is still live.
6521
6627
  */
6522
6628
  var EventMediaDispatcher = class {
6523
6629
  deps;
6630
+ /**
6631
+ * Per-frame single-slot memo of the native 960 full frame (S3): every track
6632
+ * tile in ONE `captureForFrame` pass shares the SAME `frameHandle`, so multiple
6633
+ * `boxedTimelineFrame` calls would otherwise each round-trip the runner for the
6634
+ * identical native full frame. Keyed by frame identity (`createSharedFrameResolver`)
6635
+ * so a recycled slot (new `seq`) is a fresh fetch. `maxWidth` is always
6636
+ * {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
6637
+ */
6638
+ sharedNativeFullFrame;
6524
6639
  constructor(deps) {
6525
6640
  this.deps = deps;
6641
+ this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
6526
6642
  }
6527
6643
  async captureForFrame(input) {
6528
6644
  const { deviceId, frameHandle, events, trackFrames } = input;
@@ -6581,8 +6697,8 @@ var EventMediaDispatcher = class {
6581
6697
  });
6582
6698
  return empty;
6583
6699
  }
6584
- for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
6585
- for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
6700
+ for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6701
+ for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6586
6702
  const storedSnapshots = [];
6587
6703
  const thumbnailTrackIds = [];
6588
6704
  for (const sn of snapshots) {
@@ -6613,21 +6729,7 @@ var EventMediaDispatcher = class {
6613
6729
  thumbnailWritten: false
6614
6730
  };
6615
6731
  let boxed = null;
6616
- if (sn.appendSnapshot || sn.rollingLastFrame) try {
6617
- boxed = await drawBoxedFrame(frameData, fw, fh, [{
6618
- ...sn.bbox,
6619
- ...sn.label ? { label: sn.label } : {}
6620
- }], { quality: MEDIA_QUALITY });
6621
- } catch (err) {
6622
- this.deps.logger.warn("event media: track snapshot encode failed", {
6623
- tags: { deviceId },
6624
- meta: {
6625
- deviceId,
6626
- trackId: sn.trackId,
6627
- error: err instanceof Error ? err.message : String(err)
6628
- }
6629
- });
6630
- }
6732
+ if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
6631
6733
  let stored = null;
6632
6734
  if (sn.appendSnapshot && boxed) try {
6633
6735
  const mediaKey = await this.deps.mediaStore.put({
@@ -6647,19 +6749,9 @@ var EventMediaDispatcher = class {
6647
6749
  } catch {}
6648
6750
  if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6649
6751
  let thumbnailWritten = false;
6650
- if (sn.bestThumbnail) try {
6651
- const crop = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, sn.bbox, cropPadding);
6652
- thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
6653
- } catch (err) {
6654
- this.deps.logger.warn("event media: track thumbnail crop failed", {
6655
- tags: { deviceId },
6656
- meta: {
6657
- deviceId,
6658
- trackId: sn.trackId,
6659
- error: err instanceof Error ? err.message : String(err)
6660
- }
6661
- });
6662
- if (boxed) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
6752
+ 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);
6663
6755
  }
6664
6756
  return {
6665
6757
  storedSnapshot: stored,
@@ -6671,143 +6763,126 @@ var EventMediaDispatcher = class {
6671
6763
  * object-event `crop` kind, the child `faceCrop`/`plateCrop`, and the track
6672
6764
  * `thumbnail`: the square-safe 16:9 region around the bbox with NO box drawn.
6673
6765
  *
6674
- * NATIVE-FIRST: the region is requested from the runner's retained native
6675
- * surface (normalized [0,1] coords map directly onto it), downscaled to
6676
- * {@link NATIVE_SUBJECT_CROP_MAX_WIDTH} a sharp tile at native detail. On any
6677
- * miss/error (or a runner without the method) it FALLS BACK to cropping the
6678
- * resolved ≤640 frame locally (today's behaviour), so quality never regresses.
6679
- * Both paths run inside the live-handle window opened by `captureForFrame`.
6766
+ * NATIVE-OR-NOTHING: the region is requested from the runner's retained native
6767
+ * surface (normalized [0,1] coords map directly onto it) with NO `maxWidth` —
6768
+ * the crop at TRUE native resolution (decision #3). On any miss/error (or a
6769
+ * runner without the method) it returns `null` after a loud `logger.warn`; the
6770
+ * caller SKIPS the write and the per-frame retry lands a real native crop
6771
+ * later. It is NEVER upscaled an upscaled ≤640 tile is a blurred lie
6772
+ * (case-study lapVar 5–7), so no local resize fallback exists. Runs inside the
6773
+ * live-handle window opened by `captureForFrame`.
6680
6774
  */
6681
- async cropSubjectRegion(frameHandle, frameData, fw, fh, bbox, cropPadding) {
6682
- if (this.deps.getNativeCropJpeg) try {
6775
+ async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
6776
+ if (!this.deps.getNativeCropJpeg) {
6777
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
6778
+ shmId: frameHandle.shmId,
6779
+ reason: "no-native-cap"
6780
+ } });
6781
+ return null;
6782
+ }
6783
+ try {
6683
6784
  const norm = squareSafeCropRegionNormalized(bbox, {
6684
6785
  W: fw,
6685
6786
  H: fh
6686
6787
  }, cropPadding);
6687
- const native = await this.deps.getNativeCropJpeg(frameHandle, norm, NATIVE_SUBJECT_CROP_MAX_WIDTH);
6788
+ const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
6688
6789
  if (native) return native;
6790
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
6791
+ shmId: frameHandle.shmId,
6792
+ reason: "native-miss"
6793
+ } });
6794
+ return null;
6689
6795
  } catch (err) {
6690
- this.deps.logger.debug("event media: native subject crop failedlocal fallback", { meta: {
6796
+ this.deps.logger.warn("native subject crop misswill retry, no upscale", { meta: {
6691
6797
  shmId: frameHandle.shmId,
6692
6798
  error: err instanceof Error ? err.message : String(err)
6693
6799
  } });
6800
+ return null;
6694
6801
  }
6695
- return this.cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding);
6696
6802
  }
6697
6803
  /**
6698
- * Local-fallback square-safe 16:9 crop out of the resolved ≤640 frame, resized
6699
- * to 640×360, JPEG q80. Used when the native surface is unavailable/missed.
6804
+ * The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
6805
+ * OWN box burned onto the NATIVE full frame downscaled to
6806
+ * {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
6807
+ * detection frame (`frameData`) downscaled to the same cap with a `logger.warn`
6808
+ * — an honest LOWER-res frame, NEVER an upscale. Returns the JPEG, or `null`
6809
+ * only if the sharp encode itself throws.
6700
6810
  */
6701
- async cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding) {
6702
- const region = squareSafeCropRegion(bbox, {
6703
- W: fw,
6704
- H: fh
6705
- }, cropPadding);
6706
- const left = Math.max(0, Math.min(region.x, fw - 1));
6707
- const top = Math.max(0, Math.min(region.y, fh - 1));
6708
- const width = Math.max(1, Math.min(region.w, fw - left));
6709
- const height = Math.max(1, Math.min(region.h, fh - top));
6710
- return await (0, sharp.default)(frameData, { raw: {
6711
- width: fw,
6712
- height: fh,
6713
- channels: 3
6714
- } }).extract({
6715
- left,
6716
- top,
6717
- width,
6718
- height
6719
- }).resize(CROP_WIDTH, CROP_HEIGHT).jpeg({ quality: CROP_QUALITY }).toBuffer();
6720
- }
6721
- async replaceKind(deviceId, trackId, kind, timestamp, data) {
6811
+ async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
6812
+ let nf = null;
6813
+ let fetchThrew = false;
6722
6814
  try {
6723
- await this.deps.mediaStore.putReplacing({
6724
- deviceId,
6725
- ownerKind: "track",
6726
- ownerId: trackId,
6727
- kind,
6728
- timestamp,
6729
- data
6730
- });
6731
- return true;
6815
+ nf = await this.sharedNativeFullFrame(frameHandle);
6732
6816
  } catch (err) {
6733
- this.deps.logger.debug(`event media: ${kind} replace failed`, {
6817
+ fetchThrew = true;
6818
+ this.deps.logger.warn("native full frame fetch threw — 640 boxed fallback", {
6734
6819
  tags: { deviceId },
6735
6820
  meta: {
6736
6821
  deviceId,
6737
- trackId,
6822
+ ownerId,
6738
6823
  error: err instanceof Error ? err.message : String(err)
6739
6824
  }
6740
6825
  });
6741
- return false;
6742
6826
  }
6743
- }
6744
- async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
6745
- const box = {
6746
- ...ev.bbox,
6747
- label: caption(ev.className, ev.confidence, ev.label)
6748
- };
6749
6827
  try {
6750
- const crop = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, ev.bbox, cropPadding);
6751
- await this.deps.mediaStore.put({
6752
- deviceId,
6753
- ownerKind: "event",
6754
- ownerId: ev.eventId,
6755
- kind: "crop",
6756
- timestamp: ev.timestamp,
6757
- data: crop
6758
- });
6759
- } catch (err) {
6760
- this.deps.logger.warn("event media: crop failed", {
6828
+ if (nf && nf.format === "rgb" && nf.width > 0 && nf.height > 0) {
6829
+ const box = scaleBox(bbox, fw, fh, nf.width, nf.height, label);
6830
+ return await drawBoxedFrame(nf.data, nf.width, nf.height, [box], { quality: 88 });
6831
+ }
6832
+ if (!fetchThrew) this.deps.logger.warn("native full frame miss — 640 boxed fallback (no upscale)", {
6761
6833
  tags: { deviceId },
6762
6834
  meta: {
6763
6835
  deviceId,
6764
- eventId: ev.eventId,
6765
- error: err instanceof Error ? err.message : String(err)
6836
+ ownerId,
6837
+ shmId: frameHandle.shmId
6766
6838
  }
6767
6839
  });
6768
- }
6769
- try {
6770
- const fullFrame = await drawBoxedFrame(frameData, fw, fh, [], { quality: MEDIA_QUALITY });
6771
- await this.deps.mediaStore.put({
6772
- deviceId,
6773
- ownerKind: "event",
6774
- ownerId: ev.eventId,
6775
- kind: "fullFrame",
6776
- timestamp: ev.timestamp,
6777
- data: fullFrame
6840
+ return await drawBoxedFrame(frameData, fw, fh, [{
6841
+ ...bbox,
6842
+ ...label ? { label } : {}
6843
+ }], {
6844
+ quality: 88,
6845
+ maxWidth: SNAPSHOT_MAX_WIDTH
6778
6846
  });
6779
6847
  } catch (err) {
6780
- this.deps.logger.warn("event media: clear full frame failed", {
6848
+ this.deps.logger.warn("event media: timeline frame encode failed", {
6781
6849
  tags: { deviceId },
6782
6850
  meta: {
6783
6851
  deviceId,
6784
- eventId: ev.eventId,
6852
+ ownerId,
6785
6853
  error: err instanceof Error ? err.message : String(err)
6786
6854
  }
6787
6855
  });
6856
+ return null;
6788
6857
  }
6858
+ }
6859
+ async replaceKind(deviceId, trackId, kind, timestamp, data) {
6789
6860
  try {
6790
- const fullFrameBoxed = await drawBoxedFrame(frameData, fw, fh, [box], { quality: MEDIA_QUALITY });
6791
- await this.deps.mediaStore.put({
6861
+ await this.deps.mediaStore.putReplacing({
6792
6862
  deviceId,
6793
- ownerKind: "event",
6794
- ownerId: ev.eventId,
6795
- kind: "fullFrameBoxed",
6796
- timestamp: ev.timestamp,
6797
- data: fullFrameBoxed
6863
+ ownerKind: "track",
6864
+ ownerId: trackId,
6865
+ kind,
6866
+ timestamp,
6867
+ data
6798
6868
  });
6869
+ return true;
6799
6870
  } catch (err) {
6800
- this.deps.logger.warn("event media: boxed full frame failed", {
6871
+ this.deps.logger.debug(`event media: ${kind} replace failed`, {
6801
6872
  tags: { deviceId },
6802
6873
  meta: {
6803
6874
  deviceId,
6804
- eventId: ev.eventId,
6875
+ trackId,
6805
6876
  error: err instanceof Error ? err.message : String(err)
6806
6877
  }
6807
6878
  });
6879
+ return false;
6808
6880
  }
6881
+ }
6882
+ async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
6809
6883
  if (ev.childCrops) for (const child of ev.childCrops) try {
6810
- const childCropData = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, child.bbox, cropPadding);
6884
+ const childCropData = await this.cropSubjectRegion(frameHandle, fw, fh, child.bbox, cropPadding);
6885
+ if (!childCropData) continue;
6811
6886
  await this.deps.mediaStore.put({
6812
6887
  deviceId,
6813
6888
  ownerKind: "event",
@@ -6827,13 +6902,10 @@ var EventMediaDispatcher = class {
6827
6902
  });
6828
6903
  }
6829
6904
  }
6830
- async writeTrackFrame(deviceId, frameData, fw, fh, tf) {
6831
- const box = {
6832
- ...tf.bbox,
6833
- ...tf.label ? { label: tf.label } : {}
6834
- };
6905
+ async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
6906
+ const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
6907
+ if (!boxed) return;
6835
6908
  try {
6836
- const boxed = await drawBoxedFrame(frameData, fw, fh, [box], { quality: MEDIA_QUALITY });
6837
6909
  await this.deps.mediaStore.put({
6838
6910
  deviceId,
6839
6911
  ownerKind: "track",
@@ -8709,34 +8781,13 @@ function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight) {
8709
8781
  }
8710
8782
  //#endregion
8711
8783
  //#region src/pipeline-analytics/pipeline/key-frame-capture.ts
8712
- /**
8713
- * Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
8714
- * (Design B — one native full-frame per track at its best-detection moment).
8715
- *
8716
- * ## Why this exists (the missing native keyFrame)
8717
- *
8718
- * `keyFrame` was historically captured ONLY inside the CLIP object-embedding
8719
- * best path (`persistObjectEmbeddingBests`, gated by `isClipObjectEmbedding`).
8720
- * Under the two-plane pipeline the root frame carries NO CLIP embedding (clip is
8721
- * a per-track DETAIL served via `runDetailSubtree`, and is disabled cluster-
8722
- * wide), so that gate was never satisfied and the native `keyFrame` was NEVER
8723
- * produced — every stored frame stayed at the ≤640×360 detection resolution.
8724
- *
8725
- * The fix decouples the `keyFrame` from the clip path: it is captured on the
8726
- * GENERAL best-frame signal (the same `bestThumbnail` decision that drives the
8727
- * `thumbnail`), reusing the WORKING native crop path (`captureCrop` →
8728
- * `pipelineRunner.getNativeCrop`, which cuts the ROI from the decode worker's
8729
- * retained NATIVE surface and only falls back to the detection frame on a miss).
8730
- * A full-frame ROI at {@link KEYFRAME_NATIVE_MAX_WIDTH} therefore yields a frame
8731
- * LARGER than the detection raster (up to the cap), which is the whole point of
8732
- * the `keyFrame` kind.
8733
- */
8734
8784
  /** Cap (px) on the width of the native KEY FRAME (full-frame native capture).
8735
- * Native resolution is the point, but a full 4K RGB surface over the transport
8736
- * per new-best is wasteful for a web detail view 1920px keeps a sharp native
8737
- * frame while bounding the copy (a miss falls back to the detection-res frame,
8738
- * which is already ≤640px). */
8739
- var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
8785
+ * Native resolution is the point: a 4K cam (3840) resolves TRUE native (no
8786
+ * resize, 3840 4096); an 8K cam is bounded to 4096 (still far past the retired
8787
+ * 1920 cap). This honours "native, no 1920 intermediate" while keeping the raw
8788
+ * rgb transfer wire-safe. Env-overridable (`CAMSTACK_KEYFRAME_NATIVE_MAX_WIDTH`)
8789
+ * for a homelab that wants a tighter/looser bound. */
8790
+ var KEYFRAME_NATIVE_MAX_WIDTH = Number.parseInt(process.env.CAMSTACK_KEYFRAME_NATIVE_MAX_WIDTH ?? "", 10) || 4096;
8740
8791
  /**
8741
8792
  * The tracks whose native `keyFrame` should be (re)captured THIS frame: exactly
8742
8793
  * the tracks that hit a GENUINE new best-frame moment (`keyFrame`), NOT the
@@ -8747,31 +8798,39 @@ function selectKeyFrameTrackIds(targets) {
8747
8798
  return targets.filter((t) => t.keyFrame).map((t) => t.trackId);
8748
8799
  }
8749
8800
  /**
8750
- * Build the `captureCrop` request for a track's native `keyFrame`: the FULL
8751
- * frame (no padding) at the native width cap. The full-frame box is what makes
8752
- * the capture route through the native surface at native resolution instead of
8753
- * a tight ≤640 detection crop.
8801
+ * Gate for the clean best-shot native `keyFrame` (design 2026-07-21): the keyFrame
8802
+ * is the track's CLEAN native best frame and MUST NEVER be persisted from the
8803
+ * ≤640 RAM fallback (`tier: 'ram-fullframe'`) that would pin the stored keyFrame
8804
+ * at detection resolution. On a `ram-fullframe` tier the caller drops the write
8805
+ * and the per-frame re-fire retries a real native surface later.
8806
+ *
8807
+ * An ABSENT tier (`undefined`, from a pre-tier runner on a mixed-version cluster)
8808
+ * is ACCEPTED exactly as before the tier field existed — back-compat, never break
8809
+ * mixed-version clusters. Only an explicit `ram-fullframe` is rejected.
8810
+ *
8811
+ * NOTE: this gates the native `keyFrame` ONLY. The 960 boxed TIMELINE tiles keep
8812
+ * accepting the ≤640 fallback (an honest low-res timeline is acceptable there).
8754
8813
  */
8755
- function buildKeyFrameCaptureRequest(frameWidth, frameHeight) {
8756
- return {
8757
- bbox: {
8758
- x: 0,
8759
- y: 0,
8760
- w: frameWidth,
8761
- h: frameHeight
8762
- },
8763
- padding: 0,
8764
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
8765
- };
8814
+ function keyFrameAcceptsTier(tier) {
8815
+ return tier !== "ram-fullframe";
8766
8816
  }
8767
8817
  /**
8768
- * True when a native subject crop of `width` px is a plausible native hit worth
8769
- * keeping, rather than a sub-threshold fallback stamp. Clamps the floor to the
8770
- * requested `maxWidth` so a caller that legitimately asked for a narrow crop
8771
- * (`maxWidth < MIN`) is not rejected for honouring its own cap.
8818
+ * Encode BOTH key-frame variants from a single decoded native full frame. No box
8819
+ * is drawn (the `keyFrame` is the clean best full frame); `drawBoxedFrame([])`
8820
+ * is used purely as the raw-rgb JPEG (+ optional downscale) encoder so the two
8821
+ * variants share one code path. The caller has ALREADY fetched the native frame
8822
+ * at {@link KEYFRAME_NATIVE_MAX_WIDTH}, so `keyFrame` re-encodes it verbatim
8823
+ * (no resize) and `keyFrameSmall` downscales the same buffer to ≤960 —
8824
+ * `withoutEnlargement` never upscales a sub-960 native frame.
8772
8825
  */
8773
- function nativeSubjectCropMeetsFloor(width, maxWidth) {
8774
- return width >= Math.min(320, maxWidth);
8826
+ async function encodeKeyFrameVariants(native) {
8827
+ return {
8828
+ keyFrame: await drawBoxedFrame(native.data, native.width, native.height, [], { quality: 88 }),
8829
+ keyFrameSmall: await drawBoxedFrame(native.data, native.width, native.height, [], {
8830
+ quality: 88,
8831
+ maxWidth: 960
8832
+ })
8833
+ };
8775
8834
  }
8776
8835
  //#endregion
8777
8836
  //#region src/pipeline-analytics/track-retention-sweep.ts
@@ -11534,22 +11593,6 @@ var PlateRecognizer = class {
11534
11593
  }
11535
11594
  };
11536
11595
  //#endregion
11537
- //#region src/shared/frame/shared-frame-resolver.ts
11538
- function frameHandleKey(h) {
11539
- return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
11540
- }
11541
- function createSharedFrameResolver(resolve) {
11542
- let cache = null;
11543
- return (handle) => {
11544
- const key = frameHandleKey(handle);
11545
- if (cache === null || cache.key !== key) cache = {
11546
- key,
11547
- value: resolve(handle)
11548
- };
11549
- return cache.value;
11550
- };
11551
- }
11552
- //#endregion
11553
11596
  //#region src/shared/frame/encode-crop.ts
11554
11597
  /**
11555
11598
  * JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
@@ -11565,6 +11608,21 @@ async function encodeRgbCropToJpeg(bytes, width, height) {
11565
11608
  channels: 3
11566
11609
  } }).jpeg({ quality: 90 }).toBuffer();
11567
11610
  }
11611
+ /**
11612
+ * Decode a base64 JPEG (the COMPRESSED cross-node crop the owning runner
11613
+ * returns) back into raw RGB (24-bit) pixels, so every downstream consumer
11614
+ * keeps its existing RGB contract regardless of whether the crop was fetched
11615
+ * same-node (raw) or cross-node (JPEG). The decode happens IN-PROCESS on the
11616
+ * post-processing node — no raw pixels ever cross a process boundary.
11617
+ */
11618
+ async function decodeJpegToRgb(base64Jpeg) {
11619
+ const { data, info } = await (0, sharp.default)(Buffer.from(base64Jpeg, "base64")).raw().toBuffer({ resolveWithObject: true });
11620
+ return {
11621
+ bytes: data,
11622
+ width: info.width,
11623
+ height: info.height
11624
+ };
11625
+ }
11568
11626
  //#endregion
11569
11627
  //#region src/pipeline-analytics/pipeline/event-child-crops.ts
11570
11628
  /**
@@ -11725,6 +11783,26 @@ function padBbox(bbox, padding) {
11725
11783
  };
11726
11784
  }
11727
11785
  //#endregion
11786
+ //#region src/pipeline-analytics/pipeline/capture-crop.ts
11787
+ function createCaptureCrop(deps) {
11788
+ return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
11789
+ const paddedNorm = padBbox({
11790
+ x: bbox.x / frameWidth,
11791
+ y: bbox.y / frameHeight,
11792
+ w: bbox.w / frameWidth,
11793
+ h: bbox.h / frameHeight
11794
+ }, padding);
11795
+ const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
11796
+ if (nativeCrop) {
11797
+ deps.bumpCropMetric(true);
11798
+ return nativeCrop;
11799
+ }
11800
+ deps.bumpCropMetric(false);
11801
+ deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", { meta: { nodeId: frameHandle.nodeId } });
11802
+ return null;
11803
+ };
11804
+ }
11805
+ //#endregion
11728
11806
  //#region src/pipeline-analytics/pipeline/dropout-skip.ts
11729
11807
  function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
11730
11808
  if (!cfg.enabled) return false;
@@ -11910,6 +11988,32 @@ async function makeSquareThumb(bytes, size) {
11910
11988
  }).jpeg({ quality: THUMB_QUALITY }).toBuffer();
11911
11989
  }
11912
11990
  //#endregion
11991
+ //#region src/pipeline-analytics/event-thumbnail-resolver.ts
11992
+ /** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
11993
+ * `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
11994
+ * for new tracks (served only for old stored rows). Undefined when the event
11995
+ * owns no media. */
11996
+ function pickEventOwnedMedia(files) {
11997
+ return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
11998
+ }
11999
+ /** 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. */
12002
+ 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];
12004
+ }
12005
+ /**
12006
+ * Resolve the default-path media for an event id: the event's own media when it
12007
+ * has any, else the owning track's fallback media. Returns `null` when neither
12008
+ * yields a blob (caller 404s → the surface shows an icon).
12009
+ */
12010
+ async function resolveDefaultEventMedia(deps) {
12011
+ const owned = pickEventOwnedMedia(deps.eventFiles);
12012
+ if (owned) return owned;
12013
+ const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
12014
+ return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
12015
+ }
12016
+ //#endregion
11913
12017
  //#region src/pipeline-analytics/index.ts
11914
12018
  /**
11915
12019
  * Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
@@ -11984,12 +12088,20 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
11984
12088
  * without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
11985
12089
  * which is how 65% of the timeline tiles rendered as whole scenes. It sits
11986
12090
  * right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
12091
+ *
12092
+ * `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
12093
+ * joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
12094
+ * for tracks whose only clean media is the small key frame; it sits last
12095
+ * (fullest-scene, but subject-centered enough to serve as a reasonable fallback
12096
+ * over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
12097
+ * keep resolving.
11987
12098
  */
11988
12099
  var CLEAN_MEDIA_KINDS = [
11989
12100
  "crop",
11990
12101
  "thumbnail",
11991
12102
  "fullFrame",
11992
- "keyFrame"
12103
+ "keyFrame",
12104
+ "keyFrameSmall"
11993
12105
  ];
11994
12106
  /**
11995
12107
  * Pick a CLEAN (never boxed) media file for a crop-forced request: the exact
@@ -12252,6 +12364,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12252
12364
  * optional `maxWidth` caps the native crop width (used for the full-frame key
12253
12365
  * frame so a 4K native surface never floods the transport). */
12254
12366
  captureCrop = null;
12367
+ /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
12368
+ * to the owning runner, PLUS the source `tier`. Captured in the async init
12369
+ * block so `persistKeyFrames` can fetch the native full frame for the
12370
+ * `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
12371
+ * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
12372
+ getNativeKeyFrameRgb = null;
12255
12373
  shuttingDown = false;
12256
12374
  /** True only on the cluster's designated post-processing node. When false the
12257
12375
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
@@ -12298,7 +12416,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12298
12416
  let storage = this.ctx.kernel.storage;
12299
12417
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
12300
12418
  if (mediaRoot) {
12301
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-D8vIkoch.js"));
12419
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DtSEvIXU.js"));
12302
12420
  storage = new FilesystemStorageProvider(mediaRoot);
12303
12421
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
12304
12422
  }
@@ -12445,6 +12563,23 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12445
12563
  } });
12446
12564
  }
12447
12565
  const pipelineRunnerApi = api.pipelineRunner;
12566
+ const isRemoteHandle = (handle) => handle.nodeId !== ownNodeId;
12567
+ const cropReplyToRgb = async (reply) => {
12568
+ if (reply.jpeg !== void 0) {
12569
+ const rgb = await decodeJpegToRgb(reply.jpeg);
12570
+ return {
12571
+ bytes: Buffer.from(rgb.bytes),
12572
+ width: rgb.width,
12573
+ height: rgb.height
12574
+ };
12575
+ }
12576
+ if (reply.bytes !== void 0) return {
12577
+ bytes: Buffer.from(reply.bytes),
12578
+ width: reply.width,
12579
+ height: reply.height
12580
+ };
12581
+ return null;
12582
+ };
12448
12583
  const getRemoteFrame = async (handle) => {
12449
12584
  if (!pipelineRunnerApi?.getNativeCrop) return null;
12450
12585
  const full = await pipelineRunnerApi.getNativeCrop.query({
@@ -12455,17 +12590,50 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12455
12590
  w: 1,
12456
12591
  h: 1
12457
12592
  },
12458
- maxWidth: handle.width
12593
+ maxWidth: handle.width,
12594
+ encodeJpeg: isRemoteHandle(handle)
12459
12595
  }, require_dist.nodePin(handle.nodeId));
12460
12596
  if (!full || full.width <= 0 || full.height <= 0) return null;
12597
+ const rgb = await cropReplyToRgb(full);
12598
+ if (!rgb) return null;
12461
12599
  return {
12462
- data: Buffer.from(full.bytes),
12463
- width: full.width,
12464
- height: full.height,
12600
+ data: rgb.bytes,
12601
+ width: rgb.width,
12602
+ height: rgb.height,
12465
12603
  format: "rgb",
12466
12604
  timestamp: 0
12467
12605
  };
12468
12606
  };
12607
+ const fetchNativeFullFrameTiered = async (handle, maxWidth) => {
12608
+ if (!pipelineRunnerApi?.getNativeCrop) return null;
12609
+ const full = await pipelineRunnerApi.getNativeCrop.query({
12610
+ handle,
12611
+ bbox: {
12612
+ x: 0,
12613
+ y: 0,
12614
+ w: 1,
12615
+ h: 1
12616
+ },
12617
+ maxWidth,
12618
+ encodeJpeg: isRemoteHandle(handle)
12619
+ }, require_dist.nodePin(handle.nodeId));
12620
+ if (!full || full.width <= 0 || full.height <= 0) return null;
12621
+ const rgb = await cropReplyToRgb(full);
12622
+ if (!rgb) return null;
12623
+ const tier = full.tier === "ram-fullframe" ? "ram-fullframe" : "native";
12624
+ return {
12625
+ frame: {
12626
+ data: rgb.bytes,
12627
+ width: rgb.width,
12628
+ height: rgb.height,
12629
+ format: "rgb",
12630
+ timestamp: 0
12631
+ },
12632
+ tier
12633
+ };
12634
+ };
12635
+ const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
12636
+ this.getNativeKeyFrameRgb = fetchNativeFullFrameTiered;
12469
12637
  const cropMetricLogger = logger.child("NativeCrop");
12470
12638
  let nativeHits = 0;
12471
12639
  let nativeFallbacks = 0;
@@ -12488,14 +12656,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12488
12656
  const native = await pipelineRunnerApi.getNativeCrop.query({
12489
12657
  handle: frameHandle,
12490
12658
  bbox: paddedNorm,
12491
- ...maxWidth !== void 0 ? { maxWidth } : {}
12659
+ ...maxWidth !== void 0 ? { maxWidth } : {},
12660
+ encodeJpeg: isRemoteHandle(frameHandle)
12492
12661
  }, require_dist.nodePin(frameHandle.nodeId));
12493
12662
  if (!native || native.width <= 0 || native.height <= 0) return null;
12494
- return {
12495
- bytes: Buffer.from(native.bytes),
12496
- width: native.width,
12497
- height: native.height
12498
- };
12663
+ return await cropReplyToRgb(native);
12499
12664
  } catch (err) {
12500
12665
  cropMetricLogger.debug("native crop request failed — falling back", { meta: { error: require_dist.errMsg(err) } });
12501
12666
  return null;
@@ -12508,12 +12673,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12508
12673
  };
12509
12674
  const getNativeCropJpeg = async (frameHandle, normalizedBbox, maxWidth) => {
12510
12675
  const native = await fetchNativeCropRgb(frameHandle, normalizedBbox, maxWidth);
12511
- if (!native || !nativeSubjectCropMeetsFloor(native.width, maxWidth)) {
12512
- if (native) cropMetricLogger.debug("native subject crop below floor — local fallback", { meta: {
12513
- width: native.width,
12514
- height: native.height,
12515
- maxWidth
12516
- } });
12676
+ if (!native) {
12517
12677
  bumpCropMetric(false);
12518
12678
  return null;
12519
12679
  }
@@ -12524,28 +12684,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
12524
12684
  this.eventMediaDispatcher = new EventMediaDispatcher({
12525
12685
  getRemoteFrame,
12526
12686
  getNativeCropJpeg,
12687
+ getNativeFullFrameRgb,
12527
12688
  mediaStore: this.mediaStore,
12528
12689
  logger: logger.child("EventMediaDispatcher")
12529
12690
  });
12530
- const resolveFrameShared = createSharedFrameResolver((frameHandle) => resolveFrame(frameHandle, { getRemoteFrame }));
12531
- const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
12532
- const paddedNorm = padBbox({
12533
- x: bbox.x / frameWidth,
12534
- y: bbox.y / frameHeight,
12535
- w: bbox.w / frameWidth,
12536
- h: bbox.h / frameHeight
12537
- }, padding);
12538
- const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
12539
- if (nativeCrop) {
12540
- bumpCropMetric(true);
12541
- return nativeCrop;
12542
- }
12543
- bumpCropMetric(false);
12544
- const decoded = await resolveFrameShared(frameHandle);
12545
- if (!decoded || decoded.format !== "rgb") return null;
12546
- const { crop } = await extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
12547
- return crop;
12548
- };
12691
+ const captureCrop = createCaptureCrop({
12692
+ tryNativeCrop,
12693
+ bumpCropMetric,
12694
+ logger: logger.child("CaptureCrop")
12695
+ });
12549
12696
  this.captureCrop = captureCrop;
12550
12697
  this.faceRecognizer = new FaceRecognizer({
12551
12698
  identityStore: this.identityStore,
@@ -13394,7 +13541,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
13394
13541
  else plateCrops += 1;
13395
13542
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
13396
13543
  const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
13397
- if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle, result.frameWidth, result.frameHeight);
13544
+ if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
13398
13545
  if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
13399
13546
  const captureCounts = {
13400
13547
  events: eventTargets.length,
@@ -14005,30 +14152,54 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14005
14152
  }));
14006
14153
  }
14007
14154
  /**
14008
- * Capture ONE native-resolution KEY FRAME per given track at this best-
14009
- * detection frame and store it (`putReplacing` → one keyFrame per track).
14155
+ * Capture the CLEAN best full frame per given track at this best-detection
14156
+ * moment and store BOTH variants (`putReplacing` → one row per track, per
14157
+ * kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
14158
+ * — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
14159
+ * from the SAME native surface (no second fetch). No box is drawn — these are
14160
+ * the clean hero images (`keyFrameSmall` is the web-friendly one).
14010
14161
  *
14011
- * The full frame is cropped NATIVE-FIRST via `captureCrop`: the request is the
14012
- * FULL frame (no padding) at `KEYFRAME_NATIVE_MAX_WIDTH`, which routes through
14013
- * `pipelineRunner.getNativeCrop` (the decode worker's retained native surface)
14014
- * and only falls back to the ≤640 detection frame when the native lease is
14015
- * gone. The stored key is recorded in `keyFrameKeyByTrackId` so the face /
14016
- * plate / object-embedding rows LINK the SAME native key frame (Design B).
14017
- * Issued in the live-frame window so the native lease is still held. Best-
14018
- * effort (D8) — a per-track failure is logged and never thrown.
14162
+ * NATIVE-OR-NOTHING: a native miss logs `error` and returns (no upscale, no
14163
+ * ≤640 stand-in) the next genuine new-best frame retries. The `keyFrame`
14164
+ * key is recorded in `keyFrameKeyByTrackId` so the face / plate /
14165
+ * object-embedding rows LINK the SAME native key frame (Design B). Issued in
14166
+ * the live-frame window so the native lease is still held. Best-effort (D8) —
14167
+ * a per-track failure is logged and never thrown.
14019
14168
  */
14020
- async persistKeyFrames(deviceId, timestamp, trackIds, frameHandle, frameWidth, frameHeight) {
14021
- const capture = this.captureCrop;
14169
+ async persistKeyFrames(deviceId, timestamp, trackIds, frameHandle) {
14170
+ const getNative = this.getNativeKeyFrameRgb;
14022
14171
  const mediaStore = this.mediaStore;
14023
- if (!capture || !mediaStore) return;
14024
- const req = buildKeyFrameCaptureRequest(frameWidth, frameHeight);
14172
+ if (!getNative || !mediaStore) return;
14025
14173
  const pending = trackIds.filter((id) => !this.keyFrameInFlight.has(id));
14026
14174
  if (pending.length === 0) return;
14027
14175
  for (const id of pending) this.keyFrameInFlight.add(id);
14028
14176
  await Promise.all(pending.map(async (trackId) => {
14029
14177
  try {
14030
- const keyFrame = await capture(frameHandle, req.bbox, frameWidth, frameHeight, req.padding, req.maxWidth);
14031
- if (!keyFrame) return;
14178
+ const result = await getNative(frameHandle, KEYFRAME_NATIVE_MAX_WIDTH);
14179
+ const native = result?.frame;
14180
+ if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
14181
+ this.ctx.logger.error("key-frame native miss — no upscale, will retry next best", {
14182
+ tags: { deviceId },
14183
+ meta: {
14184
+ trackId,
14185
+ shmId: frameHandle.shmId
14186
+ }
14187
+ });
14188
+ return;
14189
+ }
14190
+ const tier = result.tier;
14191
+ if (!keyFrameAcceptsTier(tier)) {
14192
+ this.ctx.logger.warn("keyFrame native miss — RAM tier rejected, will retry", {
14193
+ tags: { deviceId },
14194
+ meta: {
14195
+ trackId,
14196
+ shmId: frameHandle.shmId,
14197
+ tier
14198
+ }
14199
+ });
14200
+ return;
14201
+ }
14202
+ const { keyFrame, keyFrameSmall } = await encodeKeyFrameVariants(native);
14032
14203
  const key = await mediaStore.putReplacing({
14033
14204
  deviceId,
14034
14205
  ownerKind: "track",
@@ -14038,6 +14209,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14038
14209
  data: keyFrame
14039
14210
  });
14040
14211
  this.keyFrameKeyByTrackId.set(trackId, key);
14212
+ await mediaStore.putReplacing({
14213
+ deviceId,
14214
+ ownerKind: "track",
14215
+ ownerId: trackId,
14216
+ kind: "keyFrameSmall",
14217
+ timestamp,
14218
+ data: keyFrameSmall
14219
+ });
14041
14220
  } catch (err) {
14042
14221
  this.ctx.logger.debug("key-frame capture failed", {
14043
14222
  tags: { deviceId },
@@ -14985,11 +15164,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
14985
15164
  if (materializer === null || descriptor === void 0) return;
14986
15165
  try {
14987
15166
  const cameraIds = await cache.camerasFor(data.deviceId);
15167
+ if (cameraIds.length === 0) return;
15168
+ const producingDeviceName = await this.resolveProducingDeviceName(data.deviceId);
14988
15169
  for (const cameraId of cameraIds) await materializer.materialize({
14989
15170
  cameraId,
14990
15171
  sourceDeviceId: data.deviceId,
14991
15172
  kind: descriptor.kind,
14992
- timestamp
15173
+ timestamp,
15174
+ ...producingDeviceName !== void 0 ? { producingDeviceName } : {}
14993
15175
  });
14994
15176
  } catch (err) {
14995
15177
  this.ctx.logger.warn("synthetic sensor-track materialize failed", {
@@ -15001,6 +15183,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15001
15183
  });
15002
15184
  }
15003
15185
  }
15186
+ /**
15187
+ * Resolve the NAME of a device (the linked sensor/control that produced a
15188
+ * synthetic event) via the device-manager cap. Best-effort — returns
15189
+ * undefined on any lookup failure or an unknown device so the synthetic
15190
+ * track still materializes without a label.
15191
+ */
15192
+ async resolveProducingDeviceName(deviceId) {
15193
+ const api = this.ctx?.api;
15194
+ if (!api) return void 0;
15195
+ try {
15196
+ return (await api.deviceManager.getDevice.query({ deviceId }))?.name;
15197
+ } catch (err) {
15198
+ this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
15199
+ tags: { deviceId },
15200
+ meta: { error: require_dist.errMsg(err) }
15201
+ });
15202
+ return;
15203
+ }
15204
+ }
15004
15205
  async clearTracks(input) {
15005
15206
  this.trackStore?.clearDevice(input.deviceId);
15006
15207
  this.stationaryRegistry?.clearDevice(input.deviceId);
@@ -15516,17 +15717,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
15516
15717
  key: clean.key
15517
15718
  };
15518
15719
  }
15519
- const chosenEvent = eventFiles.find((f) => f.kind === "crop") ?? eventFiles.find((f) => f.kind === "fullFrameBoxed") ?? eventFiles[0];
15520
- if (chosenEvent) return {
15521
- bytes: Buffer.from(chosenEvent.base64, "base64"),
15522
- key: chosenEvent.key
15523
- };
15524
- const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15525
- const chosenTrack = trackFiles.find((f) => f.kind === "thumbnail") ?? trackFiles.find((f) => f.kind === "lastFrame") ?? trackFiles.find((f) => f.kind === "firstFrame") ?? [...trackFiles].reverse().find((f) => f.kind === "snapshot") ?? trackFiles[trackFiles.length - 1];
15526
- if (!chosenTrack) return null;
15720
+ const chosen = await resolveDefaultEventMedia({
15721
+ eventId: id,
15722
+ eventFiles,
15723
+ listTrackMedia: (trackId) => this.mediaStore?.listByOwner("track", trackId) ?? Promise.resolve([]),
15724
+ getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
15725
+ });
15726
+ if (!chosen) return null;
15527
15727
  return {
15528
- bytes: Buffer.from(chosenTrack.base64, "base64"),
15529
- key: chosenTrack.key
15728
+ bytes: Buffer.from(chosen.base64, "base64"),
15729
+ key: chosen.key
15530
15730
  };
15531
15731
  }
15532
15732
  /**