@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.
@@ -1,4 +1,4 @@
1
- import { $ as unknown, A as kebabToCamel, B as DeviceType, C as cosineSimilarity, D as encodeVectorBase64, F as subKindsOf, G as _enum, H as hydrateSchema, I as videoclipsCapability, J as literal, K as array, L as zoneAnalyticsCapability, M as pipelineAnalyticsCapability, N as plateGalleryCapability, O as faceGalleryCapability, P as readDeviceStateFrom, Q as string, R as errMsg, S as buildEventKindDescriptor, T as defineCustomActions, U as isDeviceScopedCap, V as createEvent, W as nodePin, X as object, Y as number, Z as record, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as EventCategory, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as notificationRulesCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as boolean, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as BaseAddon } from "../dist-BoV5RsRC.mjs";
1
+ import { $ as string, A as kebabToCamel, B as BaseAddon, C as cosineSimilarity, D as encodeVectorBase64, F as subKindsOf, G as nodePin, H as createEvent, I as vectorDimFromBase64, J as boolean, K as _enum, L as videoclipsCapability, M as pipelineAnalyticsCapability, N as plateGalleryCapability, O as faceGalleryCapability, P as readDeviceStateFrom, Q as record, R as zoneAnalyticsCapability, S as buildEventKindDescriptor, T as defineCustomActions, U as hydrateSchema, V as DeviceType, W as isDeviceScopedCap, X as number, Y as literal, Z as object, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as unknown, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as notificationRulesCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as array, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, tt as EventCategory, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as errMsg } from "../dist-BHtH4Obt.mjs";
2
2
  import { promises } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
