@camstack/addon-post-analysis 1.2.40 → 1.2.42

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-CUnWKs0D.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,66 @@ 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
+ var embeddingActions = require_dist.defineCustomActions({
5879
+ "embedding.rebuild": require_dist.customAction(RebuildInputSchema, RebuildStartedSchema, { auth: "admin" }),
5880
+ "embedding.rebuildStatus": require_dist.customAction(require_dist._void(), RebuildStatusSchema),
5881
+ "embedding.wipe": require_dist.customAction(require_dist._void(), WipeResultSchema, { auth: "admin" })
5882
+ });
5883
+ //#endregion
5824
5884
  //#region src/pipeline-analytics/pipeline/face-clustering.ts
5825
5885
  /**
5826
5886
  * face-clustering — pure greedy clustering of face embeddings by cosine
@@ -12047,57 +12107,84 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12047
12107
  const margin = options.cropMargin ?? .2;
12048
12108
  const square = options.square ?? true;
12049
12109
  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
12110
  let rebuilt = 0;
12053
12111
  let missingKeyFrame = 0;
12054
12112
  let missingBbox = 0;
12055
12113
  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;
12114
+ let scanned = 0;
12115
+ let cursor = null;
12116
+ let exhausted = false;
12117
+ let budgetExhausted = false;
12118
+ while (scanned < maxTracks) {
12119
+ const page = await deps.listTrackIdPage(cursor);
12120
+ if (page.trackIds.length === 0) {
12121
+ exhausted = true;
12122
+ break;
12074
12123
  }
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;
12124
+ const room = maxTracks - scanned;
12125
+ const batch = page.trackIds.slice(0, Number.isFinite(room) ? room : void 0);
12126
+ if (batch.length < page.trackIds.length) budgetExhausted = true;
12127
+ for (const trackId of batch) {
12128
+ scanned += 1;
12129
+ try {
12130
+ const source = await deps.loadSource(trackId);
12131
+ if (source === null) {
12132
+ missingKeyFrame += 1;
12133
+ continue;
12134
+ }
12135
+ if (source.keyFrame === null) {
12136
+ missingKeyFrame += 1;
12137
+ continue;
12138
+ }
12139
+ if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
12140
+ missingBbox += 1;
12141
+ continue;
12142
+ }
12143
+ const meta = await (0, sharp.default)(source.keyFrame).metadata();
12144
+ if (meta.width === void 0 || meta.height === void 0) {
12145
+ failed += 1;
12146
+ continue;
12147
+ }
12148
+ const window = cropWindow(source.bbox, meta.width, meta.height, {
12149
+ margin,
12150
+ square
12151
+ });
12152
+ const cut = await cropFromJpeg(source.keyFrame, window);
12153
+ if (cut === null) {
12154
+ failed += 1;
12155
+ continue;
12156
+ }
12157
+ const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12158
+ if (embedding.length === 0) {
12159
+ failed += 1;
12160
+ continue;
12161
+ }
12162
+ await deps.store(source, embedding);
12163
+ rebuilt += 1;
12164
+ } catch (err) {
12165
+ failed += 1;
12166
+ deps.logger.warn("embedding rebuild failed for a track", { meta: {
12167
+ trackId,
12168
+ error: String(err)
12169
+ } });
12170
+ }
12171
+ if (scanned % 25 === 0) options.onProgress?.({
12172
+ scanned,
12173
+ rebuilt,
12174
+ missingKeyFrame,
12175
+ missingBbox,
12176
+ failed
12177
+ });
12083
12178
  }
12084
- const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12085
- if (embedding.length === 0) {
12086
- failed += 1;
12087
- continue;
12179
+ if (page.nextCursor === null) {
12180
+ exhausted = true;
12181
+ break;
12088
12182
  }
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
- } });
12183
+ cursor = page.nextCursor;
12097
12184
  }
12098
- const complete = considered.length === trackIds.length;
12185
+ const complete = exhausted && !budgetExhausted;
12099
12186
  deps.logger.info("object embedding rebuild finished", { meta: {
12100
- scanned: considered.length,
12187
+ scanned,
12101
12188
  rebuilt,
12102
12189
  missingKeyFrame,
12103
12190
  missingBbox,
@@ -12107,7 +12194,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12107
12194
  square
12108
12195
  } });
12109
12196
  return {
12110
- scanned: considered.length,
12197
+ scanned,
12111
12198
  rebuilt,
12112
12199
  missingKeyFrame,
12113
12200
  missingBbox,
@@ -17530,26 +17617,6 @@ var SyntheticSensorTrackMaterializer = class {
17530
17617
  return track;
17531
17618
  }
17532
17619
  };
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
17620
  var TrackingSettingsSchema = require_dist.object({
17554
17621
  /**
17555
17622
  * How much of a detection's box must lie inside a zone (0-1 fraction of the
@@ -18345,6 +18412,26 @@ function buildDetectionSettingsSections() {
18345
18412
  }
18346
18413
  ];
18347
18414
  }
18415
+ var TRACK_RETENTION_DEFAULT_DAYS = require_dist.object({ trackRetentionDays: require_dist.number().min(0).default(7) }).parse({}).trackRetentionDays;
18416
+ /**
18417
+ * Sweep every device with persisted tracks: skip retention-disabled devices,
18418
+ * prune the rest at `now − retentionMs`. Per-device ISOLATED — one bad device
18419
+ * never aborts the sweep. Returns the total tracks pruned across devices.
18420
+ */
18421
+ async function sweepTrackRetention(deps) {
18422
+ const devices = await deps.listDeviceIds();
18423
+ const nowMs = deps.now();
18424
+ let totalTracks = 0;
18425
+ for (const deviceId of devices) try {
18426
+ const cutoffMs = await deps.resolveCutoffMs(deviceId, nowMs);
18427
+ if (cutoffMs === null) continue;
18428
+ const counts = await deps.pruneTracksBefore(deviceId, cutoffMs);
18429
+ totalTracks += counts.tracks;
18430
+ } catch (err) {
18431
+ deps.onError?.(deviceId, err);
18432
+ }
18433
+ return totalTracks;
18434
+ }
18348
18435
  /**
18349
18436
  * Re-home the global analytics sections into the per-device `Analytics`
18350
18437
  * top-tab. Every section defaults to the `Analytics` tab (so the
@@ -18455,6 +18542,59 @@ function buildGlobalSettingsSchema() {
18455
18542
  default: true
18456
18543
  }]
18457
18544
  },
18545
+ {
18546
+ id: "embedding-index",
18547
+ title: "Semantic search index",
18548
+ 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.",
18549
+ columns: 1,
18550
+ fields: [
18551
+ {
18552
+ type: "number",
18553
+ key: "rebuildCropMargin",
18554
+ label: "Rebuild crop margin",
18555
+ 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.",
18556
+ default: .2,
18557
+ min: 0,
18558
+ max: 2,
18559
+ step: .1
18560
+ },
18561
+ {
18562
+ type: "addon-action-button",
18563
+ key: "rebuildEmbeddingsButton",
18564
+ label: "Rebuild index",
18565
+ 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.",
18566
+ addonId: "pipeline-analytics",
18567
+ action: "embedding.rebuild",
18568
+ buttonLabel: "Rebuild",
18569
+ variant: "primary",
18570
+ paramsFromForm: { cropMargin: "rebuildCropMargin" },
18571
+ paramsStatic: { square: true },
18572
+ successMessage: "Rebuild started — press Progress to follow it."
18573
+ },
18574
+ {
18575
+ type: "addon-action-button",
18576
+ key: "rebuildStatusButton",
18577
+ label: "Progress",
18578
+ description: "Rows currently indexed, plus the running pass: scanned, rebuilt, and what it had to skip.",
18579
+ addonId: "pipeline-analytics",
18580
+ action: "embedding.rebuildStatus",
18581
+ buttonLabel: "Check progress",
18582
+ successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
18583
+ },
18584
+ {
18585
+ type: "addon-action-button",
18586
+ key: "wipeEmbeddingsButton",
18587
+ label: "Empty the index",
18588
+ 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.",
18589
+ addonId: "pipeline-analytics",
18590
+ action: "embedding.wipe",
18591
+ buttonLabel: "Empty index",
18592
+ variant: "danger",
18593
+ confirmMessage: "Delete every semantic-search vector? Search stops working until a rebuild finishes, and vectors whose tracks have aged out cannot be recovered.",
18594
+ successMessage: "Removed {deleted} vectors."
18595
+ }
18596
+ ]
18597
+ },
18458
18598
  {
18459
18599
  id: "tracking-core",
18460
18600
  title: "Tracking",
@@ -22939,10 +23079,40 @@ function buildStationarySnapshot(input) {
22939
23079
  * effect without a restart and long enough that no search pays for it twice.
22940
23080
  */
