@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.
@@ -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-CrCaHNC2.mjs";
1
+ import { $ as string, 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 boolean, K as _void, L as zoneAnalyticsCapability, M as pipelineAnalyticsCapability, N as plateGalleryCapability, O as faceGalleryCapability, P as readDeviceStateFrom, Q as record, R as errMsg, S as buildEventKindDescriptor, T as defineCustomActions, U as isDeviceScopedCap, V as createEvent, W as nodePin, 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 BaseAddon } from "../dist-Bd7HioQT.mjs";
2
2
  import { promises } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
@@ -5815,6 +5815,66 @@ function makeNcActionHandlers(deps) {
5815
5815
  };
5816
5816
  }
5817
5817
  //#endregion
5818
+ //#region src/pipeline-analytics/embedding-actions.ts
5819
+ /**
5820
+ * embedding-actions — operator buttons for the CLIP semantic-search index.
5821
+ *
5822
+ * These ride the generic `addons.custom` bridge rather than adding cap methods,
5823
+ * because the settings form already knows how to render an
5824
+ * `addon-action-button` and call it. The three operations they expose are the
5825
+ * ones an operator needs when the index and the world disagree:
5826
+ *
5827
+ * - **Rebuild** re-embeds tracks from their key frames. It is the only way to
5828
+ * change what a vector MEANS — crop margin, squaring, encoder model — since
5829
+ * mixing feature spaces in one index makes cosine scores incomparable.
5830
+ * - **Status** is what makes rebuild usable at all: a pass is ~1s per track,
5831
+ * 45 minutes for a fleet, so it runs detached and this is how progress is
5832
+ * seen.
5833
+ * - **Wipe** empties the index, for the clean slate before a policy change.
5834
+ *
5835
+ * Wipe is `danger` and carries a confirm prompt. It has already destroyed a
5836
+ * live index once when it was called casually — the button must not make that
5837
+ * easier than it needs to be.
5838
+ */
5839
+ var RebuildInputSchema = object({
5840
+ /** Restrict to one camera; omit for the whole fleet. */
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
+ /** Stop after this many tracks; the status reports whether more remain. */
5851
+ maxTracks: number().int().positive().optional()
5852
+ });
5853
+ var RebuildStartedSchema = object({
5854
+ started: boolean(),
5855
+ alreadyRunning: boolean()
5856
+ });
5857
+ var RebuildStatusSchema = object({
5858
+ running: boolean(),
5859
+ scanned: number(),
5860
+ rebuilt: number(),
5861
+ missingKeyFrame: number(),
5862
+ missingBbox: number(),
5863
+ failed: number(),
5864
+ complete: boolean().nullable(),
5865
+ startedAtMs: number().nullable(),
5866
+ finishedAtMs: number().nullable(),
5867
+ error: string().nullable(),
5868
+ /** Rows currently in the index — the number the operator is watching. */
5869
+ indexed: number()
5870
+ });
5871
+ var WipeResultSchema = object({ deleted: number() });
5872
+ var embeddingActions = defineCustomActions({
5873
+ "embedding.rebuild": customAction(RebuildInputSchema, RebuildStartedSchema, { auth: "admin" }),
5874
+ "embedding.rebuildStatus": customAction(_void(), RebuildStatusSchema),
5875
+ "embedding.wipe": customAction(_void(), WipeResultSchema, { auth: "admin" })
5876
+ });
5877
+ //#endregion
5818
5878
  //#region src/pipeline-analytics/pipeline/face-clustering.ts