@@ -5839,14 +5839,6 @@ function makeNcActionHandlers(deps) {
5839
5839
  var RebuildInputSchema = object({
5840
5840
  /** Restrict to one camera; omit for the whole fleet. */
5841
5841
  deviceId: number().optional(),
5842
- /**
5843
- * Fraction added on EACH side of the detection box before cropping.
5844
- * ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
5845
- * the pixel-tight crop.
5846
- */
5847
- cropMargin: number().min(0).max(4).optional(),
5848
- /** Square the window so a tall subject is not squashed into CLIP's input. */
5849
- square: boolean().optional(),
5850
5842
  /** Stop after this many tracks; the status reports whether more remain. */
5851
5843
  maxTracks: number().int().positive().optional()
5852
5844
  });
@@ -5860,6 +5852,8 @@ var RebuildStatusSchema = object({
5860
5852
  rebuilt: number(),
5861
5853
  missingKeyFrame: number(),
5862
5854
  missingBbox: number(),
5855
+ /** Refused by the pipeline (camera detached / step disabled), not broken. */
5856
+ notRunnable: number(),
5863
5857
  failed: number(),
5864
5858
  complete: boolean().nullable(),
5865
5859
  startedAtMs: number().nullable(),
@@ -12018,88 +12012,6 @@ var EmbeddingDispatcher = class {
12018
12012
  }
12019
12013
  }
12020
12014
  };
12021
- //#endregion
12022
- //#region src/pipeline-analytics/pipeline/bbox-pad.ts
12023
- /**
12024
- * Pad a normalized bounding box by a fractional amount relative to its own
12025
- * dimensions. All coordinates remain in the [0, 1] unit-square.
12026
- *
12027
- * Formula (before clamping):
12028
- * newX = x - padding * w
12029
- * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
12030
- * newY = y - padding * h
12031
- * newH = h + 2 * padding * h
12032
- *
12033
- * padding === 0 is the identity operation. Pure, immutable — always returns a
12034
- * new object.
12035
- *
12036
- * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
12037
- * bound slides inward and keeps the extent that was asked for, on either side.
12038
- * It shrinks only when the padded window is larger than the frame itself
12039
- * would (e.g. a left-hugging box gains a bit more right-side context). This is
12040
- * acceptable for detection crops (more context, never out of [0,1]); the
12041
- * downstream `extractCrop` clamps to pixel bounds again regardless.
12042
- */
12043
- function padBbox(bbox, padding) {
12044
- const rawX = bbox.x - padding * bbox.w;
12045
- const rawY = bbox.y - padding * bbox.h;
12046
- const rawW = bbox.w * (1 + 2 * padding);
12047
- const rawH = bbox.h * (1 + 2 * padding);
12048
- const w = Math.min(rawW, 1);
12049
- const h = Math.min(rawH, 1);
12050
- return {
12051
- x: Math.min(Math.max(0, rawX), 1 - w),
12052
- y: Math.min(Math.max(0, rawY), 1 - h),
12053
- w,
12054
- h
12055
- };
12056
- }
12057
- /**
12058
- * Widen a bbox and optionally square it, staying inside the unit frame.
12059
- *
12060
- * Squaring uses the frame's aspect so the window is square in PIXELS, not in
12061
- * normalised units — those are only the same on a square frame, and no camera
12062
- * produces one.
12063
- */
12064
- function cropWindow(bbox, frameWidth, frameHeight, options) {
12065
- const padded = padBbox(bbox, options.margin);
12066
- if (!options.square) return padded;
12067
- const pxW = padded.w * frameWidth;
12068
- const pxH = padded.h * frameHeight;
12069
- const side = Math.max(pxW, pxH);
12070
- const w = Math.min(side / frameWidth, 1);
12071
- const h = Math.min(side / frameHeight, 1);
12072
- const cx = padded.x + padded.w / 2;
12073
- const cy = padded.y + padded.h / 2;
12074
- return {
12075
- w,
12076
- h,
12077
- x: Math.min(Math.max(0, cx - w / 2), 1 - w),
12078
- y: Math.min(Math.max(0, cy - h / 2), 1 - h)
12079
- };
12080
- }
12081
- /** Cut a JPEG crop out of a JPEG frame using a normalised window. */
12082
- async function cropFromJpeg(jpeg, window) {
12083
- const image = sharp(jpeg);
12084
- const meta = await image.metadata();
12085
- const frameWidth = meta.width;
12086
- const frameHeight = meta.height;
12087
- if (frameWidth === void 0 || frameHeight === void 0) return null;
12088
- const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
12089
- const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
12090
- const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
12091
- const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
12092
- return {
12093
- crop: await image.extract({
12094
- left,
12095
- top,
12096
- width,
12097
- height
12098
- }).jpeg({ quality: 90 }).toBuffer(),
12099
- width,
12100
- height
12101
- };
12102
- }
12103
12015
  /**
12104
12016
  * Re-embed every listed track from its key frame.
12105
12017
  *
@@ -12108,12 +12020,11 @@ async function cropFromJpeg(jpeg, window) {
12108
12020
  * knowing how much was not rebuilt, not just how much was.
12109
12021
  */
12110
12022
  async function rebuildObjectEmbeddings(deps, options = {}) {
12111
- const margin = options.cropMargin ?? .2;
12112
- const square = options.square ?? true;
12113
12023
  const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
12114
12024
  let rebuilt = 0;
12115
12025
  let missingKeyFrame = 0;
12116
12026
  let missingBbox = 0;
12027
+ let notRunnable = 0;
12117
12028
  let failed = 0;
12118
12029
  let scanned = 0;
12119
12030
  let cursor = null;
@@ -12144,27 +12055,41 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12144
12055
  missingBbox += 1;
12145
12056
  continue;
12146
12057
  }
12147
- const meta = await sharp(source.keyFrame).metadata();
12148
- if (meta.width === void 0 || meta.height === void 0) {
12149
- failed += 1;
12058
+ const outcome = await deps.embed(source);
12059
+ if (outcome.kind === "not-runnable") {
12060
+ notRunnable += 1;
12061
+ deps.logger.warn("embedding rebuild: track could not be re-embedded by the pipeline", {
12062
+ tags: { deviceId: source.deviceId },
12063
+ meta: {
12064
+ trackId,
12065
+ reason: outcome.reason
12066
+ }
12067
+ });
12150
12068
  continue;
12151
12069
  }
12152
- const window = cropWindow(source.bbox, meta.width, meta.height, {
12153
- margin,
12154
- square
12155
- });
12156
- const cut = await cropFromJpeg(source.keyFrame, window);
12157
- if (cut === null) {
12070
+ if (outcome.kind === "failed") {
12158
12071
  failed += 1;
12072
+ deps.logger.warn("embedding rebuild: the pipeline returned no vector for a track", {
12073
+ tags: { deviceId: source.deviceId },
12074
+ meta: {
12075
+ trackId,
12076
+ reason: outcome.reason
12077
+ }
12078
+ });
12159
12079
  continue;
12160
12080
  }
12161
- const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12162
- if (embedding.length === 0) {
12081
+ const written = await deps.store(source, outcome.embeddingBase64);
12082
+ if (written === "persisted") rebuilt += 1;
12083
+ else {
12163
12084
  failed += 1;
12164
- continue;
12085
+ deps.logger.warn("embedding rebuild wrote nothing for a track", {
12086
+ tags: { deviceId: source.deviceId },
12087
+ meta: {
12088
+ trackId,
12089
+ outcome: written
12090
+ }
12091
+ });
12165
12092
  }
12166
- await deps.store(source, embedding);
12167
- rebuilt += 1;
12168
12093
  } catch (err) {
12169
12094
  failed += 1;
12170
12095
  deps.logger.warn("embedding rebuild failed for a track", { meta: {
@@ -12177,6 +12102,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12177
12102
  rebuilt,
12178
12103
  missingKeyFrame,
12179
12104
  missingBbox,
12105
+ notRunnable,
12180
12106
  failed
12181
12107
  });
12182
12108
  }
@@ -12192,16 +12118,16 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12192
12118
  rebuilt,
12193
12119
  missingKeyFrame,
12194
12120
  missingBbox,
12121
+ notRunnable,
12195
12122
  failed,
12196
- complete,
12197
- cropMargin: margin,
12198
- square
12123
+ complete
12199
12124
  } });
12200
12125
  return {
12201
12126
  scanned,
12202
12127
  rebuilt,
12203
12128
  missingKeyFrame,
12204
12129
  missingBbox,
12130
+ notRunnable,
12205
12131
  failed,
12206
12132
  complete
12207
12133
  };
@@ -12369,6 +12295,69 @@ async function resolveDefaultEventMedia(deps) {
12369
12295
  return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
12370
12296
  }
12371
12297
  //#endregion
12298
+ //#region src/pipeline-analytics/rebuild-detail-request.ts
12299
+ /** The one step a rebuild runs. */
12300
+ var REBUILD_DETAIL_STEP_ID = "clip-embedding";
12301
+ /**
12302
+ * Build the `runDetailSubtree` input for one stored track.
12303
+ *
12304
+ * The box is stored NORMALISED because the key frame is a native-resolution
12305
+ * capture while the detection ran on a downscaled analysis frame; the two have
12306
+ * different pixel dimensions. `parent.bbox` is frame pixels (the live path's
12307
+ * convention), so it is denormalised here against the KEY FRAME's own
12308
+ * dimensions — the image the runner is about to cut. This is coordinate
12309
+ * mapping, not crop derivation: no padding, no squaring, no rectangle choice.
12310
+ */
12311
+ function buildRebuildDetailInput(source, keyFrame) {
12312
+ return {
12313
+ deviceId: source.deviceId,
12314
+ frameJpeg: source.keyFrame.toString("base64"),
12315
+ parent: {
12316
+ bbox: {
12317
+ x: source.bbox.x * keyFrame.width,
12318
+ y: source.bbox.y * keyFrame.height,
12319
+ w: source.bbox.w * keyFrame.width,
12320
+ h: source.bbox.h * keyFrame.height
12321
+ },
12322
+ className: source.className
12323
+ },
12324
+ steps: [REBUILD_DETAIL_STEP_ID]
12325
+ };
12326
+ }
12327
+ /**
12328
+ * Turn one `runDetailSubtree` answer into a rebuild outcome.
12329
+ *
12330
+ * The three cases lead to three different counters:
12331
+ *
12332
+ * - `null` — the runner DECLINED: the camera is not attached, `clip-embedding`
12333
+ * is not enabled in its tree, or the frame was unreadable. It logs which,
12334
+ * tagged with the deviceId. That is `not-runnable`, not a failure: the remedy
12335
+ * is a configuration change, and folding it into `failed` would point the
12336
+ * operator at the inference engine instead.
12337
+ * - details with no `clip-embedding` embedding — the step RAN and produced
12338
+ * nothing usable. That is a failure.
12339
+ * - an embedding — passed through as base64, the form the vector store stores.
12340
+ */
12341
+ function readRebuildEmbedding(result) {
12342
+ if (result === null) return {
12343
+ kind: "not-runnable",
12344
+ 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)"
12345
+ };
12346
+ const embedding = result.details.find((d) => d.stepId === "clip-embedding" && d.embedding !== void 0)?.embedding;
12347
+ if (embedding === void 0) return {
12348
+ kind: "failed",
12349
+ reason: `the detail subtree ran but returned no ${REBUILD_DETAIL_STEP_ID} embedding (${result.details.length} detail(s))`
12350
+ };
12351
+ return {
12352
+ kind: "embedded",
12353
+ embeddingBase64: embedding
12354
+ };
12355
+ }
12356
+ /** Narrowing helper — a source without a key frame can never build a request. */
12357
+ function hasKeyFrame(source) {
12358
+ return source.keyFrame !== null && source.bbox !== null;
12359
+ }
12360
+ //#endregion
12372
12361
  //#region src/pipeline-analytics/pipeline/face-matcher.ts