22941
23081
  var ENCODER_INFO_TTL_MS = 6e4;
23082
+ /**
23083
+ * Rows the orphan sweep considers per pass, on both sides of the comparison.
23084
+ *
23085
+ * Bounded so a retention tick cannot turn into a full-index scan. A pass that
23086
+ * does not reach everything simply leaves the rest for the next tick — orphans
23087
+ * are not urgent, they are just dead weight the search has to skip.
23088
+ */
23089
+ var EMBEDDING_ORPHAN_SCAN_LIMIT = 2e4;
23090
+ /** Dimension of the zero probe vector used to enumerate the index. */
23091
+ var EMBEDDING_ORPHAN_PROBE_DIM = 512;
23092
+ /**
23093
+ * Tracks fetched per page while rebuilding.
23094
+ *
23095
+ * The page size is NOT the caller's `maxTracks`: that bounds the whole pass,
23096
+ * this bounds one round trip. Conflating them is what made the rebuild
23097
+ * re-embed the same newest N tracks on every call.
23098
+ */
23099
+ var REBUILD_PAGE_SIZE = 200;
22942
23100
  var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
22943
23101
  /** Operator-visible name of the fixed integration. */
22944
23102
  var NC_INTEGRATION_NAME = "Notification Center";
22945
23103
  /**
23104
+ * The FULL action catalog for this addon.
23105
+ *
23106
+ * Must contain every action, not just the notification-center ones: the hub
23107
+ * harvests THIS named export, so an action registered only in what
23108
+ * `onInitialize` returns is invisible to the bridge and answers 404. That is
23109
+ * exactly what the embedding buttons did on their first deploy.
23110
+ */
23111
+ var customActions = {
23112
+ ...ncActions,
23113
+ ...embeddingActions
23114
+ };
23115
+ /**
22946
23116
  * Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
22947
23117
  * `PipelineAudioInferenceResult` on the bus, refines raw detections
22948
23118
  * into tracks + per-kind events + persisted media, and emits canonical
@@ -23126,6 +23296,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23126
23296
  plateStore = null;
23127
23297
  plateRecognizer = null;
23128
23298
  objectEmbeddingStore = null;
23299
+ /** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
23300
+ rebuildState = { running: false };
23129
23301
  /** Cached active-encoder model id — see `resolveEncoderModelId`. */