5819
5879
  /**
5820
5880
  * face-clustering — pure greedy clustering of face embeddings by cosine
@@ -12041,57 +12101,84 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12041
12101
  const margin = options.cropMargin ?? .2;
12042
12102
  const square = options.square ?? true;
12043
12103
  const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
12044
- const trackIds = await deps.listTrackIds();
12045
- const considered = trackIds.slice(0, Number.isFinite(maxTracks) ? maxTracks : void 0);
12046
12104
  let rebuilt = 0;
12047
12105
  let missingKeyFrame = 0;
12048
12106
  let missingBbox = 0;
12049
12107
  let failed = 0;
12050
- for (const trackId of considered) try {
12051
- const source = await deps.loadSource(trackId);
12052
- if (source === null) {
12053
- missingKeyFrame += 1;
12054
- continue;
12055
- }
12056
- if (source.keyFrame === null) {
12057
- missingKeyFrame += 1;
12058
- continue;
12059
- }
12060
- if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
12061
- missingBbox += 1;
12062
- continue;
12063
- }
12064
- const meta = await sharp(source.keyFrame).metadata();
12065
- if (meta.width === void 0 || meta.height === void 0) {
12066
- failed += 1;
12067
- continue;
12108
+ let scanned = 0;
12109
+ let cursor = null;
12110
+ let exhausted = false;
12111
+ let budgetExhausted = false;
12112
+ while (scanned < maxTracks) {
12113
+ const page = await deps.listTrackIdPage(cursor);
12114
+ if (page.trackIds.length === 0) {
12115
+ exhausted = true;
12116
+ break;
12068
12117
  }
12069
- const window = cropWindow(source.bbox, meta.width, meta.height, {
12070
- margin,
12071
- square
12072
- });
12073
- const cut = await cropFromJpeg(source.keyFrame, window);
12074
- if (cut === null) {
12075
- failed += 1;
12076
- continue;
12118
+ const room = maxTracks - scanned;
12119
+ const batch = page.trackIds.slice(0, Number.isFinite(room) ? room : void 0);
12120
+ if (batch.length < page.trackIds.length) budgetExhausted = true;
12121
+ for (const trackId of batch) {
12122
+ scanned += 1;
12123
+ try {
12124
+ const source = await deps.loadSource(trackId);
12125
+ if (source === null) {
12126
+ missingKeyFrame += 1;
12127
+ continue;
12128
+ }
12129
+ if (source.keyFrame === null) {
12130
+ missingKeyFrame += 1;
12131
+ continue;
12132
+ }
12133
+ if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
12134
+ missingBbox += 1;
12135
+ continue;
12136
+ }
12137
+ const meta = await sharp(source.keyFrame).metadata();
12138
+ if (meta.width === void 0 || meta.height === void 0) {
12139
+ failed += 1;
12140
+ continue;
12141
+ }
12142
+ const window = cropWindow(source.bbox, meta.width, meta.height, {
12143
+ margin,
12144
+ square
12145
+ });
12146
+ const cut = await cropFromJpeg(source.keyFrame, window);
12147
+ if (cut === null) {
12148
+ failed += 1;
12149
+ continue;
12150
+ }
12151
+ const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12152
+ if (embedding.length === 0) {
12153
+ failed += 1;
12154
+ continue;
12155
+ }
12156
+ await deps.store(source, embedding);
12157
+ rebuilt += 1;
12158
+ } catch (err) {
12159
+ failed += 1;
12160
+ deps.logger.warn("embedding rebuild failed for a track", { meta: {
12161
+ trackId,
12162
+ error: String(err)
12163
+ } });
12164
+ }
12165
+ if (scanned % 25 === 0) options.onProgress?.({
12166
+ scanned,
12167
+ rebuilt,
12168
+ missingKeyFrame,
12169
+ missingBbox,
12170
+ failed
12171
+ });
12077
12172
  }
12078
- const embedding = await deps.encode(cut.crop, cut.width, cut.height);
12079
- if (embedding.length === 0) {
12080
- failed += 1;
12081
- continue;
12173
+ if (page.nextCursor === null) {
12174
+ exhausted = true;
12175
+ break;
12082
12176
  }
12083
- await deps.store(source, embedding);
12084
- rebuilt += 1;
12085
- } catch (err) {
12086
- failed += 1;
12087
- deps.logger.warn("embedding rebuild failed for a track", { meta: {
12088
- trackId,
12089
- error: String(err)
12090
- } });
12177
+ cursor = page.nextCursor;
12091
12178
  }
12092
- const complete = considered.length === trackIds.length;
12179
+ const complete = exhausted && !budgetExhausted;
12093
12180
  deps.logger.info("object embedding rebuild finished", { meta: {
12094
- scanned: considered.length,
12181
+ scanned,
12095
12182
  rebuilt,
12096
12183
  missingKeyFrame,
12097
12184
  missingBbox,
@@ -12101,7 +12188,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
12101
12188
  square
12102
12189
  } });
12103
12190
  return {
12104
- scanned: considered.length,
12191
+ scanned,
12105
12192
  rebuilt,
12106
12193
  missingKeyFrame,
12107
12194
  missingBbox,
@@ -17524,26 +17611,6 @@ var SyntheticSensorTrackMaterializer = class {
17524
17611
  return track;
17525
17612
  }
17526
17613
  };
17527
- var TRACK_RETENTION_DEFAULT_DAYS = object({ trackRetentionDays: number().min(0).default(7) }).parse({}).trackRetentionDays;
17528
- /**
17529
- * Sweep every device with persisted tracks: skip retention-disabled devices,
17530
- * prune the rest at `now − retentionMs`. Per-device ISOLATED — one bad device
17531
- * never aborts the sweep. Returns the total tracks pruned across devices.
17532
- */
17533
- async function sweepTrackRetention(deps) {
17534
- const devices = await deps.listDeviceIds();
17535
- const nowMs = deps.now();
17536
- let totalTracks = 0;
17537
- for (const deviceId of devices) try {
17538
- const cutoffMs = await deps.resolveCutoffMs(deviceId, nowMs);
17539
- if (cutoffMs === null) continue;
17540
- const counts = await deps.pruneTracksBefore(deviceId, cutoffMs);
17541
- totalTracks += counts.tracks;
17542
- } catch (err) {
17543
- deps.onError?.(deviceId, err);
17544
- }
17545
- return totalTracks;
17546
- }
17547
17614
  var TrackingSettingsSchema = object({
17548
17615
  /**
17549
17616
  * How much of a detection's box must lie inside a zone (0-1 fraction of the
@@ -18339,6 +18406,26 @@ function buildDetectionSettingsSections() {
18339
18406
  }
18340
18407
  ];
18341
18408
  }
18409
+ var TRACK_RETENTION_DEFAULT_DAYS = object({ trackRetentionDays: number().min(0).default(7) }).parse({}).trackRetentionDays;
18410
+ /**
18411
+ * Sweep every device with persisted tracks: skip retention-disabled devices,
18412
+ * prune the rest at `now − retentionMs`. Per-device ISOLATED — one bad device
18413
+ * never aborts the sweep. Returns the total tracks pruned across devices.
18414
+ */
18415
+ async function sweepTrackRetention(deps) {
18416
+ const devices = await deps.listDeviceIds();
18417
+ const nowMs = deps.now();
18418
+ let totalTracks = 0;
18419
+ for (const deviceId of devices) try {
18420
+ const cutoffMs = await deps.resolveCutoffMs(deviceId, nowMs);
18421
+ if (cutoffMs === null) continue;
18422
+ const counts = await deps.pruneTracksBefore(deviceId, cutoffMs);
18423
+ totalTracks += counts.tracks;
18424
+ } catch (err) {
18425
+ deps.onError?.(deviceId, err);
18426
+ }
18427
+ return totalTracks;
18428
+ }
18342
18429
  /**
18343
18430
  * Re-home the global analytics sections into the per-device `Analytics`
18344
18431
  * top-tab. Every section defaults to the `Analytics` tab (so the
@@ -18449,6 +18536,59 @@ function buildGlobalSettingsSchema() {
18449
18536
  default: true
18450
18537
  }]
18451
18538
  },
18539
+ {
18540
+ id: "embedding-index",
18541
+ title: "Semantic search index",
18542
+ 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.",
18543
+ columns: 1,
18544
+ fields: [
18545
+ {
18546
+ type: "number",
18547
+ key: "rebuildCropMargin",
18548
+ label: "Rebuild crop margin",
18549
+ 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.",
18550
+ default: .2,
18551
+ min: 0,
18552
+ max: 2,
18553
+ step: .1
18554
+ },
18555
+ {
18556
+ type: "addon-action-button",
18557
+ key: "rebuildEmbeddingsButton",
18558
+ label: "Rebuild index",
18559
+ 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.",
18560
+ addonId: "pipeline-analytics",
18561
+ action: "embedding.rebuild",
18562
+ buttonLabel: "Rebuild",
18563
+ variant: "primary",
18564
+ paramsFromForm: { cropMargin: "rebuildCropMargin" },
18565
+ paramsStatic: { square: true },
18566
+ successMessage: "Rebuild started — press Progress to follow it."
18567
+ },
18568
+ {
18569
+ type: "addon-action-button",
18570
+ key: "rebuildStatusButton",
18571
+ label: "Progress",
18572
+ description: "Rows currently indexed, plus the running pass: scanned, rebuilt, and what it had to skip.",
18573
+ addonId: "pipeline-analytics",
18574
+ action: "embedding.rebuildStatus",
18575
+ buttonLabel: "Check progress",
18576
+ successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
18577
+ },
18578
+ {
18579
+ type: "addon-action-button",
18580
+ key: "wipeEmbeddingsButton",
18581
+ label: "Empty the index",
18582
+ 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.",
18583
+ addonId: "pipeline-analytics",
18584
+ action: "embedding.wipe",
18585
+ buttonLabel: "Empty index",
18586
+ variant: "danger",
18587
+ confirmMessage: "Delete every semantic-search vector? Search stops working until a rebuild finishes, and vectors whose tracks have aged out cannot be recovered.",
18588
+ successMessage: "Removed {deleted} vectors."
18589
+ }
18590
+ ]
18591
+ },
18452
18592
  {
18453
18593
  id: "tracking-core",
18454
18594
  title: "Tracking",
@@ -22933,10 +23073,40 @@ function buildStationarySnapshot(input) {
22933
23073
  * effect without a restart and long enough that no search pays for it twice.
22934
23074
  */
22935
23075
  var ENCODER_INFO_TTL_MS = 6e4;
23076
+ /**
23077
+ * Rows the orphan sweep considers per pass, on both sides of the comparison.
23078
+ *
23079
+ * Bounded so a retention tick cannot turn into a full-index scan. A pass that
23080
+ * does not reach everything simply leaves the rest for the next tick — orphans
23081
+ * are not urgent, they are just dead weight the search has to skip.
23082
+ */
23083
+ var EMBEDDING_ORPHAN_SCAN_LIMIT = 2e4;
23084
+ /** Dimension of the zero probe vector used to enumerate the index. */
23085
+ var EMBEDDING_ORPHAN_PROBE_DIM = 512;
23086
+ /**
23087
+ * Tracks fetched per page while rebuilding.
23088
+ *
23089
+ * The page size is NOT the caller's `maxTracks`: that bounds the whole pass,
23090
+ * this bounds one round trip. Conflating them is what made the rebuild
23091
+ * re-embed the same newest N tracks on every call.
23092
+ */
23093
+ var REBUILD_PAGE_SIZE = 200;
22936
23094
  var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
22937
23095
  /** Operator-visible name of the fixed integration. */
22938
23096
  var NC_INTEGRATION_NAME = "Notification Center";
22939
23097
  /**
23098
+ * The FULL action catalog for this addon.
23099
+ *
23100
+ * Must contain every action, not just the notification-center ones: the hub
23101
+ * harvests THIS named export, so an action registered only in what
23102
+ * `onInitialize` returns is invisible to the bridge and answers 404. That is
23103
+ * exactly what the embedding buttons did on their first deploy.
23104
+ */
23105
+ var customActions = {
23106
+ ...ncActions,
23107
+ ...embeddingActions
23108
+ };
23109
+ /**
22940
23110
  * Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
22941
23111
  * `PipelineAudioInferenceResult` on the bus, refines raw detections
22942
23112
  * into tracks + per-kind events + persisted media, and emits canonical
@@ -23120,6 +23290,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
23120
23290
  plateStore = null;
23121
23291
  plateRecognizer = null;
23122
23292
  objectEmbeddingStore = null;
23293
+ /** Live state of the detached embedding rebuild — see `rebuildObjectEmbeddings`. */
23294
+ rebuildState = { running: false };
23123
23295
  /** Cached active-encoder model id — see `resolveEncoderModelId`. */
23124
23296
  encoderInfoCache = null;
23125
23297
  /** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
@@ -23594,8 +23766,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
23594
23766
  return {
23595
23767
  providers,
23596
23768
  ...isHub && ncHandlers !== void 0 ? {
23597
- customActions: ncActions,
23598
- actionHandlers: ncHandlers
23769
+ customActions: {
23770
+ ...ncActions,
23771
+ ...embeddingActions
23772
+ },
23773
+ actionHandlers: {
23774
+ ...ncHandlers,
23775
+ ...this.buildEmbeddingActionHandlers()
23776
+ }
23599
23777
  } : {}
23600
23778
  };
23601
23779
  }
@@ -26222,6 +26400,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26222
26400
  }
26223
26401
  }
26224
26402
  if (deletedEmbeddings > 0) this.ctx.logger.info("object embedding retention prune", { meta: { deleted: deletedEmbeddings } });
26403
+ try {
26404
+ const live = await this.collectLiveTrackIds(deviceIds);
26405
+ const stored = await embeddingStore.searchByVector({
26406
+ vector: new Float32Array(EMBEDDING_ORPHAN_PROBE_DIM),
26407
+ limit: EMBEDDING_ORPHAN_SCAN_LIMIT
26408
+ });
26409
+ const orphanResult = await embeddingStore.pruneOrphans({
26410
+ liveTrackIds: live,
26411
+ candidates: stored.hits.map((h) => h.row.trackId)
26412
+ });
26413
+ if (orphanResult.deleted > 0) this.ctx.logger.info("object embedding orphan sweep", { meta: {
26414
+ deleted: orphanResult.deleted,
26415
+ scanned: stored.hits.length
26416
+ } });
26417
+ } catch (err) {
26418
+ this.ctx.logger.debug("object embedding orphan sweep failed", { meta: { error: String(err) } });
26419
+ }
26225
26420
  }
26226
26421
  } catch (err) {
26227
26422
  this.ctx.logger.debug("sweepRetention failed", { meta: { error: String(err) } });
@@ -26791,6 +26986,25 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26791
26986
  return info.modelId;
26792
26987
  }
26793
26988
  /**
26989
+ * Every track id the track store still holds, across the given devices.
26990
+ *
26991
+ * Returned as a Set for the orphan sweep, which REFUSES an empty one — a
26992
+ * store that failed to answer must not be read as "no tracks exist".
26993
+ */
26994
+ async collectLiveTrackIds(deviceIds) {
26995
+ const live = /* @__PURE__ */ new Set();
26996
+ const trackStore = this.trackStore;
26997
+ if (!trackStore) return live;
26998
+ for (const deviceId of deviceIds) {
26999
+ const page = await trackStore.queryRecent({
27000
+ deviceIds: [deviceId],
27001
+ limit: EMBEDDING_ORPHAN_SCAN_LIMIT
27002
+ });
27003
+ for (const track of page.tracks) live.add(track.trackId);
27004
+ }
27005
+ return live;
27006
+ }
27007
+ /**
26794
27008
  * Empty the CLIP index — the clean slate before a crop-policy or encoder
26795
27009
  * change, to be followed by {@link rebuildObjectEmbeddings}.
26796
27010
  */
@@ -26812,33 +27026,40 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26812
27026
  * widened after the fact.
26813
27027
  */
26814
27028
  async rebuildObjectEmbeddings(input) {
27029
+ if (this.rebuildState.running) {
27030
+ this.ctx.logger.info("object embedding rebuild already running — request ignored");
27031
+ return {
27032
+ started: false,
27033
+ alreadyRunning: true
27034
+ };
27035
+ }
26815
27036
  const store = this.objectEmbeddingStore;
26816
27037
  const trackStore = this.trackStore;
26817
27038
  const mediaStore = this.mediaStore;
26818
27039
  const eventStore = this.eventStore;
26819
27040
  const api = this.ctx.api;
26820
- const empty = {
26821
- scanned: 0,
26822
- rebuilt: 0,
26823
- missingKeyFrame: 0,
26824
- missingBbox: 0,
26825
- failed: 0,
26826
- complete: true
26827
- };
26828
27041
  if (!store || !trackStore || !mediaStore || !eventStore || !api) {
26829
27042
  this.ctx.logger.warn("rebuildObjectEmbeddings: analytics stores unavailable");
26830
- return empty;
27043
+ return {
27044
+ started: false,
27045
+ alreadyRunning: false
27046
+ };
26831
27047
  }
26832
27048
  const modelId = await this.resolveEncoderModelId(api) ?? "unknown";
26833
27049
  const deviceIds = input.deviceId !== void 0 ? [input.deviceId] : await trackStore.listDeviceIds();
26834
- return rebuildObjectEmbeddings({
26835
- listTrackIds: async () => {
26836
- return (await trackStore.queryRecent({
27050
+ const deps = {
27051
+ listTrackIdPage: async (cursor) => {
27052
+ const page = await trackStore.queryRecent({
26837
27053
  deviceIds,
26838
27054
  ...input.since !== void 0 ? { since: input.since } : {},
26839
27055
  ...input.until !== void 0 ? { until: input.until } : {},
26840
- limit: input.maxTracks ?? 500
26841
- })).tracks.map((t) => t.trackId);
27056
+ ...cursor !== null ? { cursor } : {},
27057
+ limit: REBUILD_PAGE_SIZE
27058
+ });
27059
+ return {
27060
+ trackIds: page.tracks.map((t) => t.trackId),
27061
+ nextCursor: page.nextCursor
27062
+ };
26842
27063
  },
26843
27064
  loadSource: async (trackId) => {
26844
27065
  const media = await mediaStore.listByOwner("track", trackId);
@@ -26884,11 +27105,93 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26884
27105
  info: (message, extras) => this.ctx.logger.info(message, extras),
26885
27106
  warn: (message, extras) => this.ctx.logger.warn(message, extras)
26886
27107
  }
26887
- }, {
27108
+ };
27109
+ this.rebuildState = {
27110
+ running: true,
27111
+ startedAtMs: Date.now(),
27112
+ progress: {
27113
+ scanned: 0,
27114
+ rebuilt: 0,
27115
+ missingKeyFrame: 0,
27116
+ missingBbox: 0,
27117
+ failed: 0
27118
+ }
27119
+ };
27120
+ rebuildObjectEmbeddings(deps, {
26888
27121
  ...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
26889
27122
  ...input.square !== void 0 ? { square: input.square } : {},
26890
- ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {}
27123
+ ...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
27124
+ onProgress: (progress) => {
27125
+ if (this.rebuildState.running) this.rebuildState.progress = progress;
27126
+ this.ctx.logger.info("object embedding rebuild progress", { meta: { ...progress } });
27127
+ }
27128
+ }).then((outcome) => {
27129
+ this.rebuildState = {
27130
+ running: false,
27131
+ startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
27132
+ finishedAtMs: Date.now(),
27133
+ progress: outcome,
27134
+ complete: outcome.complete
27135
+ };
27136
+ }).catch((err) => {
27137
+ this.rebuildState = {
27138
+ running: false,
27139
+ startedAtMs: this.rebuildState.startedAtMs ?? Date.now(),
27140
+ finishedAtMs: Date.now(),
27141
+ error: String(err)
27142
+ };
27143
+ this.ctx.logger.warn("object embedding rebuild failed", { meta: { error: String(err) } });
26891
27144
  });
27145
+ this.ctx.logger.info("object embedding rebuild started", { meta: {
27146
+ deviceId: input.deviceId ?? "all",
27147
+ cropMargin: input.cropMargin ?? "default",
27148
+ maxTracks: input.maxTracks ?? "unbounded"
27149
+ } });
27150
+ return {
27151
+ started: true,
27152
+ alreadyRunning: false
27153
+ };
27154
+ }
27155
+ /**
27156
+ * Handlers behind the embedding buttons in the settings form.
27157
+ *
27158
+ * They are thin wrappers over the cap methods on purpose: the button and the
27159
+ * API must not be able to drift into two behaviours.
27160
+ */
27161
+ buildEmbeddingActionHandlers() {
27162
+ return {
27163
+ "embedding.rebuild": async (input) => this.rebuildObjectEmbeddings(input),
27164
+ "embedding.rebuildStatus": async () => {
27165
+ const status = await this.getObjectEmbeddingRebuildStatus();
27166
+ let indexed = 0;
27167
+ try {
27168
+ const api = this.ctx.api;
27169
+ if (api) indexed = (await api.vectorStore.stats.query({ index: OBJECT_CLIP_INDEX })).count;
27170
+ } catch {}
27171
+ return {
27172
+ ...status,
27173
+ indexed
27174
+ };
27175
+ },
27176
+ "embedding.wipe": async () => this.wipeObjectEmbeddings()
27177
+ };
27178
+ }
27179
+ /** Progress of the detached rebuild — the only way to observe a long pass. */
27180
+ async getObjectEmbeddingRebuildStatus() {
27181
+ const state = this.rebuildState;
27182
+ const p = state.progress;
27183
+ return {
27184
+ running: state.running,
27185
+ scanned: p?.scanned ?? 0,
27186
+ rebuilt: p?.rebuilt ?? 0,
27187
+ missingKeyFrame: p?.missingKeyFrame ?? 0,
27188
+ missingBbox: p?.missingBbox ?? 0,
27189
+ failed: p?.failed ?? 0,
27190
+ complete: state.complete ?? null,
27191
+ startedAtMs: state.startedAtMs ?? null,
27192
+ finishedAtMs: state.finishedAtMs ?? null,
27193
+ error: state.error ?? null
27194
+ };
26892
27195
  }
26893
27196
  async searchObjectEvents(input) {
26894
27197
  const store = this.objectEmbeddingStore;
@@ -27301,4 +27604,4 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
27301
27604
  }
27302
27605
  };
27303
27606
  //#endregion
27304
- export { DETECTION_PIPELINE_SECTION_IDS, ncActions as customActions, ncActions, PipelineAnalyticsAddon as default, pickCleanMedia, retagDetectionSections, retagRetentionSection, stripGlobalOnlyFields, toAnalyticsDeviceSections };
27607
+ export { DETECTION_PIPELINE_SECTION_IDS, customActions, PipelineAnalyticsAddon as default, ncActions, pickCleanMedia, retagDetectionSections, retagRetentionSection, stripGlobalOnlyFields, toAnalyticsDeviceSections };
@@ -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-Dt_25eeS.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DX4bQCYL.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.40",
3
+ "version": "1.2.42",
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",