@camstack/addon-post-analysis 1.2.41 → 1.2.43

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-C09kySqI.js");
5
+ const require_dist = require("../dist-BIBeAcEY.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);
@@ -5821,6 +5821,76 @@ function makeNcActionHandlers(deps) {
5821
5821
  };
5822
5822
  }
5823
5823
  //#endregion
5824
+ //#region src/pipeline-analytics/embedding-actions.ts
5825
+ /**
5826
+ * embedding-actions — operator buttons for the CLIP semantic-search index.
5827
+ *
5828
+ * These ride the generic `addons.custom` bridge rather than adding cap methods,
5829
+ * because the settings form already knows how to render an
5830
+ * `addon-action-button` and call it. The three operations they expose are the
5831
+ * ones an operator needs when the index and the world disagree:
5832
+ *
5833
+ * - **Rebuild** re-embeds tracks from their key frames. It is the only way to
5834
+ * change what a vector MEANS — crop margin, squaring, encoder model — since
5835
+ * mixing feature spaces in one index makes cosine scores incomparable.
5836
+ * - **Status** is what makes rebuild usable at all: a pass is ~1s per track,
5837
+ * 45 minutes for a fleet, so it runs detached and this is how progress is
5838
+ * seen.
5839
+ * - **Wipe** empties the index, for the clean slate before a policy change.
5840
+ *
5841
+ * Wipe is `danger` and carries a confirm prompt. It has already destroyed a
5842
+ * live index once when it was called casually — the button must not make that
5843
+ * easier than it needs to be.
5844
+ */
5845
+ var RebuildInputSchema = require_dist.object({
5846
+ /** Restrict to one camera; omit for the whole fleet. */
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
+ /** Stop after this many tracks; the status reports whether more remain. */
5857
+ maxTracks: require_dist.number().int().positive().optional()
5858
+ });
5859
+ var RebuildStartedSchema = require_dist.object({
5860
+ started: require_dist.boolean(),
5861
+ alreadyRunning: require_dist.boolean()
5862
+ });
5863
+ var RebuildStatusSchema = require_dist.object({
5864
+ running: require_dist.boolean(),
5865
+ scanned: require_dist.number(),
5866
+ rebuilt: require_dist.number(),
5867
+ missingKeyFrame: require_dist.number(),
5868
+ missingBbox: require_dist.number(),
5869
+ failed: require_dist.number(),
5870
+ complete: require_dist.boolean().nullable(),
5871
+ startedAtMs: require_dist.number().nullable(),
5872
+ finishedAtMs: require_dist.number().nullable(),
5873
+ error: require_dist.string().nullable(),
5874
+ /** Rows currently in the index — the number the operator is watching. */
5875
+ indexed: require_dist.number()
5876
+ });
5877
+ var WipeResultSchema = require_dist.object({ deleted: require_dist.number() });
5878
+ /**
5879
+ * An input-less action still takes an OBJECT, never `z.void()`.
5880
+ *
5881
+ * The `addons.custom` envelope requires an `input` field — omitting it fails
5882
+ * envelope validation before the action is even looked up — and the settings
5883
+ * form's button always sends one. So `z.void()` makes an action that resolves
5884
+ * and can never be called: the bridge insists on `{}` and the schema rejects
5885
+ * it. Verified on the live hub, both directions.
5886
+ */
5887
+ var NoInputSchema = require_dist.object({});
5888
+ var embeddingActions = require_dist.defineCustomActions({
5889
+ "embedding.rebuild": require_dist.customAction(RebuildInputSchema, RebuildStartedSchema, { auth: "admin" }),
5890
+ "embedding.rebuildStatus": require_dist.customAction(NoInputSchema, RebuildStatusSchema),
5891
+ "embedding.wipe": require_dist.customAction(NoInputSchema, WipeResultSchema, { auth: "admin" })
5892
+ });
5893
+ //#endregion
5824
5894
  //#region src/pipeline-analytics/pipeline/face-clustering.ts