23130
23302
  encoderInfoCache = null;
23131
23303
  /** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
@@ -23600,8 +23772,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23600
23772
  return {
23601
23773
  providers,
23602
23774
  ...isHub && ncHandlers !== void 0 ? {
23603
- customActions: ncActions,
23604
- actionHandlers: ncHandlers
23775
+ customActions: {
23776
+ ...ncActions,
23777
+ ...embeddingActions
23778
+ },
23779
+ actionHandlers: {
23780
+ ...ncHandlers,
23781
+ ...this.buildEmbeddingActionHandlers()
23782
+ }
23605
23783
  } : {}
23606
23784
  };
23607
23785
  }
@@ -23750,7 +23928,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
23750
23928
  let storage = this.ctx.kernel.storage;
23751
23929
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
23752
23930
  if (mediaRoot) {
23753
- const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DL2FM8_E.js"));
23931
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-ByXsH-Ce.js"));
23754
23932
  storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
23755
23933
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
23756
23934
  }
@@ -26228,6 +26406,23 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
26228
26406
  }
26229
26407
  }
26230
26408
  if (deletedEmbeddings > 0) this.ctx.logger.info("object embedding retention prune", { meta: { deleted: deletedEmbeddings } });
26409
+ try {
26410
+ const live = await this.collectLiveTrackIds(deviceIds);
26411
+ const stored = await embeddingStore.searchByVector({
26412
+ vector: new Float32Array(EMBEDDING_ORPHAN_PROBE_DIM),
26413
+ limit: EMBEDDING_ORPHAN_SCAN_LIMIT
26414
+ });
26415
+ const orphanResult = await embeddingStore.pruneOrphans({
26416
+ liveTrackIds: live,
26417
+ candidates: stored.hits.map((h) => h.row.trackId)
26418
+ });
26419
+ if (orphanResult.deleted > 0) this.ctx.logger.info("object embedding orphan sweep", { meta: {
26420
+ deleted: orphanResult.deleted,
26421
+ scanned: stored.hits.length
26422
+ } });
26423
+ } catch (err) {
26424
+ this.ctx.logger.debug("object embedding orphan sweep failed", { meta: { error: String(err) } });
26425
+ }
26231
26426
  }
26232
26427
  } catch (err) {
26233
26428
  this.ctx.logger.debug("sweepRetention failed", { meta: { error: String(err) } });
@@ -26797,6 +26992,25 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
26797
26992
  return info.modelId;
26798
26993
  }
26799
26994
  /**
26995
+ * Every track id the track store still holds, across the given devices.
26996
+ *
26997
+ * Returned as a Set for the orphan sweep, which REFUSES an empty one — a
26998
+ * store that failed to answer must not be read as "no tracks exist".
26999
+ */
27000
+ async collectLiveTrackIds(deviceIds) {
27001
+ const live = /* @__PURE__ */ new Set();
27002
+ const trackStore = this.trackStore;
27003
+ if (!trackStore) return live;
27004
+ for (const deviceId of deviceIds) {
27005
+ const page = await trackStore.queryRecent({
27006
+ deviceIds: [deviceId],
27007
+ limit: EMBEDDING_ORPHAN_SCAN_LIMIT
27008
+ });
27009
+ for (const track of page.tracks) live.add(track.trackId);
27010
+ }
27011
+ return live;
27012
+ }
27013
+ /**
26800
27014
  * Empty the CLIP index — the clean slate before a crop-policy or encoder
26801
27015
  * change, to be followed by {@link rebuildObjectEmbeddings}.
26802
27016
  */