12373
12362
  /**
12374
12363
  * Count enrolled samples per identity for probes of a matching model+dimension.
@@ -14560,6 +14549,42 @@ async function encodeKeyFrameVariants(native) {
14560
14549
  };
14561
14550
  }
14562
14551
  //#endregion
14552
+ //#region src/pipeline-analytics/pipeline/bbox-pad.ts
14553
+ /**
14554
+ * Pad a normalized bounding box by a fractional amount relative to its own
14555
+ * dimensions. All coordinates remain in the [0, 1] unit-square.
14556
+ *
14557
+ * Formula (before clamping):
14558
+ * newX = x - padding * w
14559
+ * newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
14560
+ * newY = y - padding * h
14561
+ * newH = h + 2 * padding * h
14562
+ *
14563
+ * padding === 0 is the identity operation. Pure, immutable — always returns a
14564
+ * new object.
14565
+ *
14566
+ * NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
14567
+ * bound slides inward and keeps the extent that was asked for, on either side.
14568
+ * It shrinks only when the padded window is larger than the frame itself
14569
+ * would (e.g. a left-hugging box gains a bit more right-side context). This is
14570
+ * acceptable for detection crops (more context, never out of [0,1]); the
14571
+ * downstream `extractCrop` clamps to pixel bounds again regardless.
14572
+ */
14573
+ function padBbox(bbox, padding) {
14574
+ const rawX = bbox.x - padding * bbox.w;
14575
+ const rawY = bbox.y - padding * bbox.h;
14576
+ const rawW = bbox.w * (1 + 2 * padding);
14577
+ const rawH = bbox.h * (1 + 2 * padding);
14578
+ const w = Math.min(rawW, 1);
14579
+ const h = Math.min(rawH, 1);
14580
+ return {
14581
+ x: Math.min(Math.max(0, rawX), 1 - w),
14582
+ y: Math.min(Math.max(0, rawY), 1 - h),
14583
+ w,
14584
+ h
14585
+ };
14586
+ }
14587
+ //#endregion
14563
14588
  //#region src/pipeline-analytics/pipeline/keyframe-crop.ts
