@camstack/addon-post-analysis 1.1.42 → 1.2.1

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-Cgv25jQz.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-D4KAS857.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import sharp from "sharp";
4
4
  //#region src/pipeline-analytics/videoclips-provider.ts
@@ -3650,6 +3650,10 @@ var TRACKS_COLUMNS = [
3650
3650
  name: "source",
3651
3651
  type: "TEXT"
3652
3652
  },
3653
+ {
3654
+ name: "producingDeviceName",
3655
+ type: "TEXT"
3656
+ },
3653
3657
  {
3654
3658
  name: "firstSeen",
3655
3659
  type: "INTEGER",
@@ -4357,6 +4361,7 @@ var TrackStore = class {
4357
4361
  className: t.className,
4358
4362
  ...t.label !== void 0 ? { label: t.label } : {},
4359
4363
  source: t.source ?? "sensor",
4364
+ ...t.producingDeviceName !== void 0 ? { producingDeviceName: t.producingDeviceName } : {},
4360
4365
  firstSeen: t.firstSeen,
4361
4366
  lastSeen: t.lastSeen,
4362
4367
  positions: [...t.positions],
@@ -4418,6 +4423,7 @@ var TrackStore = class {
4418
4423
  const classes = data["classes"];
4419
4424
  const label = data["label"];
4420
4425
  const source = data["source"];
4426
+ const producingDeviceName = data["producingDeviceName"];
4421
4427
  const importance = data["importance"];
4422
4428
  const bestEventId = data["bestEventId"];
4423
4429
  const importanceReason = data["importanceReason"];
@@ -4438,6 +4444,7 @@ var TrackStore = class {
4438
4444
  className: String(data["className"]),
4439
4445
  ...typeof label === "string" ? { label } : {},
4440
4446
  ...source === "sensor" || source === "pipeline" ? { source } : {},
4447
+ ...typeof producingDeviceName === "string" ? { producingDeviceName } : {},
4441
4448
  firstSeen: Number(data["firstSeen"]),
4442
4449
  lastSeen: Number(data["lastSeen"]),
4443
4450
  positions,
@@ -4483,6 +4490,7 @@ var PLATE_MEDIA_OWNER_PREFIX = "plate-";
4483
4490
  */
4484
4491
  var SINGLE_INSTANCE_KINDS = new Set([
4485
4492
  "keyFrame",
4493
+ "keyFrameSmall",
4486
4494
  "thumbnail",
4487
4495
  "firstFrame",
4488
4496
  "lastFrame"
@@ -5239,6 +5247,32 @@ var EventStore = class {
5239
5247
  });
5240
5248
  }
5241
5249
  /**
5250
+ * Resolve the OWNING track id for an object event id. Powers the event-media
5251
+ * resolver's track fallback: a new-style object event owns no crop, so its
5252
+ * `mediaUrl` degrades to the track's cadence media, addressed by this id.
5253
+ * Returns `null` when the id is not a persisted object event (e.g. a KeyEvent
5254
+ * whose id is already a track id, or a motion/audio event) — best-effort.
5255
+ */
5256
+ async getTrackIdForEvent(eventId) {
5257
+ try {
5258
+ const row = await this.store.get.query({
5259
+ collection: OBJECT_EVENTS_COLLECTION,
5260
+ key: eventId
5261
+ });
5262
+ if (row !== null && typeof row === "object" && "trackId" in row) {
5263
+ const trackId = row["trackId"];
5264
+ if (typeof trackId === "string" && trackId.length > 0) return trackId;
5265
+ }
5266
+ return null;
5267
+ } catch (err) {
5268
+ this.logger.debug("getTrackIdForEvent failed", { meta: {
5269
+ eventId,
5270
+ error: String(err)
5271
+ } });
5272
+ return null;
5273
+ }
5274
+ }
5275
+ /**
5242
5276
  * Forward-only: set `label` on every already-emitted object event of a track.
5243
5277
  * Returns the number of events updated. Best-effort per row. Used when face
5244
5278
  * recognition assigns a name to a track after its events were emitted.
@@ -6145,6 +6179,7 @@ var LinkedCamerasCache = class {
6145
6179
  for (const cameraId of cameraIds) try {
6146
6180
  const { devices } = await this.deps.linkedDevices.getLinkedDevices({ deviceId: cameraId });
6147
6181
  for (const d of devices) {
6182
+ if (d.producesTrackedEvents === false) continue;
6148
6183
  const list = next.get(d.deviceId);
6149
6184
  if (list === void 0) next.set(d.deviceId, [cameraId]);
6150
6185
  else if (!list.includes(cameraId)) list.push(cameraId);
@@ -6211,10 +6246,13 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
6211
6246
  *
6212
6247
  * A linked sensor/control state change is projected into the UNIFIED track
6213
6248
  * store as a SYNTHETIC track: `className` = the event kind (`contact` /
6214
- * `lock` / `switch` …), `source: 'sensor'`, media = an on-demand snapshot of
6249
+ * `lock` / `switch` …), `source: 'sensor'`, media = an on-demand best shot of
6215
6250
  * the linked camera in the SAME `getTrackMedia` shape the UIs consume (no
6216
- * bbox → full frame), `positions: []`. It joins the timeline via the existing
6217
- * time-based clustering no new join mechanism.
6251
+ * bbox → full frame), persisted under `kind: 'keyFrameSmall'` the kind the
6252
+ * viewer representative-image selectors (hero/clean/reel) actually consume; a
6253
+ * `'snapshot'`-kind blob is ignored by those chains, which is what stuck the
6254
+ * pa-synth reel on "loading". `positions: []`. It joins the timeline via the
6255
+ * existing time-based clustering — no new join mechanism.
6218
6256
  *
6219
6257
  * The raw `SensorEvent` remains the durable record; this track is its
6220
6258
  * projection. Snapshots are debounced per `(camera, kind)` so a chattery
@@ -6222,6 +6260,28 @@ async function downscaleFullFrameJpeg(jpeg, maxWidth = 640, maxHeight = 360) {
6222
6260
  * `source:'sensor'` tracks (they carry no trajectory).
6223
6261
  */
6224
6262
  var DEFAULT_DEBOUNCE_MS = 1e4;
6263
+ /** A hanging snapshot cap must not stall the sensor state-change path. */
6264
+ var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
6265
+ /** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
6266
+ var SnapshotTimeoutError = class extends Error {
6267
+ constructor(ms) {
6268
+ super(`snapshot cap timed out after ${ms}ms`);
6269
+ this.name = "SnapshotTimeoutError";
6270
+ }
6271
+ };
6272
+ /** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
6273
+ function withTimeout$1(promise, ms) {
6274
+ return new Promise((resolve, reject) => {
6275
+ const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
6276
+ promise.then((value) => {
6277
+ clearTimeout(timer);
6278
+ resolve(value);
6279
+ }, (err) => {
6280
+ clearTimeout(timer);
6281
+ reject(err instanceof Error ? err : new Error(String(err)));
6282
+ });
6283
+ });
6284
+ }
6225
6285
  /** Full-frame, position-less placeholder (spatial consumers skip these). */
6226
6286
  function zeroPosition(timestamp) {
6227
6287
  return {
@@ -6239,6 +6299,7 @@ function zeroPosition(timestamp) {
6239
6299
  var SyntheticSensorTrackMaterializer = class {
6240
6300
  deps;
6241
6301
  debounceMs;
6302
+ snapshotTimeoutMs;
6242
6303
  makeId;
6243
6304
  now;
6244
6305
  /** Last snapshot time per `${cameraId}:${kind}`. */
@@ -6246,10 +6307,29 @@ var SyntheticSensorTrackMaterializer = class {
6246
6307
  constructor(deps) {
6247
6308
  this.deps = deps;
6248
6309
  this.debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
6310
+ this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS;
6249
6311
  this.makeId = deps.makeId ?? (() => `pa-synth-${randomUUID()}`);
6250
6312
  this.now = deps.now ?? Date.now;
6251
6313
  }
6252
6314
  /**
6315
+ * Fetch the linked camera's snapshot under a bounded timeout. A hanging
6316
+ * snapshot cap is skipped (returns null + warns) so the track still lands;
6317
+ * a null / failed fetch is likewise reported and skipped.
6318
+ */
6319
+ async fetchSnapshot(cameraId) {
6320
+ try {
6321
+ const snap = await withTimeout$1(this.deps.snapshot.getSnapshot({
6322
+ deviceId: cameraId,
6323
+ force: true
6324
+ }), this.snapshotTimeoutMs);
6325
+ if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
6326
+ return snap;
6327
+ } catch (err) {
6328
+ this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err);
6329
+ return null;
6330
+ }
6331
+ }
6332
+ /**
6253
6333
  * Materialize a synthetic track for a sensor/control event on `cameraId`.
6254
6334
  * Returns the persisted track, or null when debounced. Snapshot failure
6255
6335
  * still lands a track (with no media) — it never blocks the sensor record.
@@ -6262,28 +6342,23 @@ var SyntheticSensorTrackMaterializer = class {
6262
6342
  const trackId = this.makeId();
6263
6343
  const ts = input.timestamp;
6264
6344
  let mediaKey = null;
6265
- try {
6266
- const snap = await this.deps.snapshot.getSnapshot({
6345
+ const snap = await this.fetchSnapshot(input.cameraId);
6346
+ if (snap !== null) try {
6347
+ const raw = Buffer.from(snap.base64, "base64");
6348
+ let data = raw;
6349
+ try {
6350
+ data = await downscaleFullFrameJpeg(raw, 960, 540);
6351
+ } catch (err) {
6352
+ this.deps.onError?.("downscaleSnapshot", err);
6353
+ }
6354
+ mediaKey = await this.deps.media.put({
6267
6355
  deviceId: input.cameraId,
6268
- force: true
6356
+ ownerKind: "track",
6357
+ ownerId: trackId,
6358
+ kind: "keyFrameSmall",
6359
+ timestamp: ts,
6360
+ data
6269
6361
  });
6270
- if (snap !== null) {
6271
- const raw = Buffer.from(snap.base64, "base64");
6272
- let data = raw;
6273
- try {
6274
- data = await downscaleFullFrameJpeg(raw);
6275
- } catch (err) {
6276
- this.deps.onError?.("downscaleSnapshot", err);
6277
- }
6278
- mediaKey = await this.deps.media.put({
6279
- deviceId: input.cameraId,
6280
- ownerKind: "track",
6281
- ownerId: trackId,
6282
- kind: "snapshot",
6283
- timestamp: ts,
6284
- data
6285
- });
6286
- } else this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
6287
6362
  } catch (err) {
6288
6363
  this.deps.onError?.("snapshotMedia", err);
6289
6364
  }
@@ -6292,6 +6367,7 @@ var SyntheticSensorTrackMaterializer = class {
6292
6367
  deviceId: input.cameraId,
6293
6368
  className: input.kind,
6294
6369
  source: "sensor",
6370
+ ...input.producingDeviceName !== void 0 ? { producingDeviceName: input.producingDeviceName } : {},
6295
6371
  firstSeen: ts,
6296
6372
  lastSeen: ts,
6297
6373
  positions: [],
@@ -6324,6 +6400,22 @@ async function resolveFrame(handle, deps) {
6324
6400
  return deps.getRemoteFrame(handle);
6325
6401
  }
6326
6402
  //#endregion
6403
+ //#region src/shared/frame/shared-frame-resolver.ts
6404
+ function frameHandleKey(h) {
6405
+ return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
6406
+ }
6407
+ function createSharedFrameResolver(resolve) {
6408
+ let cache = null;
6409
+ return (handle) => {
6410
+ const key = frameHandleKey(handle);
6411
+ if (cache === null || cache.key !== key) cache = {
6412
+ key,
6413
+ value: resolve(handle)
6414
+ };
6415
+ return cache.value;
6416
+ };
6417
+ }
6418
+ //#endregion
6327
6419
  //#region src/shared/frame/square-safe-crop.ts
6328
6420
  /**
6329
6421
  * Compute a square-safe 16:9 crop region in pixel space.
@@ -6452,27 +6544,30 @@ async function drawBoxedFrame(frameData, frameWidth, frameHeight, boxes, opts =
6452
6544
  }
6453
6545
  //#endregion
6454
6546
  //#region src/pipeline-analytics/services/event-media-dispatcher.ts
6455
- /** All event media is stored at NATIVE resolution (no downscale), high quality.
6456
- * Small downscaled `thumbnail`s are intentionally left out for now — when we
6457
- * reintroduce them they'll be a separate small kind. */
6458
- var MEDIA_QUALITY = 88;
6459
- /** Output dimensions for the LOCAL-FALLBACK square-safe 16:9 crops
6460
- * (crop/faceCrop/plateCrop/thumbnail) cut from the resolved ≤640 frame. */
6461
- var CROP_WIDTH = 640;
6462
- var CROP_HEIGHT = 360;
6463
- var CROP_QUALITY = 80;
6464
6547
  /**
6465
- * Native-surface crop width cap for subject crops (`crop`/`thumbnail`/
6466
- * `faceCrop`/`plateCrop`). ~960px yields a sharp gallery/reel tile AND a
6467
- * ≥224px classifier input straight from the runner's native surface, WITHOUT
6468
- * hauling a full 1920px frame per subject (that width is reserved for the
6469
- * full-frame `keyFrame`). It is an UPPER bound only: a native miss falls back to
6470
- * the ≤640 local crop, so quality never regresses below today's behaviour.
6548
+ * Long-side cap (px) for the boxed timeline tiles — `firstFrame`, `snapshot`,
6549
+ * `lastFrame`. These are the low-res "timeline filmstrip" images: a native full
6550
+ * frame downscaled to 960 with ONLY the track's own box burned in. A native miss
6551
+ * falls back to the ≤640 detection frame (an honest lower-res frame, NEVER an
6552
+ * upscale).
6471
6553
  */
6472
- var NATIVE_SUBJECT_CROP_MAX_WIDTH = 960;
6473
- function caption(className, confidence, label) {
6474
- const base = label && label !== className ? `${className} ${label}` : className;
6475
- return typeof confidence === "number" ? `${base} ${Math.round(confidence * 100)}%` : base;
6554
+ var SNAPSHOT_MAX_WIDTH = 960;
6555
+ /**
6556
+ * Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
6557
+ * ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
6558
+ * surface). Both frames show the SAME scene at the same aspect ratio, so a
6559
+ * uniform per-axis scale places the box correctly on the higher-res frame.
6560
+ */
6561
+ function scaleBox(bbox, fromW, fromH, toW, toH, label) {
6562
+ const sx = toW / fromW;
6563
+ const sy = toH / fromH;
6564
+ return {
6565
+ x: bbox.x * sx,
6566
+ y: bbox.y * sy,
6567
+ w: bbox.w * sx,
6568
+ h: bbox.h * sy,
6569
+ ...label ? { label } : {}
6570
+ };
6476
6571
  }
6477
6572
  /**
6478
6573
  * True when a packed-RGB frame is (near-)uniform in every channel — the
@@ -6504,20 +6599,41 @@ function isUniformRgbFrame(data, width, height) {
6504
6599
  }
6505
6600
  /**
6506
6601
  * Generates object-event + track media FROM the decoded detection-pipeline
6507
- * frame (the `frameHandle`/shm frame the detector ran on), NEVER the device
6508
- * snapshot cap. Per object event it writes: a `crop` (square-safe 16:9
6509
- * region, 640×360, q80), and — at ORIGINAL resolution — a clear `fullFrame`
6510
- * (no boxes) + a `fullFrameBoxed` (this event's box drawn) for the detail
6511
- * view. Per new track a boxed `firstFrame`. Child bboxes produce
6512
- * `faceCrop`/`plateCrop` with the same square-safe algorithm.
6513
- * The frame is resolved ONCE per call and reused across all
6514
- * targets — frames recycle in milliseconds, so the caller must invoke this
6515
- * immediately after producing the events while the handle is still live.
6602
+ * frame (the `frameHandle`/native surface the detector ran on), NEVER the device
6603
+ * snapshot cap.
6604
+ *
6605
+ * Per object event it writes ONLY the enrichment child crops
6606
+ * (`faceCrop`/`plateCrop`) the retired `crop`/`fullFrame`/`fullFrameBoxed`
6607
+ * kinds are no longer produced (2026-07-21; old stored tracks keep reading them,
6608
+ * the enum is unchanged). The representative event image is now the track
6609
+ * `thumbnail`/`keyFrame`.
6610
+ *
6611
+ * Per track it writes the boxed timeline tiles — a `firstFrame` on track start,
6612
+ * plus periodic `snapshot`/`lastFrame` — each cut from the runner's NATIVE full
6613
+ * frame downscaled to {@link SNAPSHOT_MAX_WIDTH} (960) with ONLY the track's own
6614
+ * box burned in, falling back to the ≤640 detection frame (honest lower res,
6615
+ * never an upscale) on a native miss. The best `thumbnail` stays a clean,
6616
+ * uncapped native subject crop (native-or-nothing).
6617
+ *
6618
+ * The detection frame is resolved ONCE per call (blank-frame guard); native full
6619
+ * frames + subject crops are fetched per target by handle. Frames recycle in
6620
+ * milliseconds, so the caller must invoke this immediately after producing the
6621
+ * events while the handle is still live.
6516
6622
  */
6517
6623
  var EventMediaDispatcher = class {
6518
6624
  deps;
6625
+ /**
6626
+ * Per-frame single-slot memo of the native 960 full frame (S3): every track
6627
+ * tile in ONE `captureForFrame` pass shares the SAME `frameHandle`, so multiple
6628
+ * `boxedTimelineFrame` calls would otherwise each round-trip the runner for the
6629
+ * identical native full frame. Keyed by frame identity (`createSharedFrameResolver`)
6630
+ * so a recycled slot (new `seq`) is a fresh fetch. `maxWidth` is always
6631
+ * {@link SNAPSHOT_MAX_WIDTH} here (the only dispatcher use of the fetch).
6632
+ */
6633
+ sharedNativeFullFrame;
6519
6634
  constructor(deps) {
6520
6635
  this.deps = deps;
6636
+ this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
6521
6637
  }
6522
6638
  async captureForFrame(input) {
6523
6639
  const { deviceId, frameHandle, events, trackFrames } = input;
@@ -6576,8 +6692,8 @@ var EventMediaDispatcher = class {
6576
6692
  });
6577
6693
  return empty;
6578
6694
  }
6579
- for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, input.cropPadding);
6580
- for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
6695
+ for (const ev of events) await this.writeEventMedia(deviceId, frameHandle, fw, fh, ev, input.cropPadding);
6696
+ for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf);
6581
6697
  const storedSnapshots = [];
6582
6698
  const thumbnailTrackIds = [];
6583
6699
  for (const sn of snapshots) {
@@ -6608,21 +6724,7 @@ var EventMediaDispatcher = class {
6608
6724
  thumbnailWritten: false
6609
6725
  };
6610
6726
  let boxed = null;
6611
- if (sn.appendSnapshot || sn.rollingLastFrame) try {
6612
- boxed = await drawBoxedFrame(frameData, fw, fh, [{
6613
- ...sn.bbox,
6614
- ...sn.label ? { label: sn.label } : {}
6615
- }], { quality: MEDIA_QUALITY });
6616
- } catch (err) {
6617
- this.deps.logger.warn("event media: track snapshot encode failed", {
6618
- tags: { deviceId },
6619
- meta: {
6620
- deviceId,
6621
- trackId: sn.trackId,
6622
- error: err instanceof Error ? err.message : String(err)
6623
- }
6624
- });
6625
- }
6727
+ if (sn.appendSnapshot || sn.rollingLastFrame) boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, sn.trackId, sn.bbox, sn.label);
6626
6728
  let stored = null;
6627
6729
  if (sn.appendSnapshot && boxed) try {
6628
6730
  const mediaKey = await this.deps.mediaStore.put({
@@ -6642,19 +6744,9 @@ var EventMediaDispatcher = class {
6642
6744
  } catch {}
6643
6745
  if (sn.rollingLastFrame && boxed) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
6644
6746
  let thumbnailWritten = false;
6645
- if (sn.bestThumbnail) try {
6646
- const crop = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, sn.bbox, cropPadding);
6647
- thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
6648
- } catch (err) {
6649
- this.deps.logger.warn("event media: track thumbnail crop failed", {
6650
- tags: { deviceId },
6651
- meta: {
6652
- deviceId,
6653
- trackId: sn.trackId,
6654
- error: err instanceof Error ? err.message : String(err)
6655
- }
6656
- });
6657
- if (boxed) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
6747
+ if (sn.bestThumbnail) {
6748
+ const crop = await this.cropSubjectRegion(frameHandle, fw, fh, sn.bbox, cropPadding);
6749
+ if (crop) thumbnailWritten = await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, crop);
6658
6750
  }
6659
6751
  return {
6660
6752
  storedSnapshot: stored,
@@ -6666,143 +6758,126 @@ var EventMediaDispatcher = class {
6666
6758
  * object-event `crop` kind, the child `faceCrop`/`plateCrop`, and the track
6667
6759
  * `thumbnail`: the square-safe 16:9 region around the bbox with NO box drawn.
6668
6760
  *
6669
- * NATIVE-FIRST: the region is requested from the runner's retained native
6670
- * surface (normalized [0,1] coords map directly onto it), downscaled to
6671
- * {@link NATIVE_SUBJECT_CROP_MAX_WIDTH} a sharp tile at native detail. On any
6672
- * miss/error (or a runner without the method) it FALLS BACK to cropping the
6673
- * resolved ≤640 frame locally (today's behaviour), so quality never regresses.
6674
- * Both paths run inside the live-handle window opened by `captureForFrame`.
6761
+ * NATIVE-OR-NOTHING: the region is requested from the runner's retained native
6762
+ * surface (normalized [0,1] coords map directly onto it) with NO `maxWidth` —
6763
+ * the crop at TRUE native resolution (decision #3). On any miss/error (or a
6764
+ * runner without the method) it returns `null` after a loud `logger.warn`; the
6765
+ * caller SKIPS the write and the per-frame retry lands a real native crop
6766
+ * later. It is NEVER upscaled an upscaled ≤640 tile is a blurred lie
6767
+ * (case-study lapVar 5–7), so no local resize fallback exists. Runs inside the
6768
+ * live-handle window opened by `captureForFrame`.
6675
6769
  */
6676
- async cropSubjectRegion(frameHandle, frameData, fw, fh, bbox, cropPadding) {
6677
- if (this.deps.getNativeCropJpeg) try {
6770
+ async cropSubjectRegion(frameHandle, fw, fh, bbox, cropPadding) {
6771
+ if (!this.deps.getNativeCropJpeg) {
6772
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
6773
+ shmId: frameHandle.shmId,
6774
+ reason: "no-native-cap"
6775
+ } });
6776
+ return null;
6777
+ }
6778
+ try {
6678
6779
  const norm = squareSafeCropRegionNormalized(bbox, {
6679
6780
  W: fw,
6680
6781
  H: fh
6681
6782
  }, cropPadding);
6682
- const native = await this.deps.getNativeCropJpeg(frameHandle, norm, NATIVE_SUBJECT_CROP_MAX_WIDTH);
6783
+ const native = await this.deps.getNativeCropJpeg(frameHandle, norm);
6683
6784
  if (native) return native;
6785
+ this.deps.logger.warn("native subject crop miss — will retry, no upscale", { meta: {
6786
+ shmId: frameHandle.shmId,
6787
+ reason: "native-miss"
6788
+ } });
6789
+ return null;
6684
6790
  } catch (err) {
6685
- this.deps.logger.debug("event media: native subject crop failedlocal fallback", { meta: {
6791
+ this.deps.logger.warn("native subject crop misswill retry, no upscale", { meta: {
6686
6792
  shmId: frameHandle.shmId,
6687
6793
  error: err instanceof Error ? err.message : String(err)
6688
6794
  } });
6795
+ return null;
6689
6796
  }
6690
- return this.cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding);
6691
6797
  }
6692
6798
  /**
6693
- * Local-fallback square-safe 16:9 crop out of the resolved ≤640 frame, resized
6694
- * to 640×360, JPEG q80. Used when the native surface is unavailable/missed.
6799
+ * The boxed timeline tile (`firstFrame`/`snapshot`/`lastFrame`): the track's
6800
+ * OWN box burned onto the NATIVE full frame downscaled to
6801
+ * {@link SNAPSHOT_MAX_WIDTH}. On a native miss it falls back to the ≤640
6802
+ * detection frame (`frameData`) downscaled to the same cap with a `logger.warn`
6803
+ * — an honest LOWER-res frame, NEVER an upscale. Returns the JPEG, or `null`
6804
+ * only if the sharp encode itself throws.
6695
6805
  */
6696
- async cropSubjectRegionLocal(frameData, fw, fh, bbox, cropPadding) {
6697
- const region = squareSafeCropRegion(bbox, {
6698
- W: fw,
6699
- H: fh
6700
- }, cropPadding);
6701
- const left = Math.max(0, Math.min(region.x, fw - 1));
6702
- const top = Math.max(0, Math.min(region.y, fh - 1));
6703
- const width = Math.max(1, Math.min(region.w, fw - left));
6704
- const height = Math.max(1, Math.min(region.h, fh - top));
6705
- return await sharp(frameData, { raw: {
6706
- width: fw,
6707
- height: fh,
6708
- channels: 3
6709
- } }).extract({
6710
- left,
6711
- top,
6712
- width,
6713
- height
6714
- }).resize(CROP_WIDTH, CROP_HEIGHT).jpeg({ quality: CROP_QUALITY }).toBuffer();
6715
- }
6716
- async replaceKind(deviceId, trackId, kind, timestamp, data) {
6806
+ async boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, ownerId, bbox, label) {
6807
+ let nf = null;
6808
+ let fetchThrew = false;
6717
6809
  try {
6718
- await this.deps.mediaStore.putReplacing({
6719
- deviceId,
6720
- ownerKind: "track",
6721
- ownerId: trackId,
6722
- kind,
6723
- timestamp,
6724
- data
6725
- });
6726
- return true;
6810
+ nf = await this.sharedNativeFullFrame(frameHandle);
6727
6811
  } catch (err) {
6728
- this.deps.logger.debug(`event media: ${kind} replace failed`, {
6812
+ fetchThrew = true;
6813
+ this.deps.logger.warn("native full frame fetch threw — 640 boxed fallback", {
6729
6814
  tags: { deviceId },
6730
6815
  meta: {
6731
6816
  deviceId,
6732
- trackId,
6817
+ ownerId,
6733
6818
  error: err instanceof Error ? err.message : String(err)
6734
6819
  }
6735
6820
  });
6736
- return false;
6737
6821
  }
6738
- }
6739
- async writeEventMedia(deviceId, frameHandle, frameData, fw, fh, ev, cropPadding) {
6740
- const box = {
6741
- ...ev.bbox,
6742
- label: caption(ev.className, ev.confidence, ev.label)
6743
- };
6744
6822
  try {
6745
- const crop = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, ev.bbox, cropPadding);
6746
- await this.deps.mediaStore.put({
6747
- deviceId,
6748
- ownerKind: "event",
6749
- ownerId: ev.eventId,
6750
- kind: "crop",
6751
- timestamp: ev.timestamp,
6752
- data: crop
6753
- });
6754
- } catch (err) {
6755
- this.deps.logger.warn("event media: crop failed", {
6823
+ if (nf && nf.format === "rgb" && nf.width > 0 && nf.height > 0) {
6824
+ const box = scaleBox(bbox, fw, fh, nf.width, nf.height, label);
6825
+ return await drawBoxedFrame(nf.data, nf.width, nf.height, [box], { quality: 88 });
6826
+ }
6827
+ if (!fetchThrew) this.deps.logger.warn("native full frame miss — 640 boxed fallback (no upscale)", {
6756
6828
  tags: { deviceId },
6757
6829
  meta: {
6758
6830
  deviceId,
6759
- eventId: ev.eventId,
6760
- error: err instanceof Error ? err.message : String(err)
6831
+ ownerId,
6832
+ shmId: frameHandle.shmId
6761
6833
  }
6762
6834
  });
6763
- }
6764
- try {
6765
- const fullFrame = await drawBoxedFrame(frameData, fw, fh, [], { quality: MEDIA_QUALITY });
6766
- await this.deps.mediaStore.put({
6767
- deviceId,
6768
- ownerKind: "event",
6769
- ownerId: ev.eventId,
6770
- kind: "fullFrame",
6771
- timestamp: ev.timestamp,
6772
- data: fullFrame
6835
+ return await drawBoxedFrame(frameData, fw, fh, [{
6836
+ ...bbox,
6837
+ ...label ? { label } : {}
6838
+ }], {
6839
+ quality: 88,
6840
+ maxWidth: SNAPSHOT_MAX_WIDTH
6773
6841
  });
6774
6842
  } catch (err) {
6775
- this.deps.logger.warn("event media: clear full frame failed", {
6843
+ this.deps.logger.warn("event media: timeline frame encode failed", {
6776
6844
  tags: { deviceId },
6777
6845
  meta: {
6778
6846
  deviceId,
6779
- eventId: ev.eventId,
6847
+ ownerId,
6780
6848
  error: err instanceof Error ? err.message : String(err)
6781
6849
  }
6782
6850
  });
6851
+ return null;
6783
6852
  }
6853
+ }
6854
+ async replaceKind(deviceId, trackId, kind, timestamp, data) {
6784
6855
  try {
6785
- const fullFrameBoxed = await drawBoxedFrame(frameData, fw, fh, [box], { quality: MEDIA_QUALITY });
6786
- await this.deps.mediaStore.put({
6856
+ await this.deps.mediaStore.putReplacing({
6787
6857
  deviceId,
6788
- ownerKind: "event",
6789
- ownerId: ev.eventId,
6790
- kind: "fullFrameBoxed",
6791
- timestamp: ev.timestamp,
6792
- data: fullFrameBoxed
6858
+ ownerKind: "track",
6859
+ ownerId: trackId,
6860
+ kind,
6861
+ timestamp,
6862
+ data
6793
6863
  });
6864
+ return true;
6794
6865
  } catch (err) {
6795
- this.deps.logger.warn("event media: boxed full frame failed", {
6866
+ this.deps.logger.debug(`event media: ${kind} replace failed`, {
6796
6867
  tags: { deviceId },
6797
6868
  meta: {
6798
6869
  deviceId,
6799
- eventId: ev.eventId,
6870
+ trackId,
6800
6871
  error: err instanceof Error ? err.message : String(err)
6801
6872
  }
6802
6873
  });
6874
+ return false;
6803
6875
  }
6876
+ }
6877
+ async writeEventMedia(deviceId, frameHandle, fw, fh, ev, cropPadding) {
6804
6878
  if (ev.childCrops) for (const child of ev.childCrops) try {
6805
- const childCropData = await this.cropSubjectRegion(frameHandle, frameData, fw, fh, child.bbox, cropPadding);
6879
+ const childCropData = await this.cropSubjectRegion(frameHandle, fw, fh, child.bbox, cropPadding);
6880
+ if (!childCropData) continue;
6806
6881
  await this.deps.mediaStore.put({
6807
6882
  deviceId,
6808
6883
  ownerKind: "event",
@@ -6822,13 +6897,10 @@ var EventMediaDispatcher = class {
6822
6897
  });
6823
6898
  }
6824
6899
  }
6825
- async writeTrackFrame(deviceId, frameData, fw, fh, tf) {
6826
- const box = {
6827
- ...tf.bbox,
6828
- ...tf.label ? { label: tf.label } : {}
6829
- };
6900
+ async writeTrackFrame(deviceId, frameHandle, frameData, fw, fh, tf) {
6901
+ const boxed = await this.boxedTimelineFrame(deviceId, frameHandle, frameData, fw, fh, tf.trackId, tf.bbox, tf.label);
6902
+ if (!boxed) return;
6830
6903
  try {
6831
- const boxed = await drawBoxedFrame(frameData, fw, fh, [box], { quality: MEDIA_QUALITY });
6832
6904
  await this.deps.mediaStore.put({
6833
6905
  deviceId,
6834
6906
  ownerKind: "track",
@@ -8704,34 +8776,13 @@ function isPlausibleThumbnailBox(bbox, frameWidth, frameHeight) {
8704
8776
  }
8705
8777
  //#endregion
8706
8778
  //#region src/pipeline-analytics/pipeline/key-frame-capture.ts
8707
- /**
8708
- * Pure helpers for the per-track NATIVE-resolution `keyFrame` capture
8709
- * (Design B — one native full-frame per track at its best-detection moment).
8710
- *
8711
- * ## Why this exists (the missing native keyFrame)
8712
- *
8713
- * `keyFrame` was historically captured ONLY inside the CLIP object-embedding
8714
- * best path (`persistObjectEmbeddingBests`, gated by `isClipObjectEmbedding`).
8715
- * Under the two-plane pipeline the root frame carries NO CLIP embedding (clip is
8716
- * a per-track DETAIL served via `runDetailSubtree`, and is disabled cluster-
8717
- * wide), so that gate was never satisfied and the native `keyFrame` was NEVER
8718
- * produced — every stored frame stayed at the ≤640×360 detection resolution.
8719
- *
8720
- * The fix decouples the `keyFrame` from the clip path: it is captured on the
8721
- * GENERAL best-frame signal (the same `bestThumbnail` decision that drives the
8722
- * `thumbnail`), reusing the WORKING native crop path (`captureCrop` →
8723
- * `pipelineRunner.getNativeCrop`, which cuts the ROI from the decode worker's
8724
- * retained NATIVE surface and only falls back to the detection frame on a miss).
8725
- * A full-frame ROI at {@link KEYFRAME_NATIVE_MAX_WIDTH} therefore yields a frame
8726
- * LARGER than the detection raster (up to the cap), which is the whole point of
8727
- * the `keyFrame` kind.
8728
- */
8729
8779
  /** Cap (px) on the width of the native KEY FRAME (full-frame native capture).
8730
- * Native resolution is the point, but a full 4K RGB surface over the transport
8731
- * per new-best is wasteful for a web detail view 1920px keeps a sharp native
8732
- * frame while bounding the copy (a miss falls back to the detection-res frame,
8733
- * which is already ≤640px). */
8734
- var KEYFRAME_NATIVE_MAX_WIDTH = 1920;
8780
+ * Native resolution is the point: a 4K cam (3840) resolves TRUE native (no
8781
+ * resize, 3840 4096); an 8K cam is bounded to 4096 (still far past the retired
8782
+ * 1920 cap). This honours "native, no 1920 intermediate" while keeping the raw
8783
+ * rgb transfer wire-safe. Env-overridable (`CAMSTACK_KEYFRAME_NATIVE_MAX_WIDTH`)
8784
+ * for a homelab that wants a tighter/looser bound. */
8785
+ var KEYFRAME_NATIVE_MAX_WIDTH = Number.parseInt(process.env.CAMSTACK_KEYFRAME_NATIVE_MAX_WIDTH ?? "", 10) || 4096;
8735
8786
  /**
8736
8787
  * The tracks whose native `keyFrame` should be (re)captured THIS frame: exactly
8737
8788
  * the tracks that hit a GENUINE new best-frame moment (`keyFrame`), NOT the
@@ -8742,31 +8793,39 @@ function selectKeyFrameTrackIds(targets) {
8742
8793
  return targets.filter((t) => t.keyFrame).map((t) => t.trackId);
8743
8794
  }
8744
8795
  /**
8745
- * Build the `captureCrop` request for a track's native `keyFrame`: the FULL
8746
- * frame (no padding) at the native width cap. The full-frame box is what makes
8747
- * the capture route through the native surface at native resolution instead of
8748
- * a tight ≤640 detection crop.
8796
+ * Gate for the clean best-shot native `keyFrame` (design 2026-07-21): the keyFrame
8797
+ * is the track's CLEAN native best frame and MUST NEVER be persisted from the
8798
+ * ≤640 RAM fallback (`tier: 'ram-fullframe'`) that would pin the stored keyFrame
8799
+ * at detection resolution. On a `ram-fullframe` tier the caller drops the write
8800
+ * and the per-frame re-fire retries a real native surface later.
8801
+ *
8802
+ * An ABSENT tier (`undefined`, from a pre-tier runner on a mixed-version cluster)
8803
+ * is ACCEPTED exactly as before the tier field existed — back-compat, never break
8804
+ * mixed-version clusters. Only an explicit `ram-fullframe` is rejected.
8805
+ *
8806
+ * NOTE: this gates the native `keyFrame` ONLY. The 960 boxed TIMELINE tiles keep
8807
+ * accepting the ≤640 fallback (an honest low-res timeline is acceptable there).
8749
8808
  */
8750
- function buildKeyFrameCaptureRequest(frameWidth, frameHeight) {
8751
- return {
8752
- bbox: {
8753
- x: 0,
8754
- y: 0,
8755
- w: frameWidth,
8756
- h: frameHeight
8757
- },
8758
- padding: 0,
8759
- maxWidth: KEYFRAME_NATIVE_MAX_WIDTH
8760
- };
8809
+ function keyFrameAcceptsTier(tier) {
8810
+ return tier !== "ram-fullframe";
8761
8811
  }
8762
8812
  /**
8763
- * True when a native subject crop of `width` px is a plausible native hit worth
8764
- * keeping, rather than a sub-threshold fallback stamp. Clamps the floor to the
8765
- * requested `maxWidth` so a caller that legitimately asked for a narrow crop
8766
- * (`maxWidth < MIN`) is not rejected for honouring its own cap.
8813
+ * Encode BOTH key-frame variants from a single decoded native full frame. No box
8814
+ * is drawn (the `keyFrame` is the clean best full frame); `drawBoxedFrame([])`
8815
+ * is used purely as the raw-rgb JPEG (+ optional downscale) encoder so the two
8816
+ * variants share one code path. The caller has ALREADY fetched the native frame
8817
+ * at {@link KEYFRAME_NATIVE_MAX_WIDTH}, so `keyFrame` re-encodes it verbatim
8818
+ * (no resize) and `keyFrameSmall` downscales the same buffer to ≤960 —
8819
+ * `withoutEnlargement` never upscales a sub-960 native frame.
8767
8820
  */
8768
- function nativeSubjectCropMeetsFloor(width, maxWidth) {
8769
- return width >= Math.min(320, maxWidth);
8821
+ async function encodeKeyFrameVariants(native) {
8822
+ return {
8823
+ keyFrame: await drawBoxedFrame(native.data, native.width, native.height, [], { quality: 88 }),
8824
+ keyFrameSmall: await drawBoxedFrame(native.data, native.width, native.height, [], {
8825
+ quality: 88,
8826
+ maxWidth: 960
8827
+ })
8828
+ };
8770
8829
  }
8771
8830
  //#endregion
8772
8831
  //#region src/pipeline-analytics/track-retention-sweep.ts
@@ -11529,22 +11588,6 @@ var PlateRecognizer = class {
11529
11588
  }
11530
11589
  };
11531
11590
  //#endregion
11532
- //#region src/shared/frame/shared-frame-resolver.ts
11533
- function frameHandleKey(h) {
11534
- return `${h.nodeId}:${h.shmId}:${h.slot}:${h.seq}`;
11535
- }
11536
- function createSharedFrameResolver(resolve) {
11537
- let cache = null;
11538
- return (handle) => {
11539
- const key = frameHandleKey(handle);
11540
- if (cache === null || cache.key !== key) cache = {
11541
- key,
11542
- value: resolve(handle)
11543
- };
11544
- return cache.value;
11545
- };
11546
- }
11547
- //#endregion
11548
11591
  //#region src/shared/frame/encode-crop.ts
11549
11592
  /**
11550
11593
  * JPEG-encode an already-cropped raw RGB (24-bit) buffer. Used for the
@@ -11720,6 +11763,26 @@ function padBbox(bbox, padding) {
11720
11763
  };
11721
11764
  }
11722
11765
  //#endregion
11766
+ //#region src/pipeline-analytics/pipeline/capture-crop.ts
11767
+ function createCaptureCrop(deps) {
11768
+ return async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
11769
+ const paddedNorm = padBbox({
11770
+ x: bbox.x / frameWidth,
11771
+ y: bbox.y / frameHeight,
11772
+ w: bbox.w / frameWidth,
11773
+ h: bbox.h / frameHeight
11774
+ }, padding);
11775
+ const nativeCrop = await deps.tryNativeCrop(frameHandle, paddedNorm, maxWidth);
11776
+ if (nativeCrop) {
11777
+ deps.bumpCropMetric(true);
11778
+ return nativeCrop;
11779
+ }
11780
+ deps.bumpCropMetric(false);
11781
+ deps.logger.debug("enrichment crop native miss — detail scheduler will re-run", { meta: { nodeId: frameHandle.nodeId } });
11782
+ return null;
11783
+ };
11784
+ }
11785
+ //#endregion
11723
11786
  //#region src/pipeline-analytics/pipeline/dropout-skip.ts
11724
11787
  function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
11725
11788
  if (!cfg.enabled) return false;
@@ -11905,6 +11968,32 @@ async function makeSquareThumb(bytes, size) {
11905
11968
  }).jpeg({ quality: THUMB_QUALITY }).toBuffer();
11906
11969
  }
11907
11970
  //#endregion
11971
+ //#region src/pipeline-analytics/event-thumbnail-resolver.ts
11972
+ /** Pick an event-OWNED media blob: the tight `crop`, else the native-res boxed
11973
+ * `fullFrameBoxed`, else any available file. Both preferred kinds are RETIRED
11974
+ * for new tracks (served only for old stored rows). Undefined when the event
11975
+ * owns no media. */
11976
+ function pickEventOwnedMedia(files) {
11977
+ return files.find((f) => f.kind === "crop") ?? files.find((f) => f.kind === "fullFrameBoxed") ?? files[0];
11978
+ }
11979
+ /** Pick a track's fallback media in the shared cadence-preference order the
11980
+ * KeyEvent path uses: best `thumbnail` → rolling `lastFrame` → `firstFrame` →
11981
+ * newest `snapshot` → any track blob. Undefined when the track owns no media. */
11982
+ function pickTrackFallbackMedia(files) {
11983
+ 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];
11984
+ }
11985
+ /**
11986
+ * Resolve the default-path media for an event id: the event's own media when it
11987
+ * has any, else the owning track's fallback media. Returns `null` when neither
11988
+ * yields a blob (caller 404s → the surface shows an icon).
11989
+ */
11990
+ async function resolveDefaultEventMedia(deps) {
11991
+ const owned = pickEventOwnedMedia(deps.eventFiles);
11992
+ if (owned) return owned;
11993
+ const trackId = await deps.getTrackIdForEvent(deps.eventId) ?? deps.eventId;
11994
+ return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
11995
+ }
11996
+ //#endregion
11908
11997
  //#region src/pipeline-analytics/index.ts
11909
11998
  /**
11910
11999
  * Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
@@ -11979,12 +12068,20 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
11979
12068
  * without it a crop-forced request degraded to the `keyFrame` FULL FRAME,
11980
12069
  * which is how 65% of the timeline tiles rendered as whole scenes. It sits
11981
12070
  * right after a real `crop`, before the full-scene `fullFrame`/`keyFrame`.
12071
+ *
12072
+ * `keyFrameSmall` (the 960px no-box downscale of the best-shot `keyFrame`)
12073
+ * joined the clean set on 2026-07-21 so a crop-forced request can degrade to it
12074
+ * for tracks whose only clean media is the small key frame; it sits last
12075
+ * (fullest-scene, but subject-centered enough to serve as a reasonable fallback
12076
+ * over 404). The retired-but-still-stored kinds (`crop`) stay listed so old rows
12077
+ * keep resolving.
11982
12078
  */
11983
12079
  var CLEAN_MEDIA_KINDS = [
11984
12080
  "crop",
11985
12081
  "thumbnail",
11986
12082
  "fullFrame",
11987
- "keyFrame"
12083
+ "keyFrame",
12084
+ "keyFrameSmall"
11988
12085
  ];
11989
12086
  /**
11990
12087
  * Pick a CLEAN (never boxed) media file for a crop-forced request: the exact
@@ -12247,6 +12344,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12247
12344
  * optional `maxWidth` caps the native crop width (used for the full-frame key
12248
12345
  * frame so a 4K native surface never floods the transport). */
12249
12346
  captureCrop = null;
12347
+ /** Full NATIVE frame by handle (downscaled worker-side to `maxWidth`), routed
12348
+ * to the owning runner, PLUS the source `tier`. Captured in the async init
12349
+ * block so `persistKeyFrames` can fetch the native full frame for the
12350
+ * `keyFrame` + `keyFrameSmall` writes in the same live-frame window AND reject
12351
+ * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
12352
+ getNativeKeyFrameRgb = null;
12250
12353
  shuttingDown = false;
12251
12354
  /** True only on the cluster's designated post-processing node. When false the
12252
12355
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
@@ -12461,6 +12564,33 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12461
12564
  timestamp: 0
12462
12565
  };
12463
12566
  };
12567
+ const fetchNativeFullFrameTiered = async (handle, maxWidth) => {
12568
+ if (!pipelineRunnerApi?.getNativeCrop) return null;
12569
+ const full = await pipelineRunnerApi.getNativeCrop.query({
12570
+ handle,
12571
+ bbox: {
12572
+ x: 0,
12573
+ y: 0,
12574
+ w: 1,
12575
+ h: 1
12576
+ },
12577
+ maxWidth
12578
+ }, nodePin(handle.nodeId));
12579
+ if (!full || full.width <= 0 || full.height <= 0) return null;
12580
+ const tier = full.tier === "ram-fullframe" ? "ram-fullframe" : "native";
12581
+ return {
12582
+ frame: {
12583
+ data: Buffer.from(full.bytes),
12584
+ width: full.width,
12585
+ height: full.height,
12586
+ format: "rgb",
12587
+ timestamp: 0
12588
+ },
12589
+ tier
12590
+ };
12591
+ };
12592
+ const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
12593
+ this.getNativeKeyFrameRgb = fetchNativeFullFrameTiered;
12464
12594
  const cropMetricLogger = logger.child("NativeCrop");
12465
12595
  let nativeHits = 0;
12466
12596
  let nativeFallbacks = 0;
@@ -12503,12 +12633,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12503
12633
  };
12504
12634
  const getNativeCropJpeg = async (frameHandle, normalizedBbox, maxWidth) => {
12505
12635
  const native = await fetchNativeCropRgb(frameHandle, normalizedBbox, maxWidth);
12506
- if (!native || !nativeSubjectCropMeetsFloor(native.width, maxWidth)) {
12507
- if (native) cropMetricLogger.debug("native subject crop below floor — local fallback", { meta: {
12508
- width: native.width,
12509
- height: native.height,
12510
- maxWidth
12511
- } });
12636
+ if (!native) {
12512
12637
  bumpCropMetric(false);
12513
12638
  return null;
12514
12639
  }
@@ -12519,28 +12644,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
12519
12644
  this.eventMediaDispatcher = new EventMediaDispatcher({
12520
12645
  getRemoteFrame,
12521
12646
  getNativeCropJpeg,
12647
+ getNativeFullFrameRgb,
12522
12648
  mediaStore: this.mediaStore,
12523
12649
  logger: logger.child("EventMediaDispatcher")
12524
12650
  });
12525
- const resolveFrameShared = createSharedFrameResolver((frameHandle) => resolveFrame(frameHandle, { getRemoteFrame }));
12526
- const captureCrop = async (frameHandle, bbox, frameWidth, frameHeight, padding, maxWidth) => {
12527
- const paddedNorm = padBbox({
12528
- x: bbox.x / frameWidth,
12529
- y: bbox.y / frameHeight,
12530
- w: bbox.w / frameWidth,
12531
- h: bbox.h / frameHeight
12532
- }, padding);
12533
- const nativeCrop = await tryNativeCrop(frameHandle, paddedNorm, maxWidth);
12534
- if (nativeCrop) {
12535
- bumpCropMetric(true);
12536
- return nativeCrop;
12537
- }
12538
- bumpCropMetric(false);
12539
- const decoded = await resolveFrameShared(frameHandle);
12540
- if (!decoded || decoded.format !== "rgb") return null;
12541
- const { crop } = await extractCrop(Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data), decoded.width, decoded.height, paddedNorm);
12542
- return crop;
12543
- };
12651
+ const captureCrop = createCaptureCrop({
12652
+ tryNativeCrop,
12653
+ bumpCropMetric,
12654
+ logger: logger.child("CaptureCrop")
12655
+ });
12544
12656
  this.captureCrop = captureCrop;
12545
12657
  this.faceRecognizer = new FaceRecognizer({
12546
12658
  identityStore: this.identityStore,
@@ -13389,7 +13501,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
13389
13501
  else plateCrops += 1;
13390
13502
  const snapshotTargets = this.buildSnapshotTargets(deviceId, result.tracked, result.timestamp, mediaSettings, result.frameWidth, result.frameHeight);
13391
13503
  const keyFrameTrackIds = selectKeyFrameTrackIds(snapshotTargets);
13392
- if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle, result.frameWidth, result.frameHeight);
13504
+ if (keyFrameTrackIds.length > 0) this.persistKeyFrames(deviceId, result.timestamp, keyFrameTrackIds, frameHandle);
13393
13505
  if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
13394
13506
  const captureCounts = {
13395
13507
  events: eventTargets.length,
@@ -14000,30 +14112,54 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14000
14112
  }));
14001
14113
  }
14002
14114
  /**
14003
- * Capture ONE native-resolution KEY FRAME per given track at this best-
14004
- * detection frame and store it (`putReplacing` → one keyFrame per track).
14115
+ * Capture the CLEAN best full frame per given track at this best-detection
14116
+ * moment and store BOTH variants (`putReplacing` → one row per track, per
14117
+ * kind): the native `keyFrame` (full frame fetched at `KEYFRAME_NATIVE_MAX_WIDTH`
14118
+ * — true native on a 4K cam) plus its 960 companion `keyFrameSmall`, encoded
14119
+ * from the SAME native surface (no second fetch). No box is drawn — these are
14120
+ * the clean hero images (`keyFrameSmall` is the web-friendly one).
14005
14121
  *
14006
- * The full frame is cropped NATIVE-FIRST via `captureCrop`: the request is the
14007
- * FULL frame (no padding) at `KEYFRAME_NATIVE_MAX_WIDTH`, which routes through
14008
- * `pipelineRunner.getNativeCrop` (the decode worker's retained native surface)
14009
- * and only falls back to the ≤640 detection frame when the native lease is
14010
- * gone. The stored key is recorded in `keyFrameKeyByTrackId` so the face /
14011
- * plate / object-embedding rows LINK the SAME native key frame (Design B).
14012
- * Issued in the live-frame window so the native lease is still held. Best-
14013
- * effort (D8) — a per-track failure is logged and never thrown.
14122
+ * NATIVE-OR-NOTHING: a native miss logs `error` and returns (no upscale, no
14123
+ * ≤640 stand-in) the next genuine new-best frame retries. The `keyFrame`
14124
+ * key is recorded in `keyFrameKeyByTrackId` so the face / plate /
14125
+ * object-embedding rows LINK the SAME native key frame (Design B). Issued in
14126
+ * the live-frame window so the native lease is still held. Best-effort (D8) —
14127
+ * a per-track failure is logged and never thrown.
14014
14128
  */
14015
- async persistKeyFrames(deviceId, timestamp, trackIds, frameHandle, frameWidth, frameHeight) {
14016
- const capture = this.captureCrop;
14129
+ async persistKeyFrames(deviceId, timestamp, trackIds, frameHandle) {
14130
+ const getNative = this.getNativeKeyFrameRgb;
14017
14131
  const mediaStore = this.mediaStore;
14018
- if (!capture || !mediaStore) return;
14019
- const req = buildKeyFrameCaptureRequest(frameWidth, frameHeight);
14132
+ if (!getNative || !mediaStore) return;
14020
14133
  const pending = trackIds.filter((id) => !this.keyFrameInFlight.has(id));
14021
14134
  if (pending.length === 0) return;
14022
14135
  for (const id of pending) this.keyFrameInFlight.add(id);
14023
14136
  await Promise.all(pending.map(async (trackId) => {
14024
14137
  try {
14025
- const keyFrame = await capture(frameHandle, req.bbox, frameWidth, frameHeight, req.padding, req.maxWidth);
14026
- if (!keyFrame) return;
14138
+ const result = await getNative(frameHandle, KEYFRAME_NATIVE_MAX_WIDTH);
14139
+ const native = result?.frame;
14140
+ if (!native || native.format !== "rgb" || native.width <= 0 || native.height <= 0) {
14141
+ this.ctx.logger.error("key-frame native miss — no upscale, will retry next best", {
14142
+ tags: { deviceId },
14143
+ meta: {
14144
+ trackId,
14145
+ shmId: frameHandle.shmId
14146
+ }
14147
+ });
14148
+ return;
14149
+ }
14150
+ const tier = result.tier;
14151
+ if (!keyFrameAcceptsTier(tier)) {
14152
+ this.ctx.logger.warn("keyFrame native miss — RAM tier rejected, will retry", {
14153
+ tags: { deviceId },
14154
+ meta: {
14155
+ trackId,
14156
+ shmId: frameHandle.shmId,
14157
+ tier
14158
+ }
14159
+ });
14160
+ return;
14161
+ }
14162
+ const { keyFrame, keyFrameSmall } = await encodeKeyFrameVariants(native);
14027
14163
  const key = await mediaStore.putReplacing({
14028
14164
  deviceId,
14029
14165
  ownerKind: "track",
@@ -14033,6 +14169,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14033
14169
  data: keyFrame
14034
14170
  });
14035
14171
  this.keyFrameKeyByTrackId.set(trackId, key);
14172
+ await mediaStore.putReplacing({
14173
+ deviceId,
14174
+ ownerKind: "track",
14175
+ ownerId: trackId,
14176
+ kind: "keyFrameSmall",
14177
+ timestamp,
14178
+ data: keyFrameSmall
14179
+ });
14036
14180
  } catch (err) {
14037
14181
  this.ctx.logger.debug("key-frame capture failed", {
14038
14182
  tags: { deviceId },
@@ -14980,11 +15124,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14980
15124
  if (materializer === null || descriptor === void 0) return;
14981
15125
  try {
14982
15126
  const cameraIds = await cache.camerasFor(data.deviceId);
15127
+ if (cameraIds.length === 0) return;
15128
+ const producingDeviceName = await this.resolveProducingDeviceName(data.deviceId);
14983
15129
  for (const cameraId of cameraIds) await materializer.materialize({
14984
15130
  cameraId,
14985
15131
  sourceDeviceId: data.deviceId,
14986
15132
  kind: descriptor.kind,
14987
- timestamp
15133
+ timestamp,
15134
+ ...producingDeviceName !== void 0 ? { producingDeviceName } : {}
14988
15135
  });
14989
15136
  } catch (err) {
14990
15137
  this.ctx.logger.warn("synthetic sensor-track materialize failed", {
@@ -14996,6 +15143,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
14996
15143
  });
14997
15144
  }
14998
15145
  }
15146
+ /**
15147
+ * Resolve the NAME of a device (the linked sensor/control that produced a
15148
+ * synthetic event) via the device-manager cap. Best-effort — returns
15149
+ * undefined on any lookup failure or an unknown device so the synthetic
15150
+ * track still materializes without a label.
15151
+ */
15152
+ async resolveProducingDeviceName(deviceId) {
15153
+ const api = this.ctx?.api;
15154
+ if (!api) return void 0;
15155
+ try {
15156
+ return (await api.deviceManager.getDevice.query({ deviceId }))?.name;
15157
+ } catch (err) {
15158
+ this.ctx?.logger?.debug("resolveProducingDeviceName failed", {
15159
+ tags: { deviceId },
15160
+ meta: { error: errMsg(err) }
15161
+ });
15162
+ return;
15163
+ }
15164
+ }
14999
15165
  async clearTracks(input) {
15000
15166
  this.trackStore?.clearDevice(input.deviceId);
15001
15167
  this.stationaryRegistry?.clearDevice(input.deviceId);
@@ -15511,17 +15677,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
15511
15677
  key: clean.key
15512
15678
  };
15513
15679
  }
15514
- const chosenEvent = eventFiles.find((f) => f.kind === "crop") ?? eventFiles.find((f) => f.kind === "fullFrameBoxed") ?? eventFiles[0];
15515
- if (chosenEvent) return {
15516
- bytes: Buffer.from(chosenEvent.base64, "base64"),
15517
- key: chosenEvent.key
15518
- };
15519
- const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
15520
- 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];
15521
- if (!chosenTrack) return null;
15680
+ const chosen = await resolveDefaultEventMedia({
15681
+ eventId: id,
15682
+ eventFiles,
15683
+ listTrackMedia: (trackId) => this.mediaStore?.listByOwner("track", trackId) ?? Promise.resolve([]),
15684
+ getTrackIdForEvent: (eventId) => this.eventStore?.getTrackIdForEvent(eventId) ?? Promise.resolve(null)
15685
+ });
15686
+ if (!chosen) return null;
15522
15687
  return {
15523
- bytes: Buffer.from(chosenTrack.base64, "base64"),
15524
- key: chosenTrack.key
15688
+ bytes: Buffer.from(chosen.base64, "base64"),
15689
+ key: chosen.key
15525
15690
  };
15526
15691
  }
15527
15692
  /**