@@ -26818,33 +27032,40 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
26818
27032
  * widened after the fact.
26819
27033
  */
26820
27034
  async rebuildObjectEmbeddings(input) {
27035
+ if (this.rebuildState.running) {
27036
+ this.ctx.logger.info("object embedding rebuild already running — request ignored");
27037
+ return {
27038
+ started: false,
27039
+ alreadyRunning: true
27040
+ };
27041
+ }
26821
27042
  const store = this.objectEmbeddingStore;
26822
27043
  const trackStore = this.trackStore;
26823
27044
  const mediaStore = this.mediaStore;
26824
27045
  const eventStore = this.eventStore;
26825
27046
  const api = this.ctx.api;
26826
- const empty = {
26827
- scanned: 0,
26828
- rebuilt: 0,
26829
- missingKeyFrame: 0,
26830
- missingBbox: 0,
26831
- failed: 0,
26832
- complete: true
26833
- };
26834
27047
  if (!store || !trackStore || !mediaStore || !eventStore || !api) {
26835
27048
  this.ctx.logger.warn("rebuildObjectEmbeddings: analytics stores unavailable");
26836
- return empty;
27049
+ return {
27050
+ started: false,
27051
+ alreadyRunning: false
27052
+ };
26837
27053
  }
26838
27054
  const modelId = await this.resolveEncoderModelId(api) ?? "unknown";
26839
27055
  const deviceIds = input.deviceId !== void 0 ? [input.deviceId] : await trackStore.listDeviceIds();
26840
- return rebuildObjectEmbeddings({
26841
- listTrackIds: async () => {
26842
- return (await trackStore.queryRecent({
27056
+ const deps = {
27057
+ listTrackIdPage: async (cursor) => {
27058
+ const page = await trackStore.queryRecent({
26843
27059
  deviceIds,
26844
27060
  ...input.since !== void 0 ? { since: input.since } : {},
26845
27061
  ...input.until !== void 0 ? { until: input.until } : {},
26846
- limit: input.maxTracks ?? 500
26847
- })).tracks.map((t) => t.trackId);
27062
+ ...cursor !== null ? { cursor } : {},
27063
+ limit: REBUILD_PAGE_SIZE
27064
+ });
27065
+ return {
27066
+ trackIds: page.tracks.map((t) => t.trackId),
27067
+ nextCursor: page.nextCursor
27068
+ };
26848
27069
  },
26849
27070
  loadSource: async (trackId) => {
26850
27071
  const media = await mediaStore.listByOwner("track", trackId);
@@ -26890,11 +27111,93 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
26890
27111
  info: (message, extras) => this.ctx.logger.info(message, extras),
26891
27112
  warn: (message, extras) => this.ctx.logger.warn(message, extras)
26892
27113
  }
26893
- }, {
27114
+ };
27115
+ this.rebuildState = {
27116
+ running: true,
27117
+ startedAtMs: Date.now(),
27118
+ progress: {
27119
+ scanned: 0,
27120
+ rebuilt: 0,
27121
+ missingKeyFrame: 0,
27122
+ missingBbox: 0,
27123
+ failed: 0
27124
+ }
27125
+ };
27126
+ rebuildObjectEmbeddings(deps, {
26894
27127
  ...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
26895
27128
  ...input.square !== void 0 ? { square: input.square } : {},
26896
- ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {}
27129
+ ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
27130
+ onProgress: (progress) => {
27131
+ if (this.rebuildState.running) this.rebuildState.progress = progress;
27132
+ this.ctx.logger.info("object embedding rebuild progress", { meta: { ...progress } });
27133
+ }
27134
+ }).then((outcome) => {
27135
+ this.rebuildState = {
27136
+ running: false,
27137
+ startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
27138
+ finishedAtMs: Date.now(),
27139
+ progress: outcome,
27140
+ complete: outcome.complete
27141
+ };
27142
+ }).catch((err) => {
27143
+ this.rebuildState = {
27144
+ running: false,
27145
+ startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
27146
+ finishedAtMs: Date.now(),
27147
+ error: String(err)
27148
+ };
27149
+ this.ctx.logger.warn("object embedding rebuild failed", { meta: { error: String(err) } });
26897
27150
  });
27151
+ this.ctx.logger.info("object embedding rebuild started", { meta: {
27152
+ deviceId: input.deviceId ?? "all",
27153
+ cropMargin: input.cropMargin ?? "default",
27154
+ maxTracks: input.maxTracks ?? "unbounded"
27155
+ } });
27156
+ return {
27157
+ started: true,
27158
+ alreadyRunning: false
27159
+ };
27160
+ }
27161
+ /**
27162
+ * Handlers behind the embedding buttons in the settings form.
27163
+ *
27164
+ * They are thin wrappers over the cap methods on purpose: the button and the
27165
+ * API must not be able to drift into two behaviours.
27166
+ */
27167
+ buildEmbeddingActionHandlers() {
27168
+ return {
27169
+ "embedding.rebuild": async (input) => this.rebuildObjectEmbeddings(input),
27170
+ "embedding.rebuildStatus": async () => {
27171
+ const status = await this.getObjectEmbeddingRebuildStatus();
27172
+ let indexed = 0;
27173
+ try {
27174
+ const api = this.ctx.api;
27175
+ if (api) indexed = (await api.vectorStore.stats.query({ index: OBJECT_CLIP_INDEX })).count;
27176
+ } catch {}
27177
+ return {
27178
+ ...status,
27179
+ indexed
27180
+ };
27181
+ },
27182
+ "embedding.wipe": async () => this.wipeObjectEmbeddings()
27183
+ };
27184
+ }
27185
+ /** Progress of the detached rebuild — the only way to observe a long pass. */
27186
+ async getObjectEmbeddingRebuildStatus() {
27187
+ const state = this.rebuildState;
27188
+ const p = state.progress;
27189
+ return {
27190
+ running: state.running,
27191
+ scanned: p?.scanned ?? 0,
27192
+ rebuilt: p?.rebuilt ?? 0,
27193
+ missingKeyFrame: p?.missingKeyFrame ?? 0,
27194
+ missingBbox: p?.missingBbox ?? 0,
27195
+ failed: p?.failed ?? 0,
27196
+ complete: state.complete ?? null,
27197
+ startedAtMs: state.startedAtMs ?? null,
27198
+ finishedAtMs: state.finishedAtMs ?? null,
27199
+ error: state.error ?? null
27200
+ };
26898
27201
  }
26899
27202
  async searchObjectEvents(input) {
26900
27203
  const store = this.objectEmbeddingStore;
@@ -27308,7 +27611,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
27308
27611
  };
27309
27612
  //#endregion
27310
27613
  exports.DETECTION_PIPELINE_SECTION_IDS = DETECTION_PIPELINE_SECTION_IDS;
27311
- exports.customActions = ncActions;
27614
+ exports.customActions = customActions;
27312
27615
  exports.default = PipelineAnalyticsAddon;
27313
27616
  exports.ncActions = ncActions;
27314
27617
  exports.pickCleanMedia = pickCleanMedia;