14564
14589
  function createKeyFrameCrop(deps) {
14565
14590
  return async (input) => {
@@ -18549,30 +18574,18 @@ function buildGlobalSettingsSchema() {
18549
18574
  {
18550
18575
  id: "embedding-index",
18551
18576
  title: "Semantic search index",
18552
- 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.",
18577
+ 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.",
18553
18578
  columns: 1,
18554
18579
  fields: [
18555
- {
18556
- type: "number",
18557
- key: "rebuildCropMargin",
18558
- label: "Rebuild crop margin",
18559
- 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.",
18560
- default: .2,
18561
- min: 0,
18562
- max: 2,
18563
- step: .1
18564
- },
18565
18580
  {
18566
18581
  type: "addon-action-button",
18567
18582
  key: "rebuildEmbeddingsButton",
18568
18583
  label: "Rebuild index",
18569
- 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.",
18584
+ 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.",
18570
18585
  addonId: "pipeline-analytics",
18571
18586
  action: "embedding.rebuild",
18572
18587
  buttonLabel: "Rebuild",
18573
18588
  variant: "primary",
18574
- paramsFromForm: { cropMargin: "rebuildCropMargin" },
18575
- paramsStatic: { square: true },
18576
18589
  successMessage: "Rebuild started — press Progress to follow it."
18577
18590
  },
18578
18591
  {
@@ -18583,7 +18596,7 @@ function buildGlobalSettingsSchema() {
18583
18596
  addonId: "pipeline-analytics",
18584
18597
  action: "embedding.rebuildStatus",
18585
18598
  buttonLabel: "Check progress",
18586
- successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
18599
+ successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · not runnable {notRunnable} · failed {failed}"
18587
18600
  },
18588
18601
  {
18589
18602
  type: "addon-action-button",
@@ -20575,6 +20588,29 @@ var IdentityStore = class {
20575
20588
  */
20576
20589
  /** The vector-store index this store owns. */
20577
20590
  var OBJECT_CLIP_INDEX = "object-clip";
20591
+ /**
20592
+ * Reduce either input form to the stored form.
20593
+ *
20594
+ * A base64 payload passes through untouched; its dimension is read from the
20595
+ * byte length. A length that is not a whole number of float32s cannot be a
20596
+ * vector, and storing it would produce a row whose dimension silently
20597
+ * disagrees with the declared index.
20598
+ */
20599
+ function resolveVectorPayload(input) {
20600
+ if (input.embeddingBase64 !== void 0) {
20601
+ const byteLength = Buffer.from(input.embeddingBase64, "base64").byteLength;
20602
+ if (byteLength === 0 || byteLength % 4 !== 0) return null;
20603
+ return {
20604
+ base64: input.embeddingBase64,
20605
+ dim: vectorDimFromBase64(input.embeddingBase64)
20606
+ };
20607
+ }
20608
+ if (input.embedding.length === 0) return null;
20609
+ return {
20610
+ base64: encodeVectorBase64(input.embedding),
20611
+ dim: input.embedding.length
20612
+ };
20613
+ }
20578
20614
  var ObjectEmbeddingStore = class {
20579
20615
  vectorStore;
20580
20616
  logger;
@@ -20591,6 +20627,14 @@ var ObjectEmbeddingStore = class {
20591
20627
  * write: the provider rejects an upsert into an index it has never seen. A
20592
20628
  * dim change against a populated index is REFUSED rather than wiping it, so
20593
20629
  * this failing is the signal that the encoder model changed.
20630
+ *
20631
+ * `declaredDim` caches the declaration, and that cache is per-PROCESS while
20632
+ * the index lives in whichever addon currently provides `vector-store`. When
20633
+ * the active provider changed under a running hub the cache stayed warm
20634
+ * against an index that no longer existed, and every write from then on threw
20635
+ * `index "object-clip" was never declared` — for hours, until the addon was
20636
+ * restarted by hand. So a failed write clears the cache (see
20637
+ * {@link upsertIfBetter}) and the next one re-declares.
20594
20638
  */
20595
20639
  async ensureIndex(dim) {
20596
20640
  if (this.declaredDim === dim) return true;
@@ -20622,33 +20666,50 @@ var ObjectEmbeddingStore = class {
20622
20666
  * short-circuited, and every later pass silently wrote nothing while
20623
20667
  * reporting hundreds rebuilt. Observed live — 214 "rebuilt" moved the index
20624
20668
  * by 4 rows.
20669
+ *
20670
+ * ## Why this returns an outcome instead of `void`
20671
+ *
20672
+ * It used to return `void` and turn every failure into a warn. The rebuild
20673
+ * therefore counted a write the instant the call returned, and the second time
20674
+ * this class of bug appeared it produced `rebuilt: 704` against `indexed: 0` —
20675
+ * a progress number that was pure fiction while the real cause sat in a warn
20676
+ * nobody was reading. The caller now has to distinguish "written" from "logged
20677
+ * and dropped", because a counter that cannot reads as verification.
20625
20678
  */
20626
20679
  async upsertIfBetter(input) {
20627
- if (input.embedding.length === 0) return;
20628
- if (!await this.ensureIndex(input.embedding.length)) return;
20680
+ const vector = resolveVectorPayload(input);
20681
+ if (vector === null) return "failed";
20682
+ if (!await this.ensureIndex(vector.dim)) return "failed";
20629
20683
  try {
20630
20684
  if (input.force !== true) {
20631
20685
  const stored = (await this.vectorStore.getByIds.query({
20632
20686
  index: OBJECT_CLIP_INDEX,
20633
20687
  ids: [input.trackId]
20634
20688
  })).items[0]?.metadata["confidence"];
20635
- if (typeof stored === "number" && input.confidence <= stored) return;
20689
+ if (typeof stored === "number" && input.confidence <= stored) return "skipped";
20636
20690
  }
20637
- if ((await this.vectorStore.upsert.mutate({
20638
- index: "object-clip",
20691
+ const result = await this.vectorStore.upsert.mutate({
20692
+ index: OBJECT_CLIP_INDEX,
20639
20693
  items: [{
20640
20694
  id: input.trackId,
20641
- vector: encodeVectorBase64(input.embedding),
20695
+ vector: vector.base64,
20642
20696
  metadata: metadataFor(input)
20643
20697
  }]
20644
- })).rejected > 0) this.logger.warn("object-embedding upsert rejected", {
20645
- tags: { deviceId: input.deviceId },
20646
- meta: {
20647
- trackId: input.trackId,
20648
- dim: input.embedding.length
20649
- }
20650
20698
  });
20699
+ if (result.upserted === 0) {
20700
+ this.logger.warn("object-embedding upsert rejected", {
20701
+ tags: { deviceId: input.deviceId },
20702
+ meta: {
20703
+ trackId: input.trackId,
20704
+ dim: vector.dim,
20705
+ rejected: result.rejected
20706
+ }
20707
+ });
20708
+ return "failed";
20709
+ }
20710
+ return "persisted";
20651
20711
  } catch (err) {
20712
+ this.declaredDim = null;
20652
20713
  this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", {
20653
20714
  tags: { deviceId: input.deviceId },
20654
20715
  meta: {
@@ -20656,6 +20717,7 @@ var ObjectEmbeddingStore = class {
20656
20717
  error: String(err)
20657
20718
  }
20658
20719
  });
20720
+ return "failed";
20659
20721
  }
20660
20722
  }
20661
20723
  /**
@@ -27005,6 +27067,43 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27005
27067
  return info.modelId;
27006
27068
  }
27007
27069
  /**
27070
+ * Re-embed ONE stored track by running its `clip-embedding` step through the
27071
+ * camera's runner.
27072
+ *
27073
+ * The whole point: the runner cuts. This method reads the key frame's pixel
27074
+ * dimensions (metadata only — no pixel work, no rectangle chosen here) so the
27075
+ * stored normalised box can be expressed in the key frame's own pixels, ships
27076
+ * the full frame, and reads the vector back. Where the crop boundary falls is
27077
+ * decided inside `runDetailSubtree` by the one cluster-wide convention, the
27078
+ * same call the live detail plane makes.
27079
+ *
27080
+ * NOT node-pinned: `runDetailSubtree` is device-bound, so the cap router
27081
+ * sends it to the runner that owns this camera. A pin would be a guess about
27082
+ * placement that the binding already knows.
27083
+ */
27084
+ async embedRebuildSourceViaPipeline(source) {
27085
+ if (!hasKeyFrame(source)) return {
27086
+ kind: "failed",
27087
+ reason: "no key frame or no bbox"
27088
+ };
27089
+ const runner = this.ctx.api?.pipelineRunner;
27090
+ if (!runner?.runDetailSubtree) return {
27091
+ kind: "not-runnable",
27092
+ reason: "pipelineRunner.runDetailSubtree is unavailable on this cluster"
27093
+ };
27094
+ const sharp = (await import("sharp")).default;
27095
+ const meta = await sharp(source.keyFrame).metadata();
27096
+ if (meta.width === void 0 || meta.height === void 0) return {
27097
+ kind: "failed",
27098
+ reason: "key frame has no readable dimensions"
27099
+ };
27100
+ const input = buildRebuildDetailInput(source, {
27101
+ width: meta.width,
27102
+ height: meta.height
27103
+ });
27104
+ return readRebuildEmbedding(await runner.runDetailSubtree.mutate(input));
27105
+ }
27106
+ /**
27008
27107
  * Every track id the track store still holds, across the given devices.
27009
27108
  *
27010
27109
  * Returned as a Set for the orphan sweep, which REFUSES an empty one — a
@@ -27043,6 +27142,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27043
27142
  * resolution, captured at the best detection) plus that detection's bbox, so
27044
27143
  * the window can be WIDENED. The tight crop originally embedded cannot be
27045
27144
  * widened after the fact.
27145
+ *
27146
+ * The frame and the box are shipped to the camera's RUNNER, which cuts and
27147
+ * encodes them through `runDetailSubtree` — the same code path, the same
27148
+ * crop convention and the same step catalog the live detail plane uses. This
27149
+ * side no longer chooses a rectangle, and there is no `cropMargin` argument
27150
+ * to choose one with: see `embedding-rebuild.ts` for what that cost.
27046
27151
  */
27047
27152
  async rebuildObjectEmbeddings(input) {
27048
27153
  if (this.rebuildState.running) {
@@ -27100,27 +27205,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27100
27205
  ...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
27101
27206
  };
27102
27207
  },
27103
- encode: async (crop, width, height) => {
27104
- return (await api.embeddingEncoder.encode.query({
27105
- crop: new Uint8Array(crop),
27106
- width,
27107
- height
27108
- })).embedding;
27109
- },
27110
- store: async (source, embedding) => {
27111
- await store.upsertIfBetter({
27112
- trackId: source.trackId,
27113
- deviceId: source.deviceId,
27114
- timestamp: source.timestamp,
27115
- className: source.className,
27116
- embedding,
27117
- modelId,
27118
- confidence: source.confidence,
27119
- force: true,
27120
- ...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
27121
- ...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
27122
- });
27123
- },
27208
+ embed: (source) => this.embedRebuildSourceViaPipeline(source),
27209
+ store: async (source, embeddingBase64) => store.upsertIfBetter({
27210
+ trackId: source.trackId,
27211
+ deviceId: source.deviceId,
27212
+ timestamp: source.timestamp,
27213
+ className: source.className,
27214
+ embeddingBase64,
27215
+ modelId,
27216
+ confidence: source.confidence,
27217
+ force: true,
27218
+ ...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
27219
+ ...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
27220
+ }),
27124
27221
  logger: {
27125
27222
  info: (message, extras) => this.ctx.logger.info(message, extras),
27126
27223
  warn: (message, extras) => this.ctx.logger.warn(message, extras)
@@ -27134,12 +27231,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27134
27231
  rebuilt: 0,
27135
27232
  missingKeyFrame: 0,
27136
27233
  missingBbox: 0,
27234
+ notRunnable: 0,
27137
27235
  failed: 0
27138
27236
  }
27139
27237
  };
27140
27238
  rebuildObjectEmbeddings(deps, {
27141
- ...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
27142
- ...input.square !== void 0 ? { square: input.square } : {},
27143
27239
  ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
27144
27240
  onProgress: (progress) => {
27145
27241
  if (this.rebuildState.running) this.rebuildState.progress = progress;
@@ -27164,7 +27260,6 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27164
27260
  });
27165
27261
  this.ctx.logger.info("object embedding rebuild started", { meta: {
27166
27262
  deviceId: input.deviceId ?? "all",
27167
- cropMargin: input.cropMargin ?? "default",
27168
27263
  maxTracks: input.maxTracks ?? "unbounded"
27169
27264
  } });
27170
27265
  return {
@@ -27206,6 +27301,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27206
27301
  rebuilt: p?.rebuilt ?? 0,
27207
27302
  missingKeyFrame: p?.missingKeyFrame ?? 0,
27208
27303
  missingBbox: p?.missingBbox ?? 0,
27304
+ notRunnable: p?.notRunnable ?? 0,
27209
27305
  failed: p?.failed ?? 0,
27210
27306
  complete: state.complete ?? null,
27211
27307
  startedAtMs: state.startedAtMs ?? null,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DvfzPjZx.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cqisi0Ci.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.45",
3
+ "version": "1.2.47",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -87,6 +87,12 @@
87
87
  },
88
88
  {
89
89
  "name": "videoclips"
90
+ },
91
+ {
92
+ "name": "face-gallery"
93
+ },
94
+ {
95
+ "name": "plate-gallery"
90
96
  }
91
97
  ],
92
98
  "storageLocations": [