@camstack/addon-post-analysis 1.2.45 → 1.2.47

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-QzgUDrwU.js");
5
+ const require_dist = require("../dist-B5O4mNmd.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_dist.__toESM(node_path);
@@ -5845,14 +5845,6 @@ function makeNcActionHandlers(deps) {
5845
5845
  var RebuildInputSchema = require_dist.object({
5846
5846
  /** Restrict to one camera; omit for the whole fleet. */
5847
5847
  deviceId: require_dist.number().optional(),
5848
- /**
5849
- * Fraction added on EACH side of the detection box before cropping.
5850
- * ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
5851
- * the pixel-tight crop.
5852
- */
5853
- cropMargin: require_dist.number().min(0).max(4).optional(),
5854
- /** Square the window so a tall subject is not squashed into CLIP's input. */
5855
- square: require_dist.boolean().optional(),
5856
5848
  /** Stop after this many tracks; the status reports whether more remain. */
5857
5849
  maxTracks: require_dist.number().int().positive().optional()
5858
5850
  });
@@ -5866,6 +5858,8 @@ var RebuildStatusSchema = require_dist.object({
5866
5858
  rebuilt: require_dist.number(),
5867
5859
  missingKeyFrame: require_dist.number(),
5868
5860
  missingBbox: require_dist.number(),
5861
+ /** Refused by the pipeline (camera detached / step disabled), not broken. */
5862
+ notRunnable: require_dist.number(),
5869
5863
  failed: require_dist.number(),
5870
5864
  complete: require_dist.boolean().nullable(),
5871
5865
  startedAtMs: require_dist.number().nullable(),
@@ -12024,88 +12018,6 @@ var EmbeddingDispatcher = class {
12024
12018
  }
12025
12019
  }
12026
12020
  };
12027
- //#endregion
12028
- //#region src/pipeline-analytics/pipeline/bbox-pad.ts
12029
- /**
12030
- * Pad a normalized bounding box by a fractional amount relative to its own
12031
- * dimensions. All coordinates remain in the [0, 1] unit-square.
12032
- *
12033
- * Formula (before clamping):
12034
- * newX = x - padding * w
12035
- * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
12036
- * newY = y - padding * h
12037
- * newH = h + 2 * padding * h
12038
- *
12039
- * padding === 0 is the identity operation. Pure, immutable — always returns a
12040
- * new object.
12041
- *
12042
- * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
12043
- * bound slides inward and keeps the extent that was asked for, on either side.
12044
- * It shrinks only when the padded window is larger than the frame itself
12045
- * would (e.g. a left-hugging box gains a bit more right-side context). This is
12046
- * acceptable for detection crops (more context, never out of [0,1]); the
12047
- * downstream `extractCrop` clamps to pixel bounds again regardless.
12048
- */
12049
- function padBbox(bbox, padding) {
12050
- const rawX = bbox.x - padding * bbox.w;
12051
- const rawY = bbox.y - padding * bbox.h;
12052
- const rawW = bbox.w * (1 + 2 * padding);
12053
- const rawH = bbox.h * (1 + 2 * padding);
12054
- const w = Math.min(rawW, 1);
12055
- const h = Math.min(rawH, 1);
12056
- return {
12057
- x: Math.min(Math.max(0, rawX), 1 - w),
12058
- y: Math.min(Math.max(0, rawY), 1 - h),
12059
- w,
12060
- h
12061
- };
12062
- }
12063
- /**
12064
- * Widen a bbox and optionally square it, staying inside the unit frame.
12065
- *
12066
- * Squaring uses the frame's aspect so the window is square in PIXELS, not in
12067
- * normalised units — those are only the same on a square frame, and no camera
12068
- * produces one.
12069
- */
12070
- function cropWindow(bbox, frameWidth, frameHeight, options) {
12071
- const padded = padBbox(bbox, options.margin);
12072
- if (!options.square) return padded;
12073
- const pxW = padded.w * frameWidth;
12074
- const pxH = padded.h * frameHeight;
12075
- const side = Math.max(pxW, pxH);
12076
- const w = Math.min(side / frameWidth, 1);
12077
- const h = Math.min(side / frameHeight, 1);
12078
- const cx = padded.x + padded.w / 2;
12079
- const cy = padded.y + padded.h / 2;
12080
- return {
12081
- w,
12082
- h,
12083
- x: Math.min(Math.max(0, cx - w / 2), 1 - w),
12084
- y: Math.min(Math.max(0, cy - h / 2), 1 - h)
12085
- };
12086
- }
12087
- /** Cut a JPEG crop out of a JPEG frame using a normalised window. */
12088
- async function cropFromJpeg(jpeg, window) {
12089
- const image = (0, sharp.default)(jpeg);
12090
- const meta = await image.metadata();
12091
- const frameWidth = meta.width;
12092
- const frameHeight = meta.height;
12093
- if (frameWidth === void 0 || frameHeight === void 0) return null;
12094
- const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
12095
- const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
12096
- const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
12097
- const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
12098
- return {
12099
- crop: await image.extract({
12100
- left,
12101
- top,
12102
- width,
12103
- height
12104
- }).jpeg({ quality: 90 }).toBuffer(),
12105
- width,
12106
- height
12107
- };
12108
- }
12109
12021
  /**
12110
12022
  * Re-embed every listed track from its key frame.
12111
12023
  *
@@ -12114,12 +12026,11 @@ async function cropFromJpeg(jpeg, window) {
12114
12026
  * knowing how much was not rebuilt, not just how much was.
12115
12027
  */
12116
12028
  async function rebuildObjectEmbeddings(deps, options = {}) {
12117
- const margin = options.cropMargin ?? .2;
12118
- const square = options.square ?? true;
12119
12029
  const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
12120
12030
  let rebuilt = 0;
12121
12031
  let missingKeyFrame = 0;
12122
12032
  let missingBbox = 0;
12033
+ let notRunnable = 0;
12123
12034
  let failed = 0;
12124
12035
  let scanned = 0;
12125
12036
  let cursor = null;
@@ -12150,27 +12061,41 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12150
12061
  missingBbox += 1;
12151
12062
  continue;
12152
12063
  }
12153
- const meta = await (0, sharp.default)(source.keyFrame).metadata();
12154
- if (meta.width === void 0 || meta.height === void 0) {
12155
- failed += 1;
12064
+ const outcome = await deps.embed(source);
12065
+ if (outcome.kind === "not-runnable") {
12066
+ notRunnable += 1;
12067
+ deps.logger.warn("embedding rebuild: track could not be re-embedded by the pipeline", {
12068
+ tags: { deviceId: source.deviceId },
12069
+ meta: {
12070
+ trackId,
12071
+ reason: outcome.reason
12072
+ }
12073
+ });
12156
12074
  continue;
12157
12075
  }
12158
- const window = cropWindow(source.bbox, meta.width, meta.height, {
12159
- margin,
12160
- square
12161
- });
12162
- const cut = await cropFromJpeg(source.keyFrame, window);
12163
- if (cut === null) {
12076
+ if (outcome.kind === "failed") {
12164
12077
  failed += 1;
12078
+ deps.logger.warn("embedding rebuild: the pipeline returned no vector for a track", {
12079
+ tags: { deviceId: source.deviceId },
12080
+ meta: {
12081
+ trackId,
12082
+ reason: outcome.reason
12083
+ }
12084
+ });
12165
12085
  continue;
12166
12086
  }
12167
- const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12168
- if (embedding.length === 0) {
12087
+ const written = await deps.store(source, outcome.embeddingBase64);
12088
+ if (written === "persisted") rebuilt += 1;
12089
+ else {
12169
12090
  failed += 1;
12170
- continue;
12091
+ deps.logger.warn("embedding rebuild wrote nothing for a track", {
12092
+ tags: { deviceId: source.deviceId },
12093
+ meta: {
12094
+ trackId,
12095
+ outcome: written
12096
+ }
12097
+ });
12171
12098
  }
12172
- await deps.store(source, embedding);
12173
- rebuilt += 1;
12174
12099
  } catch (err) {
12175
12100
  failed += 1;
12176
12101
  deps.logger.warn("embedding rebuild failed for a track", { meta: {
@@ -12183,6 +12108,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12183
12108
  rebuilt,
12184
12109
  missingKeyFrame,
12185
12110
  missingBbox,
12111
+ notRunnable,
12186
12112
  failed
12187
12113
  });
12188
12114
  }
@@ -12198,16 +12124,16 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12198
12124
  rebuilt,
12199
12125
  missingKeyFrame,
12200
12126
  missingBbox,
12127
+ notRunnable,
12201
12128
  failed,
12202
- complete,
12203
- cropMargin: margin,
12204
- square
12129
+ complete
12205
12130
  } });
12206
12131
  return {
12207
12132
  scanned,
12208
12133
  rebuilt,
12209
12134
  missingKeyFrame,
12210
12135
  missingBbox,
12136
+ notRunnable,
12211
12137
  failed,
12212
12138
  complete
12213
12139
  };
@@ -12375,6 +12301,69 @@ async function resolveDefaultEventMedia(deps) {
12375
12301
  return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
12376
12302
  }
12377
12303
  //#endregion
12304
+ //#region src/pipeline-analytics/rebuild-detail-request.ts
12305
+ /** The one step a rebuild runs. */
12306
+ var REBUILD_DETAIL_STEP_ID = "clip-embedding";
12307
+ /**
12308
+ * Build the `runDetailSubtree` input for one stored track.
12309
+ *
12310
+ * The box is stored NORMALISED because the key frame is a native-resolution
12311
+ * capture while the detection ran on a downscaled analysis frame; the two have
12312
+ * different pixel dimensions. `parent.bbox` is frame pixels (the live path's
12313
+ * convention), so it is denormalised here against the KEY FRAME's own
12314
+ * dimensions — the image the runner is about to cut. This is coordinate
12315
+ * mapping, not crop derivation: no padding, no squaring, no rectangle choice.
12316
+ */
12317
+ function buildRebuildDetailInput(source, keyFrame) {
12318
+ return {
12319
+ deviceId: source.deviceId,
12320
+ frameJpeg: source.keyFrame.toString("base64"),
12321
+ parent: {
12322
+ bbox: {
12323
+ x: source.bbox.x * keyFrame.width,
12324
+ y: source.bbox.y * keyFrame.height,
12325
+ w: source.bbox.w * keyFrame.width,
12326
+ h: source.bbox.h * keyFrame.height
12327
+ },
12328
+ className: source.className
12329
+ },
12330
+ steps: [REBUILD_DETAIL_STEP_ID]
12331
+ };
12332
+ }
12333
+ /**
12334
+ * Turn one `runDetailSubtree` answer into a rebuild outcome.
12335
+ *
12336
+ * The three cases lead to three different counters:
12337
+ *
12338
+ * - `null` — the runner DECLINED: the camera is not attached, `clip-embedding`
12339
+ * is not enabled in its tree, or the frame was unreadable. It logs which,
12340
+ * tagged with the deviceId. That is `not-runnable`, not a failure: the remedy
12341
+ * is a configuration change, and folding it into `failed` would point the
12342
+ * operator at the inference engine instead.
12343
+ * - details with no `clip-embedding` embedding — the step RAN and produced
12344
+ * nothing usable. That is a failure.
12345
+ * - an embedding — passed through as base64, the form the vector store stores.
12346
+ */
12347
+ function readRebuildEmbedding(result) {
12348
+ if (result === null) return {
12349
+ kind: "not-runnable",
12350
+ reason: "the runner returned no result — camera not attached, clip-embedding not enabled for it, or the key frame was unreadable (the runner logged which, tagged with the deviceId)"
12351
+ };
12352
+ const embedding = result.details.find((d) => d.stepId === "clip-embedding" && d.embedding !== void 0)?.embedding;
12353
+ if (embedding === void 0) return {
12354
+ kind: "failed",
12355
+ reason: `the detail subtree ran but returned no ${REBUILD_DETAIL_STEP_ID} embedding (${result.details.length} detail(s))`
12356
+ };
12357
+ return {
12358
+ kind: "embedded",
12359
+ embeddingBase64: embedding
12360
+ };
12361
+ }
12362
+ /** Narrowing helper — a source without a key frame can never build a request. */
12363
+ function hasKeyFrame(source) {
12364
+ return source.keyFrame !== null && source.bbox !== null;
12365
+ }
12366
+ //#endregion
12378
12367
  //#region src/pipeline-analytics/pipeline/face-matcher.ts
12379
12368
  /**
12380
12369
  * Count enrolled samples per identity for probes of a matching model+dimension.
@@ -14566,6 +14555,42 @@ async function encodeKeyFrameVariants(native) {
14566
14555
  };
14567
14556
  }
14568
14557
  //#endregion
14558
+ //#region src/pipeline-analytics/pipeline/bbox-pad.ts
14559
+ /**
14560
+ * Pad a normalized bounding box by a fractional amount relative to its own
14561
+ * dimensions. All coordinates remain in the [0, 1] unit-square.
14562
+ *
14563
+ * Formula (before clamping):
14564
+ * newX = x - padding * w
14565
+ * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
14566
+ * newY = y - padding * h
14567
+ * newH = h + 2 * padding * h
14568
+ *
14569
+ * padding === 0 is the identity operation. Pure, immutable — always returns a
14570
+ * new object.
14571
+ *
14572
+ * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
14573
+ * bound slides inward and keeps the extent that was asked for, on either side.
14574
+ * It shrinks only when the padded window is larger than the frame itself
14575
+ * would (e.g. a left-hugging box gains a bit more right-side context). This is
14576
+ * acceptable for detection crops (more context, never out of [0,1]); the
14577
+ * downstream `extractCrop` clamps to pixel bounds again regardless.
14578
+ */
14579
+ function padBbox(bbox, padding) {
14580
+ const rawX = bbox.x - padding * bbox.w;
14581
+ const rawY = bbox.y - padding * bbox.h;
14582
+ const rawW = bbox.w * (1 + 2 * padding);
14583
+ const rawH = bbox.h * (1 + 2 * padding);
14584
+ const w = Math.min(rawW, 1);
14585
+ const h = Math.min(rawH, 1);
14586
+ return {
14587
+ x: Math.min(Math.max(0, rawX), 1 - w),
14588
+ y: Math.min(Math.max(0, rawY), 1 - h),
14589
+ w,
14590
+ h
14591
+ };
14592
+ }
14593
+ //#endregion
14569
14594
  //#region src/pipeline-analytics/pipeline/keyframe-crop.ts
14570
14595
  function createKeyFrameCrop(deps) {
14571
14596
  return async (input) => {
@@ -18555,30 +18580,18 @@ function buildGlobalSettingsSchema() {
18555
18580
  {
18556
18581
  id: "embedding-index",
18557
18582
  title: "Semantic search index",
18558
- description: "The CLIP vector index behind text search. Rebuild re-embeds stored tracks from their native key frames — the only way to change what a vector means, since a new crop margin or encoder model leaves two incomparable feature spaces in one index. Vectors live in the vector-store capability, alongside the events they are shown with.",
18583
+ description: "The CLIP vector index behind text search. Rebuild re-embeds stored tracks from their native key frames — the only way to change what a vector means, since a new crop convention or encoder model leaves two incomparable feature spaces in one index. Vectors live in the vector-store capability, alongside the events they are shown with.",
18559
18584
  columns: 1,
18560
18585
  fields: [
18561
- {
18562
- type: "number",
18563
- key: "rebuildCropMargin",
18564
- label: "Rebuild crop margin",
18565
- description: "Fraction added on EACH side of the detection box when re-embedding. 0.2 gives a 1.4x window: CLIP is trained on natural images WITH surroundings, and a pixel-tight crop removes what it is strongest on. 0 reproduces the tight crop. Only affects Rebuild — it does not change live capture, and the thumbnail is never re-cropped.",
18566
- default: .2,
18567
- min: 0,
18568
- max: 2,
18569
- step: .1
18570
- },
18571
18586
  {
18572
18587
  type: "addon-action-button",
18573
18588
  key: "rebuildEmbeddingsButton",
18574
18589
  label: "Rebuild index",
18575
- description: "Re-embed every stored track from its key frame, using the margin above. Runs in the background — press Progress to watch it. Tracks whose key frame has aged out cannot be rebuilt and are reported as skipped.",
18590
+ description: "Re-embed every stored track from its key frame, through the detection pipeline, using the cluster Enrichment crop setting — so a rebuilt vector is comparable with a live one. Runs in the background; press Progress to watch it. Tracks whose key frame has aged out, or whose camera is detached, are reported as skipped rather than dropped.",
18576
18591
  addonId: "pipeline-analytics",
18577
18592
  action: "embedding.rebuild",
18578
18593
  buttonLabel: "Rebuild",
18579
18594
  variant: "primary",
18580
- paramsFromForm: { cropMargin: "rebuildCropMargin" },
18581
- paramsStatic: { square: true },
18582
18595
  successMessage: "Rebuild started — press Progress to follow it."
18583
18596
  },
18584
18597
  {
@@ -18589,7 +18602,7 @@ function buildGlobalSettingsSchema() {
18589
18602
  addonId: "pipeline-analytics",
18590
18603
  action: "embedding.rebuildStatus",
18591
18604
  buttonLabel: "Check progress",
18592
- successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
18605
+ successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · not runnable {notRunnable} · failed {failed}"
18593
18606
  },
18594
18607
  {
18595
18608
  type: "addon-action-button",
@@ -20581,6 +20594,29 @@ var IdentityStore = class {
20581
20594
  */
20582
20595
  /** The vector-store index this store owns. */
20583
20596
  var OBJECT_CLIP_INDEX = "object-clip";
20597
+ /**
20598
+ * Reduce either input form to the stored form.
20599
+ *
20600
+ * A base64 payload passes through untouched; its dimension is read from the
20601
+ * byte length. A length that is not a whole number of float32s cannot be a
20602
+ * vector, and storing it would produce a row whose dimension silently
20603
+ * disagrees with the declared index.
20604
+ */
20605
+ function resolveVectorPayload(input) {
20606
+ if (input.embeddingBase64 !== void 0) {
20607
+ const byteLength = Buffer.from(input.embeddingBase64, "base64").byteLength;
20608
+ if (byteLength === 0 || byteLength % 4 !== 0) return null;
20609
+ return {
20610
+ base64: input.embeddingBase64,
20611
+ dim: require_dist.vectorDimFromBase64(input.embeddingBase64)
20612
+ };
20613
+ }
20614
+ if (input.embedding.length === 0) return null;
20615
+ return {
20616
+ base64: require_dist.encodeVectorBase64(input.embedding),
20617
+ dim: input.embedding.length
20618
+ };
20619
+ }
20584
20620
  var ObjectEmbeddingStore = class {
20585
20621
  vectorStore;
20586
20622
  logger;
@@ -20597,6 +20633,14 @@ var ObjectEmbeddingStore = class {
20597
20633
  * write: the provider rejects an upsert into an index it has never seen. A
20598
20634
  * dim change against a populated index is REFUSED rather than wiping it, so
20599
20635
  * this failing is the signal that the encoder model changed.
20636
+ *
20637
+ * `declaredDim` caches the declaration, and that cache is per-PROCESS while
20638
+ * the index lives in whichever addon currently provides `vector-store`. When
20639
+ * the active provider changed under a running hub the cache stayed warm
20640
+ * against an index that no longer existed, and every write from then on threw
20641
+ * `index "object-clip" was never declared` — for hours, until the addon was
20642
+ * restarted by hand. So a failed write clears the cache (see
20643
+ * {@link upsertIfBetter}) and the next one re-declares.
20600
20644
  */
20601
20645
  async ensureIndex(dim) {
20602
20646
  if (this.declaredDim === dim) return true;
@@ -20628,33 +20672,50 @@ var ObjectEmbeddingStore = class {
20628
20672
  * short-circuited, and every later pass silently wrote nothing while
20629
20673
  * reporting hundreds rebuilt. Observed live — 214 "rebuilt" moved the index
20630
20674
  * by 4 rows.
20675
+ *
20676
+ * ## Why this returns an outcome instead of `void`
20677
+ *
20678
+ * It used to return `void` and turn every failure into a warn. The rebuild
20679
+ * therefore counted a write the instant the call returned, and the second time
20680
+ * this class of bug appeared it produced `rebuilt: 704` against `indexed: 0` —
20681
+ * a progress number that was pure fiction while the real cause sat in a warn
20682
+ * nobody was reading. The caller now has to distinguish "written" from "logged
20683
+ * and dropped", because a counter that cannot reads as verification.
20631
20684
  */
20632
20685
  async upsertIfBetter(input) {
20633
- if (input.embedding.length === 0) return;
20634
- if (!await this.ensureIndex(input.embedding.length)) return;
20686
+ const vector = resolveVectorPayload(input);
20687
+ if (vector === null) return "failed";
20688
+ if (!await this.ensureIndex(vector.dim)) return "failed";
20635
20689
  try {
20636
20690
  if (input.force !== true) {
20637
20691
  const stored = (await this.vectorStore.getByIds.query({
20638
20692
  index: OBJECT_CLIP_INDEX,
20639
20693
  ids: [input.trackId]
20640
20694
  })).items[0]?.metadata["confidence"];
20641
- if (typeof stored === "number" && input.confidence <= stored) return;
20695
+ if (typeof stored === "number" && input.confidence <= stored) return "skipped";
20642
20696
  }
20643
- if ((await this.vectorStore.upsert.mutate({
20644
- index: "object-clip",
20697
+ const result = await this.vectorStore.upsert.mutate({
20698
+ index: OBJECT_CLIP_INDEX,
20645
20699
  items: [{
20646
20700
  id: input.trackId,
20647
- vector: require_dist.encodeVectorBase64(input.embedding),
20701
+ vector: vector.base64,
20648
20702
  metadata: metadataFor(input)
20649
20703
  }]
20650
- })).rejected > 0) this.logger.warn("object-embedding upsert rejected", {
20651
- tags: { deviceId: input.deviceId },
20652
- meta: {
20653
- trackId: input.trackId,
20654
- dim: input.embedding.length
20655
- }
20656
20704
  });
20705
+ if (result.upserted === 0) {
20706
+ this.logger.warn("object-embedding upsert rejected", {
20707
+ tags: { deviceId: input.deviceId },
20708
+ meta: {
20709
+ trackId: input.trackId,
20710
+ dim: vector.dim,
20711
+ rejected: result.rejected
20712
+ }
20713
+ });
20714
+ return "failed";
20715
+ }
20716
+ return "persisted";
20657
20717
  } catch (err) {
20718
+ this.declaredDim = null;
20658
20719
  this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", {
20659
20720
  tags: { deviceId: input.deviceId },
20660
20721
  meta: {
@@ -20662,6 +20723,7 @@ var ObjectEmbeddingStore = class {
20662
20723
  error: String(err)
20663
20724
  }
20664
20725
  });
20726
+ return "failed";
20665
20727
  }
20666
20728
  }
20667
20729
  /**
@@ -23947,7 +24009,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23947
24009
  let storage = this.ctx.kernel.storage;
23948
24010
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
23949
24011
  if (mediaRoot) {
23950
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-HrEN5lB7.js"));
24012
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-m6HS7TOO.js"));
23951
24013
  storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
23952
24014
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
23953
24015
  }
@@ -27011,6 +27073,43 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27011
27073
  return info.modelId;
27012
27074
  }
27013
27075
  /**
27076
+ * Re-embed ONE stored track by running its `clip-embedding` step through the
27077
+ * camera's runner.
27078
+ *
27079
+ * The whole point: the runner cuts. This method reads the key frame's pixel
27080
+ * dimensions (metadata only — no pixel work, no rectangle chosen here) so the
27081
+ * stored normalised box can be expressed in the key frame's own pixels, ships
27082
+ * the full frame, and reads the vector back. Where the crop boundary falls is
27083
+ * decided inside `runDetailSubtree` by the one cluster-wide convention, the
27084
+ * same call the live detail plane makes.
27085
+ *
27086
+ * NOT node-pinned: `runDetailSubtree` is device-bound, so the cap router
27087
+ * sends it to the runner that owns this camera. A pin would be a guess about
27088
+ * placement that the binding already knows.
27089
+ */
27090
+ async embedRebuildSourceViaPipeline(source) {
27091
+ if (!hasKeyFrame(source)) return {
27092
+ kind: "failed",
27093
+ reason: "no key frame or no bbox"
27094
+ };
27095
+ const runner = this.ctx.api?.pipelineRunner;
27096
+ if (!runner?.runDetailSubtree) return {
27097
+ kind: "not-runnable",
27098
+ reason: "pipelineRunner.runDetailSubtree is unavailable on this cluster"
27099
+ };
27100
+ const sharp$7 = (await import("sharp")).default;
27101
+ const meta = await sharp$7(source.keyFrame).metadata();
27102
+ if (meta.width === void 0 || meta.height === void 0) return {
27103
+ kind: "failed",
27104
+ reason: "key frame has no readable dimensions"
27105
+ };
27106
+ const input = buildRebuildDetailInput(source, {
27107
+ width: meta.width,
27108
+ height: meta.height
27109
+ });
27110
+ return readRebuildEmbedding(await runner.runDetailSubtree.mutate(input));
27111
+ }
27112
+ /**
27014
27113
  * Every track id the track store still holds, across the given devices.
27015
27114
  *
27016
27115
  * Returned as a Set for the orphan sweep, which REFUSES an empty one — a
@@ -27049,6 +27148,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27049
27148
  * resolution, captured at the best detection) plus that detection's bbox, so
27050
27149
  * the window can be WIDENED. The tight crop originally embedded cannot be
27051
27150
  * widened after the fact.
27151
+ *
27152
+ * The frame and the box are shipped to the camera's RUNNER, which cuts and
27153
+ * encodes them through `runDetailSubtree` — the same code path, the same
27154
+ * crop convention and the same step catalog the live detail plane uses. This
27155
+ * side no longer chooses a rectangle, and there is no `cropMargin` argument
27156
+ * to choose one with: see `embedding-rebuild.ts` for what that cost.
27052
27157
  */
27053
27158
  async rebuildObjectEmbeddings(input) {
27054
27159
  if (this.rebuildState.running) {
@@ -27106,27 +27211,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27106
27211
  ...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
27107
27212
  };
27108
27213
  },
27109
- encode: async (crop, width, height) => {
27110
- return (await api.embeddingEncoder.encode.query({
27111
- crop: new Uint8Array(crop),
27112
- width,
27113
- height
27114
- })).embedding;
27115
- },
27116
- store: async (source, embedding) => {
27117
- await store.upsertIfBetter({
27118
- trackId: source.trackId,
27119
- deviceId: source.deviceId,
27120
- timestamp: source.timestamp,
27121
- className: source.className,
27122
- embedding,
27123
- modelId,
27124
- confidence: source.confidence,
27125
- force: true,
27126
- ...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
27127
- ...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
27128
- });
27129
- },
27214
+ embed: (source) => this.embedRebuildSourceViaPipeline(source),
27215
+ store: async (source, embeddingBase64) => store.upsertIfBetter({
27216
+ trackId: source.trackId,
27217
+ deviceId: source.deviceId,
27218
+ timestamp: source.timestamp,
27219
+ className: source.className,
27220
+ embeddingBase64,
27221
+ modelId,
27222
+ confidence: source.confidence,
27223
+ force: true,
27224
+ ...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
27225
+ ...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
27226
+ }),
27130
27227
  logger: {
27131
27228
  info: (message, extras) => this.ctx.logger.info(message, extras),
27132
27229
  warn: (message, extras) => this.ctx.logger.warn(message, extras)
@@ -27140,12 +27237,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27140
27237
  rebuilt: 0,
27141
27238
  missingKeyFrame: 0,
27142
27239
  missingBbox: 0,
27240
+ notRunnable: 0,
27143
27241
  failed: 0
27144
27242
  }
27145
27243
  };
27146
27244
  rebuildObjectEmbeddings(deps, {
27147
- ...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
27148
- ...input.square !== void 0 ? { square: input.square } : {},
27149
27245
  ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
27150
27246
  onProgress: (progress) => {
27151
27247
  if (this.rebuildState.running) this.rebuildState.progress = progress;
@@ -27170,7 +27266,6 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27170
27266
  });
27171
27267
  this.ctx.logger.info("object embedding rebuild started", { meta: {
27172
27268
  deviceId: input.deviceId ?? "all",
27173
- cropMargin: input.cropMargin ?? "default",
27174
27269
  maxTracks: input.maxTracks ?? "unbounded"
27175
27270
  } });
27176
27271
  return {
@@ -27212,6 +27307,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27212
27307
  rebuilt: p?.rebuilt ?? 0,
27213
27308
  missingKeyFrame: p?.missingKeyFrame ?? 0,
27214
27309
  missingBbox: p?.missingBbox ?? 0,
27310
+ notRunnable: p?.notRunnable ?? 0,
27215
27311
  failed: p?.failed ?? 0,
27216
27312
  complete: state.complete ?? null,
27217
27313
  startedAtMs: state.startedAtMs ?? null,