5825
5895
  /**
5826
5896
  * face-clustering — pure greedy clustering of face embeddings by cosine
@@ -12047,57 +12117,84 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12047
12117
  const margin = options.cropMargin ?? .2;
12048
12118
  const square = options.square ?? true;
12049
12119
  const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
12050
- const trackIds = await deps.listTrackIds();
12051
- const considered = trackIds.slice(0, Number.isFinite(maxTracks) ? maxTracks : void 0);
12052
12120
  let rebuilt = 0;
12053
12121
  let missingKeyFrame = 0;
12054
12122
  let missingBbox = 0;
12055
12123
  let failed = 0;
12056
- for (const trackId of considered) try {
12057
- const source = await deps.loadSource(trackId);
12058
- if (source === null) {
12059
- missingKeyFrame += 1;
12060
- continue;
12061
- }
12062
- if (source.keyFrame === null) {
12063
- missingKeyFrame += 1;
12064
- continue;
12065
- }
12066
- if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
12067
- missingBbox += 1;
12068
- continue;
12069
- }
12070
- const meta = await (0, sharp.default)(source.keyFrame).metadata();
12071
- if (meta.width === void 0 || meta.height === void 0) {
12072
- failed += 1;
12073
- continue;
12124
+ let scanned = 0;
12125
+ let cursor = null;
12126
+ let exhausted = false;
12127
+ let budgetExhausted = false;
12128
+ while (scanned < maxTracks) {
12129
+ const page = await deps.listTrackIdPage(cursor);
12130
+ if (page.trackIds.length === 0) {
12131
+ exhausted = true;
12132
+ break;
12074
12133
  }
12075
- const window = cropWindow(source.bbox, meta.width, meta.height, {
12076
- margin,
12077
- square
12078
- });
12079
- const cut = await cropFromJpeg(source.keyFrame, window);
12080
- if (cut === null) {
12081
- failed += 1;
12082
- continue;
12134
+ const room = maxTracks - scanned;
12135
+ const batch = page.trackIds.slice(0, Number.isFinite(room) ? room : void 0);
12136
+ if (batch.length < page.trackIds.length) budgetExhausted = true;
12137
+ for (const trackId of batch) {
12138
+ scanned += 1;
12139
+ try {
12140
+ const source = await deps.loadSource(trackId);
12141
+ if (source === null) {
12142
+ missingKeyFrame += 1;
12143
+ continue;
12144
+ }
12145
+ if (source.keyFrame === null) {
12146
+ missingKeyFrame += 1;
12147
+ continue;
12148
+ }
12149
+ if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
12150
+ missingBbox += 1;
12151
+ continue;
12152
+ }
12153
+ const meta = await (0, sharp.default)(source.keyFrame).metadata();
12154
+ if (meta.width === void 0 || meta.height === void 0) {
12155
+ failed += 1;
12156
+ continue;
12157
+ }
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) {
12164
+ failed += 1;
12165
+ continue;
12166
+ }
12167
+ const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12168
+ if (embedding.length === 0) {
12169
+ failed += 1;
12170
+ continue;
12171
+ }
12172
+ await deps.store(source, embedding);
12173
+ rebuilt += 1;
12174
+ } catch (err) {
12175
+ failed += 1;
12176
+ deps.logger.warn("embedding rebuild failed for a track", { meta: {
12177
+ trackId,
12178
+ error: String(err)
12179
+ } });
12180
+ }
12181
+ if (scanned % 25 === 0) options.onProgress?.({
12182
+ scanned,
12183
+ rebuilt,
12184
+ missingKeyFrame,
12185
+ missingBbox,
12186
+ failed
12187
+ });
12083
12188
  }
12084
- const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12085
- if (embedding.length === 0) {
12086
- failed += 1;
12087
- continue;
12189
+ if (page.nextCursor === null) {
12190
+ exhausted = true;
12191
+ break;
12088
12192
  }
12089
- await deps.store(source, embedding);
12090
- rebuilt += 1;
12091
- } catch (err) {
12092
- failed += 1;
12093
- deps.logger.warn("embedding rebuild failed for a track", { meta: {
12094
- trackId,
12095
- error: String(err)
12096
- } });
12193
+ cursor = page.nextCursor;
12097
12194
  }
12098
- const complete = considered.length === trackIds.length;
12195
+ const complete = exhausted && !budgetExhausted;
12099
12196
  deps.logger.info("object embedding rebuild finished", { meta: {
12100
- scanned: considered.length,
12197
+ scanned,
12101
12198
  rebuilt,
12102
12199
  missingKeyFrame,
12103
12200
  missingBbox,
@@ -12107,7 +12204,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12107
12204
  square
12108
12205
  } });
12109
12206
  return {
12110
- scanned: considered.length,
12207
+ scanned,
12111
12208
  rebuilt,
12112
12209
  missingKeyFrame,
12113
12210
  missingBbox,
@@ -17530,26 +17627,6 @@ var SyntheticSensorTrackMaterializer = class {
17530
17627
  return track;
17531
17628
  }
17532
17629
  };
17533
- var TRACK_RETENTION_DEFAULT_DAYS = require_dist.object({ trackRetentionDays: require_dist.number().min(0).default(7) }).parse({}).trackRetentionDays;
17534
- /**
17535
- * Sweep every device with persisted tracks: skip retention-disabled devices,
17536
- * prune the rest at `now − retentionMs`. Per-device ISOLATED — one bad device
17537
- * never aborts the sweep. Returns the total tracks pruned across devices.
17538
- */
17539
- async function sweepTrackRetention(deps) {
17540
- const devices = await deps.listDeviceIds();
17541
- const nowMs = deps.now();
17542
- let totalTracks = 0;
17543
- for (const deviceId of devices) try {
17544
- const cutoffMs = await deps.resolveCutoffMs(deviceId, nowMs);
17545
- if (cutoffMs === null) continue;
17546
- const counts = await deps.pruneTracksBefore(deviceId, cutoffMs);
17547
- totalTracks += counts.tracks;
17548
- } catch (err) {
17549
- deps.onError?.(deviceId, err);
17550
- }
17551
- return totalTracks;
17552
- }
17553
17630
  var TrackingSettingsSchema = require_dist.object({
17554
17631
  /**
17555
17632
  * How much of a detection's box must lie inside a zone (0-1 fraction of the
@@ -18345,6 +18422,26 @@ function buildDetectionSettingsSections() {
18345
18422
  }
18346
18423
  ];
18347
18424
  }
18425
+ var TRACK_RETENTION_DEFAULT_DAYS = require_dist.object({ trackRetentionDays: require_dist.number().min(0).default(7) }).parse({}).trackRetentionDays;
18426
+ /**
18427
+ * Sweep every device with persisted tracks: skip retention-disabled devices,
18428
+ * prune the rest at `now − retentionMs`. Per-device ISOLATED — one bad device
18429
+ * never aborts the sweep. Returns the total tracks pruned across devices.
18430
+ */
18431
+ async function sweepTrackRetention(deps) {
18432
+ const devices = await deps.listDeviceIds();
18433
+ const nowMs = deps.now();
18434
+ let totalTracks = 0;
18435
+ for (const deviceId of devices) try {
18436
+ const cutoffMs = await deps.resolveCutoffMs(deviceId, nowMs);
18437
+ if (cutoffMs === null) continue;
18438
+ const counts = await deps.pruneTracksBefore(deviceId, cutoffMs);
18439
+ totalTracks += counts.tracks;
18440
+ } catch (err) {
18441
+ deps.onError?.(deviceId, err);
18442
+ }
18443
+ return totalTracks;
18444
+ }
18348
18445
  /**
18349
18446
  * Re-home the global analytics sections into the per-device `Analytics`
18350
18447
  * top-tab. Every section defaults to the `Analytics` tab (so the
@@ -18455,6 +18552,59 @@ function buildGlobalSettingsSchema() {
18455
18552
  default: true
18456
18553
  }]
18457
18554
  },
18555
+ {
18556
+ id: "embedding-index",
18557
+ 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.",
18559
+ columns: 1,
18560
+ 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
+ {
18572
+ type: "addon-action-button",
18573
+ key: "rebuildEmbeddingsButton",
18574
+ 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.",
18576
+ addonId: "pipeline-analytics",
18577
+ action: "embedding.rebuild",
18578
+ buttonLabel: "Rebuild",
18579
+ variant: "primary",
18580
+ paramsFromForm: { cropMargin: "rebuildCropMargin" },
18581
+ paramsStatic: { square: true },
18582
+ successMessage: "Rebuild started — press Progress to follow it."
18583
+ },
18584
+ {
18585
+ type: "addon-action-button",
18586
+ key: "rebuildStatusButton",
18587
+ label: "Progress",
18588
+ description: "Rows currently indexed, plus the running pass: scanned, rebuilt, and what it had to skip.",
18589
+ addonId: "pipeline-analytics",
18590
+ action: "embedding.rebuildStatus",
18591
+ buttonLabel: "Check progress",
18592
+ successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
18593
+ },
18594
+ {
18595
+ type: "addon-action-button",
18596
+ key: "wipeEmbeddingsButton",
18597
+ label: "Empty the index",
18598
+ description: "Deletes every vector. Search returns nothing until a rebuild repopulates it, and only tracks still inside the retention window can be rebuilt — anything older is gone for good.",
18599
+ addonId: "pipeline-analytics",
18600
+ action: "embedding.wipe",
18601
+ buttonLabel: "Empty index",
18602
+ variant: "danger",
18603
+ confirmMessage: "Delete every semantic-search vector? Search stops working until a rebuild finishes, and vectors whose tracks have aged out cannot be recovered.",
18604
+ successMessage: "Removed {deleted} vectors."
18605
+ }
18606
+ ]
18607
+ },
18458
18608
  {
18459
18609
  id: "tracking-core",
18460
18610
  title: "Tracking",
@@ -22949,10 +23099,30 @@ var ENCODER_INFO_TTL_MS = 6e4;
22949
23099
  var EMBEDDING_ORPHAN_SCAN_LIMIT = 2e4;
22950
23100
  /** Dimension of the zero probe vector used to enumerate the index. */
22951
23101
  var EMBEDDING_ORPHAN_PROBE_DIM = 512;
23102
+ /**
23103
+ * Tracks fetched per page while rebuilding.
23104
+ *
23105
+ * The page size is NOT the caller's `maxTracks`: that bounds the whole pass,
23106
+ * this bounds one round trip. Conflating them is what made the rebuild
23107
+ * re-embed the same newest N tracks on every call.
23108
+ */
23109
+ var REBUILD_PAGE_SIZE = 200;
22952
23110
  var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
22953
23111
  /** Operator-visible name of the fixed integration. */
22954
23112
  var NC_INTEGRATION_NAME = "Notification Center";
22955
23113
  /**
23114
+ * The FULL action catalog for this addon.
23115
+ *
23116
+ * Must contain every action, not just the notification-center ones: the hub
23117
+ * harvests THIS named export, so an action registered only in what
23118
+ * `onInitialize` returns is invisible to the bridge and answers 404. That is
23119
+ * exactly what the embedding buttons did on their first deploy.
23120
+ */
23121
+ var customActions = {
23122
+ ...ncActions,
23123
+ ...embeddingActions
23124
+ };
23125
+ /**
22956
23126
  * Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
22957
23127
  * `PipelineAudioInferenceResult` on the bus, refines raw detections
22958
23128
  * into tracks + per-kind events + persisted media, and emits canonical
@@ -23136,6 +23306,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23136
23306
  plateStore = null;
23137
23307
  plateRecognizer = null;
23138
23308
  objectEmbeddingStore = null;
23309
+ /** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
23310
+ rebuildState = { running: false };
23139
23311
  /** Cached active-encoder model id — see `resolveEncoderModelId`. */
23140
23312
  encoderInfoCache = null;
23141
23313
  /** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
@@ -23610,8 +23782,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23610
23782
  return {
23611
23783
  providers,
23612
23784
  ...isHub && ncHandlers !== void 0 ? {
23613
- customActions: ncActions,
23614
- actionHandlers: ncHandlers
23785
+ customActions: {
23786
+ ...ncActions,
23787
+ ...embeddingActions
23788
+ },
23789
+ actionHandlers: {
23790
+ ...ncHandlers,
23791
+ ...this.buildEmbeddingActionHandlers()
23792
+ }
23615
23793
  } : {}
23616
23794
  };
23617
23795
  }
@@ -23760,7 +23938,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23760
23938
  let storage = this.ctx.kernel.storage;
23761
23939
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
23762
23940
  if (mediaRoot) {
23763
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DL2FM8_E.js"));
23941
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-INtH-sDE.js"));
23764
23942
  storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
23765
23943
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
23766
23944
  }
@@ -26864,33 +27042,40 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
26864
27042
  * widened after the fact.
26865
27043
  */
26866
27044
  async rebuildObjectEmbeddings(input) {
27045
+ if (this.rebuildState.running) {
27046
+ this.ctx.logger.info("object embedding rebuild already running — request ignored");
27047
+ return {
27048
+ started: false,
27049
+ alreadyRunning: true
27050
+ };
27051
+ }
26867
27052
  const store = this.objectEmbeddingStore;
26868
27053
  const trackStore = this.trackStore;
26869
27054
  const mediaStore = this.mediaStore;
26870
27055
  const eventStore = this.eventStore;
26871
27056
  const api = this.ctx.api;
26872
- const empty = {
26873
- scanned: 0,
26874
- rebuilt: 0,
26875
- missingKeyFrame: 0,
26876
- missingBbox: 0,
26877
- failed: 0,
26878
- complete: true
26879
- };
26880
27057
  if (!store || !trackStore || !mediaStore || !eventStore || !api) {
26881
27058
  this.ctx.logger.warn("rebuildObjectEmbeddings: analytics stores unavailable");
26882
- return empty;
27059
+ return {
27060
+ started: false,
27061
+ alreadyRunning: false
27062
+ };
26883
27063
  }
26884
27064
  const modelId = await this.resolveEncoderModelId(api) ?? "unknown";
26885
27065
  const deviceIds = input.deviceId !== void 0 ? [input.deviceId] : await trackStore.listDeviceIds();
26886
- return rebuildObjectEmbeddings({
26887
- listTrackIds: async () => {
26888
- return (await trackStore.queryRecent({
27066
+ const deps = {
27067
+ listTrackIdPage: async (cursor) => {
27068
+ const page = await trackStore.queryRecent({
26889
27069
  deviceIds,
26890
27070
  ...input.since !== void 0 ? { since: input.since } : {},
26891
27071
  ...input.until !== void 0 ? { until: input.until } : {},
26892
- limit: input.maxTracks ?? 500
26893
- })).tracks.map((t) => t.trackId);
27072
+ ...cursor !== null ? { cursor } : {},
27073
+ limit: REBUILD_PAGE_SIZE
27074
+ });
27075
+ return {
27076
+ trackIds: page.tracks.map((t) => t.trackId),
27077
+ nextCursor: page.nextCursor
27078
+ };
26894
27079
  },
26895
27080
  loadSource: async (trackId) => {
26896
27081
  const media = await mediaStore.listByOwner("track", trackId);
@@ -26936,11 +27121,93 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
26936
27121
  info: (message, extras) => this.ctx.logger.info(message, extras),
26937
27122
  warn: (message, extras) => this.ctx.logger.warn(message, extras)
26938
27123
  }
26939
- }, {
27124
+ };
27125
+ this.rebuildState = {
27126
+ running: true,
27127
+ startedAtMs: Date.now(),
27128
+ progress: {
27129
+ scanned: 0,
27130
+ rebuilt: 0,
27131
+ missingKeyFrame: 0,
27132
+ missingBbox: 0,
27133
+ failed: 0
27134
+ }
27135
+ };
27136
+ rebuildObjectEmbeddings(deps, {
26940
27137
  ...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
26941
27138
  ...input.square !== void 0 ? { square: input.square } : {},
26942
- ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {}
27139
+ ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
27140
+ onProgress: (progress) => {
27141
+ if (this.rebuildState.running) this.rebuildState.progress = progress;
27142
+ this.ctx.logger.info("object embedding rebuild progress", { meta: { ...progress } });
27143
+ }
27144
+ }).then((outcome) => {
27145
+ this.rebuildState = {
27146
+ running: false,
27147
+ startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
27148
+ finishedAtMs: Date.now(),
27149
+ progress: outcome,
27150
+ complete: outcome.complete
27151
+ };
27152
+ }).catch((err) => {
27153
+ this.rebuildState = {
27154
+ running: false,
27155
+ startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
27156
+ finishedAtMs: Date.now(),
27157
+ error: String(err)
27158
+ };
27159
+ this.ctx.logger.warn("object embedding rebuild failed", { meta: { error: String(err) } });
26943
27160
  });
27161
+ this.ctx.logger.info("object embedding rebuild started", { meta: {
27162
+ deviceId: input.deviceId ?? "all",
27163
+ cropMargin: input.cropMargin ?? "default",
27164
+ maxTracks: input.maxTracks ?? "unbounded"
27165
+ } });
27166
+ return {
27167
+ started: true,
27168
+ alreadyRunning: false
27169
+ };
27170
+ }
27171
+ /**
27172
+ * Handlers behind the embedding buttons in the settings form.
27173
+ *
27174
+ * They are thin wrappers over the cap methods on purpose: the button and the
27175
+ * API must not be able to drift into two behaviours.
27176
+ */
27177
+ buildEmbeddingActionHandlers() {
27178
+ return {
27179
+ "embedding.rebuild": async (input) => this.rebuildObjectEmbeddings(input),
27180
+ "embedding.rebuildStatus": async () => {
27181
+ const status = await this.getObjectEmbeddingRebuildStatus();
27182
+ let indexed = 0;
27183
+ try {
27184
+ const api = this.ctx.api;
27185
+ if (api) indexed = (await api.vectorStore.stats.query({ index: OBJECT_CLIP_INDEX })).count;
27186
+ } catch {}
27187
+ return {
27188
+ ...status,
27189
+ indexed
27190
+ };
27191
+ },
27192
+ "embedding.wipe": async () => this.wipeObjectEmbeddings()
27193
+ };
27194
+ }
27195
+ /** Progress of the detached rebuild — the only way to observe a long pass. */
27196
+ async getObjectEmbeddingRebuildStatus() {
27197
+ const state = this.rebuildState;
27198
+ const p = state.progress;
27199
+ return {
27200
+ running: state.running,
27201
+ scanned: p?.scanned ?? 0,
27202
+ rebuilt: p?.rebuilt ?? 0,
27203
+ missingKeyFrame: p?.missingKeyFrame ?? 0,
27204
+ missingBbox: p?.missingBbox ?? 0,
27205
+ failed: p?.failed ?? 0,
27206
+ complete: state.complete ?? null,
27207
+ startedAtMs: state.startedAtMs ?? null,
27208
+ finishedAtMs: state.finishedAtMs ?? null,
27209
+ error: state.error ?? null
27210
+ };
26944
27211
  }
26945
27212
  async searchObjectEvents(input) {
26946
27213
  const store = this.objectEmbeddingStore;
@@ -27354,7 +27621,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27354
27621
  };
27355
27622
  //#endregion
27356
27623
  exports.DETECTION_PIPELINE_SECTION_IDS = DETECTION_PIPELINE_SECTION_IDS;
27357
- exports.customActions = ncActions;
27624
+ exports.customActions = customActions;
27358
27625
  exports.default = PipelineAnalyticsAddon;
27359
27626
  exports.ncActions = ncActions;
27360
27627
  exports.pickCleanMedia = pickCleanMedia;