@camstack/addon-post-analysis 1.2.36 → 1.2.37

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-B-2HqwZV.mjs";
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-BdBTDQxw.mjs";
2
2
  import { promises } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
@@ -14024,41 +14024,6 @@ function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSki
14024
14024
  return consecutiveSkips < cfg.maxSkipFrames;
14025
14025
  }
14026
14026
  //#endregion
14027
- //#region src/pipeline-analytics/pipeline/embedding-search.ts
14028
- /**
14029
- * embedding-search — CLIP text→embedding cosine ranking helper.
14030
- *
14031
- * `rankByText` mirrors `face-matcher.matchEmbedding` with the identity-grouping
14032
- * removed: it compares a single query vector against all rows, enforces the
14033
- * same-modelId/dim gate, and returns the top-N rows above `minScore` sorted
14034
- * descending by cosine similarity.
14035
- */
14036
- /**
14037
- * Brute-force cosine rank of `rows` against `queryVec`.
14038
- *
14039
- * Gate: same-modelId (when `opts.modelId` is set) AND same dimension as
14040
- * `queryVec.length`. Rows that fail either gate are skipped.
14041
- * Returns at most `opts.limit` rows with `score >= opts.minScore`,
14042
- * ordered descending by score. Immutable — does not modify inputs.
14043
- */
14044
- function rankByText(queryVec, rows, opts) {
14045
- const queryDim = queryVec.length;
14046
- const scored = [];
14047
- for (const row of rows) {
14048
- if (opts.modelId !== void 0 && row.modelId !== opts.modelId) continue;
14049
- if (row.dim !== queryDim) continue;
14050
- if (row.embedding.length !== queryDim) continue;
14051
- const score = cosineSimilarity(queryVec, new Float32Array(row.embedding));
14052
- if (score < opts.minScore) continue;
14053
- scored.push({
14054
- row,
14055
- score
14056
- });
14057
- }
14058
- scored.sort((a, b) => b.score - a.score);
14059
- return scored.slice(0, opts.limit);
14060
- }
14061
- //#endregion
14062
14027
  //#region src/pipeline-analytics/pipeline/event-child-crops.ts
14063
14028
  /**
14064
14029
  * Compute Intersection-over-Union between two (x,y,w,h) bounding boxes.
@@ -20285,50 +20250,35 @@ var IdentityStore = class {
20285
20250
  }
20286
20251
  };
20287
20252
  //#endregion
20288
- //#region src/pipeline-analytics/store/object-embedding-mirror.ts
20253
+ //#region src/pipeline-analytics/store/object-embedding-store.ts
20289
20254
  /**
20290
- * Mirrors per-track object embeddings into the `vector-store` capability.
20291
- *
20292
- * ## Why a mirror and not a swap
20255
+ * ObjectEmbeddingStore — per-track CLIP image embeddings, in the `vector-store`
20256
+ * capability.
20293
20257
  *
20294
- * The read path (`searchObjectEvents`) is live and, since the whereIn join fix,
20295
- * fast enough to use. Flipping it onto a vector index that has not been
20296
- * backfilled would return an empty search on a hub with 8,455 embeddings — a
20297
- * regression an operator would notice before any benefit landed.
20258
+ * ## What this used to be
20298
20259
  *
20299
- * So this writes to BOTH: the existing settings-store collection stays
20300
- * authoritative and the vector index is populated alongside it. Once
20301
- * `vectorStore.stats` reports a count matching the legacy collection on the
20302
- * live hub, the read path can move over in one commit with a real before/after
20303
- * measurement, and this mirror becomes the only writer.
20260
+ * Until 2026-08-06 this owned a `pipeline-analytics:object-embeddings` settings
20261
+ * collection whose `embedding` column was a JSON array of 512 floats — measured
20262
+ * at ~11.7 KB per row on the live hub and `JSON.parse`d 5,000 times on every
20263
+ * semantic search. That collection is gone. The vector store is now the only
20264
+ * home for these vectors: base64 Float32LE (2,732 bytes for the same 512 dims),
20265
+ * with `deviceId` / `timestamp` / `className` / `modelId` promoted to indexed
20266
+ * columns so prefilters are pushed into SQL rather than applied after loading.
20304
20267
  *
20305
- * That ordering is deliberate and is written down in `docs/roadmap.md` — the
20306
- * failure this repo keeps repeating is a mechanism that looks finished and is
20307
- * wired to nothing, and a half-migrated store is the same shape.
20268
+ * The dual-write mirror and the boot backfill that carried the transition were
20269
+ * deleted along with the collection: the index converged (8,467 = 8,467, delta
20270
+ * 0) and a migration aid left running is just a second writer nobody reads.
20308
20271
  *
20309
- * ## The index
20272
+ * ## What did NOT change
20310
20273
  *
20311
- * One index, `object-clip`, dim taken from the first vector seen. Metadata
20312
- * carries `deviceId` / `timestamp` / `className` / `modelId` (promoted to real
20313
- * columns by the provider, so prefilters are pushed into SQL) plus `trackId`,
20314
- * `confidence` and the two media keys, which ride in the JSON remainder.
20274
+ * One row per track, keyed by `trackId`, holding the track's best-confidence
20275
+ * detection. The confidence gate lives here rather than in the caller because
20276
+ * the in-memory `TrackBestSelector` resets on restart and this is the durable
20277
+ * half — see `best-selection-policies.ts`.
20315
20278
  */
20316
- /** The one index this mirror owns. */
20279
+ /** The vector-store index this store owns. */
20317
20280
  var OBJECT_CLIP_INDEX = "object-clip";
20318
- /** Build the metadata record for one embedding. Exported for tests. */
20319
- function metadataFor(row) {
20320
- return {
20321
- deviceId: row.deviceId,
20322
- timestamp: row.timestamp,
20323
- className: row.className,
20324
- modelId: row.modelId,
20325
- trackId: row.trackId,
20326
- confidence: row.confidence,
20327
- ...row.mediaKey !== void 0 ? { mediaKey: row.mediaKey } : {},
20328
- ...row.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: row.keyFrameMediaKey } : {}
20329
- };
20330
- }
20331
- var ObjectEmbeddingMirror = class {
20281
+ var ObjectEmbeddingStore = class {
20332
20282
  vectorStore;
20333
20283
  logger;
20334
20284
  declaredDim = null;
@@ -20337,11 +20287,13 @@ var ObjectEmbeddingMirror = class {
20337
20287
  this.logger = deps.logger;
20338
20288
  }
20339
20289
  /**
20340
- * Declare the index lazily, on the first vector, because the dimension comes
20341
- * from the active encoder and is not known at construction time.
20290
+ * Declare the index lazily, on the first vector — its dimension comes from
20291
+ * the active encoder and is not known at construction.
20342
20292
  *
20343
- * Returns false when the index could not be declared — the caller must then
20344
- * skip the write rather than upsert into an index that does not exist.
20293
+ * Returns false when declaration failed, and the caller must then skip the
20294
+ * write: the provider rejects an upsert into an index it has never seen. A
20295
+ * dim change against a populated index is REFUSED rather than wiping it, so
20296
+ * this failing is the signal that the encoder model changed.
20345
20297
  */
20346
20298
  async ensureIndex(dim) {
20347
20299
  if (this.declaredDim === dim) return true;
@@ -20354,7 +20306,7 @@ var ObjectEmbeddingMirror = class {
20354
20306
  this.declaredDim = dim;
20355
20307
  return true;
20356
20308
  } catch (err) {
20357
- this.logger.warn("vector-store index declaration failed — mirror disabled for this dim", { meta: {
20309
+ this.logger.warn("object-embedding index declaration failed — writes disabled for this dim", { meta: {
20358
20310
  index: OBJECT_CLIP_INDEX,
20359
20311
  dim,
20360
20312
  error: String(err)
@@ -20362,232 +20314,34 @@ var ObjectEmbeddingMirror = class {
20362
20314
  return false;
20363
20315
  }
20364
20316
  }
20365
- /** Mirror one embedding. Never throws — the legacy write is authoritative. */
20366
- async mirror(row) {
20367
- if (row.embedding.length === 0) return;
20368
- if (!await this.ensureIndex(row.embedding.length)) return;
20317
+ /**
20318
+ * Upsert the best-confidence embedding for a track. At most ONE row per
20319
+ * track: a write only replaces the stored row when its confidence is
20320
+ * strictly higher, keeping the track's best crop over its lifetime.
20321
+ */
20322
+ async upsertIfBetter(input) {
20323
+ if (input.embedding.length === 0) return;
20324
+ if (!await this.ensureIndex(input.embedding.length)) return;
20369
20325
  try {
20326
+ const stored = (await this.vectorStore.getByIds.query({
20327
+ index: OBJECT_CLIP_INDEX,
20328
+ ids: [input.trackId]
20329
+ })).items[0]?.metadata["confidence"];
20330
+ if (typeof stored === "number" && input.confidence <= stored) return;
20370
20331
  if ((await this.vectorStore.upsert.mutate({
20371
20332
  index: "object-clip",
20372
20333
  items: [{
20373
- id: row.trackId,
20374
- vector: encodeVectorBase64(row.embedding),
20375
- metadata: metadataFor(row)
20334
+ id: input.trackId,
20335
+ vector: encodeVectorBase64(input.embedding),
20336
+ metadata: metadataFor(input)
20376
20337
  }]
20377
- })).rejected > 0) this.logger.warn("vector-store rejected a mirrored embedding", {
20378
- tags: { deviceId: row.deviceId },
20379
- meta: {
20380
- trackId: row.trackId,
20381
- dim: row.embedding.length
20382
- }
20383
- });
20384
- } catch (err) {
20385
- this.logger.warn("vector-store mirror failed", {
20386
- tags: { deviceId: row.deviceId },
20338
+ })).rejected > 0) this.logger.warn("object-embedding upsert rejected", {
20339
+ tags: { deviceId: input.deviceId },
20387
20340
  meta: {
20388
- trackId: row.trackId,
20389
- error: String(err)
20341
+ trackId: input.trackId,
20342
+ dim: input.embedding.length
20390
20343
  }
20391
20344
  });
20392
- }
20393
- }
20394
- /** Mirror a page of embeddings — the backfill path. */
20395
- async mirrorMany(rows) {
20396
- const usable = rows.filter((r) => r.embedding.length > 0);
20397
- const first = usable[0];
20398
- if (first === void 0) return { upserted: 0 };
20399
- if (!await this.ensureIndex(first.embedding.length)) return { upserted: 0 };
20400
- const items = usable.filter((r) => r.embedding.length === first.embedding.length).map((r) => ({
20401
- id: r.trackId,
20402
- vector: encodeVectorBase64(r.embedding),
20403
- metadata: metadataFor(r)
20404
- }));
20405
- const skipped = usable.length - items.length;
20406
- if (skipped > 0) this.logger.warn("backfill skipped embeddings of a different dimension", { meta: {
20407
- skipped,
20408
- expectedDim: first.embedding.length
20409
- } });
20410
- try {
20411
- return { upserted: (await this.vectorStore.upsert.mutate({
20412
- index: OBJECT_CLIP_INDEX,
20413
- items
20414
- })).upserted };
20415
- } catch (err) {
20416
- this.logger.warn("vector-store backfill page failed", { meta: {
20417
- count: items.length,
20418
- error: String(err)
20419
- } });
20420
- return { upserted: 0 };
20421
- }
20422
- }
20423
- /** Keep the index in step with the legacy cascade delete. */
20424
- async remove(trackIds) {
20425
- if (trackIds.length === 0) return;
20426
- try {
20427
- await this.vectorStore.deleteByIds.mutate({
20428
- index: OBJECT_CLIP_INDEX,
20429
- ids: [...trackIds]
20430
- });
20431
- } catch (err) {
20432
- this.logger.warn("vector-store mirror delete failed", { meta: {
20433
- count: trackIds.length,
20434
- error: String(err)
20435
- } });
20436
- }
20437
- }
20438
- /**
20439
- * How far the mirror has got, for the decision to flip the read path.
20440
- * Returns null when the index is not reachable or not yet declared.
20441
- */
20442
- async count() {
20443
- try {
20444
- return (await this.vectorStore.stats.query({ index: OBJECT_CLIP_INDEX })).count;
20445
- } catch {
20446
- return null;
20447
- }
20448
- }
20449
- };
20450
- //#endregion
20451
- //#region src/pipeline-analytics/store/object-embedding-store.ts
20452
- var OBJECT_EMBEDDINGS_COLLECTION = "pipeline-analytics:object-embeddings";
20453
- var OBJECT_EMBEDDING_COLUMNS = [
20454
- {
20455
- name: "id",
20456
- type: "TEXT",
20457
- primaryKey: true,
20458
- notNull: true
20459
- },
20460
- {
20461
- name: "deviceId",
20462
- type: "INTEGER",
20463
- notNull: true
20464
- },
20465
- {
20466
- name: "trackId",
20467
- type: "TEXT",
20468
- notNull: true
20469
- },
20470
- {
20471
- name: "timestamp",
20472
- type: "INTEGER",
20473
- notNull: true
20474
- },
20475
- {
20476
- name: "className",
20477
- type: "TEXT",
20478
- notNull: true
20479
- },
20480
- {
20481
- name: "embedding",
20482
- type: "JSON",
20483
- notNull: true
20484
- },
20485
- {
20486
- name: "modelId",
20487
- type: "TEXT",
20488
- notNull: true
20489
- },
20490
- {
20491
- name: "dim",
20492
- type: "INTEGER",
20493
- notNull: true
20494
- },
20495
- {
20496
- name: "confidence",
20497
- type: "REAL",
20498
- notNull: true
20499
- },
20500
- {
20501
- name: "mediaKey",
20502
- type: "TEXT"
20503
- },
20504
- {
20505
- name: "keyFrameMediaKey",
20506
- type: "TEXT"
20507
- }
20508
- ];
20509
- /**
20510
- * Ceiling on the CANDIDATE pool a semantic search may load.
20511
- *
20512
- * `query()` was called with no limit at all, so a default search loaded the
20513
- * entire collection — 86.5 MB across 7,375 rows on a live hub, every time,
20514
- * before ranking anything.
20515
- *
20516
- * It cannot simply be the caller's result limit: search RANKS the pool and then
20517
- * takes the top N, so limiting to N up front would return N arbitrary rows
20518
- * instead of the N best. This bounds the pool well above any result count, and
20519
- * `query()` orders by `timestamp` DESC whenever a limit applies so "the pool"
20520
- * means the most recent rows rather than whatever the page order happened to
20521
- * be. The caller logs when the cap is reached — a silently truncated search
20522
- * reads as "nothing older matched".
20523
- */
20524
- var EMBEDDING_SEARCH_CANDIDATE_CAP = 5e3;
20525
- var ObjectEmbeddingStore = class {
20526
- store;
20527
- logger;
20528
- mirror;
20529
- constructor(deps) {
20530
- this.store = deps.store;
20531
- this.logger = deps.logger;
20532
- this.mirror = deps.mirror ?? null;
20533
- }
20534
- static async declare(store) {
20535
- await store.declareCollection.mutate({
20536
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20537
- columns: [...OBJECT_EMBEDDING_COLUMNS],
20538
- indexes: [{
20539
- name: "idx_obj_emb_device_ts",
20540
- columns: ["deviceId", "timestamp"]
20541
- }, {
20542
- name: "idx_obj_emb_track",
20543
- columns: ["trackId"]
20544
- }]
20545
- });
20546
- }
20547
- /**
20548
- * Upsert the best-confidence CLIP embedding for a track. The id is the
20549
- * trackId so there is at most ONE row per track. An upsert only replaces
20550
- * the row when the new confidence is strictly higher than the stored value,
20551
- * keeping the best-crop embedding for the track over its lifetime.
20552
- */
20553
- async upsertIfBetter(input) {
20554
- const existing = await this.store.get.query({
20555
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20556
- key: input.trackId
20557
- });
20558
- if (existing !== void 0 && existing !== null) {
20559
- const storedConf = existing.confidence;
20560
- if (typeof storedConf === "number" && input.confidence <= storedConf) return;
20561
- }
20562
- const record = {
20563
- deviceId: input.deviceId,
20564
- trackId: input.trackId,
20565
- timestamp: input.timestamp,
20566
- className: input.className,
20567
- embedding: input.embedding,
20568
- modelId: input.modelId,
20569
- dim: input.embedding.length,
20570
- confidence: input.confidence,
20571
- ...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
20572
- ...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
20573
- };
20574
- try {
20575
- await this.store.set.mutate({
20576
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20577
- key: input.trackId,
20578
- value: record
20579
- });
20580
- await this.mirror?.mirror({
20581
- trackId: input.trackId,
20582
- deviceId: input.deviceId,
20583
- timestamp: input.timestamp,
20584
- className: input.className,
20585
- embedding: input.embedding,
20586
- modelId: input.modelId,
20587
- confidence: input.confidence,
20588
- ...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
20589
- ...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
20590
- });
20591
20345
  } catch (err) {
20592
20346
  this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", {
20593
20347
  tags: { deviceId: input.deviceId },
@@ -20599,93 +20353,141 @@ var ObjectEmbeddingStore = class {
20599
20353
  }
20600
20354
  }
20601
20355
  /**
20602
- * Load all embeddings for a device within a time range. Applies `modelId`
20603
- * and `className` filters when provided. Used by `searchObjectEvents`.
20604
- */
20605
- async query(input) {
20606
- const where = {};
20607
- if (input.deviceId !== void 0) where["deviceId"] = input.deviceId;
20608
- if (input.className !== void 0) where["className"] = input.className;
20609
- if (input.modelId !== void 0) where["modelId"] = input.modelId;
20610
- const filter = { where };
20611
- if (input.since !== void 0 || input.until !== void 0) filter["whereBetween"] = { timestamp: [input.since ?? 0, input.until ?? Date.now()] };
20612
- if (input.limit !== void 0) {
20613
- filter["limit"] = input.limit;
20614
- filter["orderBy"] = {
20615
- field: "timestamp",
20616
- direction: "desc"
20356
+ * Rank stored embeddings against a query vector.
20357
+ *
20358
+ * Ranking happens in the provider, over compact rows, with the prefilters
20359
+ * pushed into SQL. This used to load every candidate into this process and
20360
+ * cosine them in JS behind a 5,000-row cap.
20361
+ */
20362
+ async searchByVector(input) {
20363
+ const equals = {};
20364
+ if (input.deviceId !== void 0) equals["deviceId"] = input.deviceId;
20365
+ if (input.className !== void 0) equals["className"] = input.className;
20366
+ if (input.modelId !== void 0) equals["modelId"] = input.modelId;
20367
+ const between = {};
20368
+ if (input.since !== void 0 || input.until !== void 0) between["timestamp"] = [input.since ?? 0, input.until ?? Date.now()];
20369
+ const filter = {
20370
+ ...Object.keys(equals).length > 0 ? { equals } : {},
20371
+ ...Object.keys(between).length > 0 ? { between } : {}
20372
+ };
20373
+ try {
20374
+ const result = await this.vectorStore.query.query({
20375
+ index: OBJECT_CLIP_INDEX,
20376
+ vector: encodeVectorBase64(input.vector),
20377
+ topK: input.limit,
20378
+ ...input.minScore !== void 0 ? { minScore: input.minScore } : {},
20379
+ ...Object.keys(filter).length > 0 ? { filter } : {}
20380
+ });
20381
+ const hits = [];
20382
+ for (const match of result.matches) {
20383
+ const row = rowFromMetadata(match.id, match.metadata);
20384
+ if (row === null) continue;
20385
+ hits.push({
20386
+ row,
20387
+ score: match.score
20388
+ });
20389
+ }
20390
+ return {
20391
+ hits,
20392
+ scanned: result.scanned,
20393
+ truncated: result.truncated
20617
20394
  };
20618
- }
20619
- return (await this.store.query.query({
20620
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20621
- filter
20622
- })).map((r) => {
20623
- const data = r.data;
20395
+ } catch (err) {
20396
+ this.logger.warn("ObjectEmbeddingStore.searchByVector failed", {
20397
+ ...input.deviceId !== void 0 ? { tags: { deviceId: input.deviceId } } : {},
20398
+ meta: { error: String(err) }
20399
+ });
20624
20400
  return {
20625
- id: r.id,
20626
- ...data,
20627
- mediaKey: data.mediaKey ?? void 0,
20628
- keyFrameMediaKey: data.keyFrameMediaKey ?? void 0
20401
+ hits: [],
20402
+ scanned: 0,
20403
+ truncated: false
20629
20404
  };
20630
- });
20405
+ }
20631
20406
  }
20632
20407
  /**
20633
- * Retention sweep: delete all embeddings whose timestamp < cutoffMs across
20634
- * every device. Drains fully: loops a page at a time until no stale rows
20635
- * remain. Returns deleted row ids.
20408
+ * Retention sweep: drop every embedding older than `cutoffMs`, in one
20409
+ * filtered delete rather than the page-and-delete loop the collection needed.
20410
+ * Returns the number removed.
20636
20411
  */
20637
20412
  async pruneAll(cutoffMs) {
20638
- const ids = [];
20639
- for (;;) {
20640
- const rows = await this.store.query.query({
20641
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20642
- filter: {
20643
- whereBetween: { timestamp: [0, cutoffMs] },
20644
- limit: 500
20645
- }
20646
- });
20647
- if (rows.length === 0) break;
20648
- let deletedInPage = 0;
20649
- for (const row of rows) {
20650
- const id = typeof row.id === "string" ? row.id : void 0;
20651
- if (!id) continue;
20652
- try {
20653
- await this.store.delete.mutate({
20654
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20655
- key: id
20656
- });
20657
- ids.push(id);
20658
- deletedInPage++;
20659
- } catch {}
20660
- }
20661
- if (deletedInPage === 0) break;
20413
+ try {
20414
+ return (await this.vectorStore.deleteByFilter.mutate({
20415
+ index: OBJECT_CLIP_INDEX,
20416
+ filter: { between: { timestamp: [0, cutoffMs] } }
20417
+ })).deleted;
20418
+ } catch (err) {
20419
+ this.logger.warn("ObjectEmbeddingStore.pruneAll failed", { meta: {
20420
+ cutoffMs,
20421
+ error: String(err)
20422
+ } });
20423
+ return 0;
20662
20424
  }
20663
- await this.mirror?.remove(ids);
20664
- return ids;
20665
20425
  }
20666
20426
  /**
20667
20427
  * `TrackScopedStore` contract for the retention cascade: delete the per-track
20668
- * CLIP search vector for the given tracks. The row id IS the trackId (one row
20669
- * per track), so this deletes by key directly. Pure EPHEMERAL track state —
20670
- * the identity MATCHING embeddings (ArcFace) live in a SEPARATE durable store
20428
+ * CLIP search vector for the given tracks. Pure EPHEMERAL track state — the
20429
+ * identity MATCHING embeddings (ArcFace) live in a SEPARATE durable store
20671
20430
  * (`identity-samples`) and are never touched here, so pruning this never
20672
- * breaks recognition (design §2/§4). Per-track ISOLATED + best-effort.
20431
+ * breaks recognition (design §2/§4).
20673
20432
  */
20674
20433
  async deleteByTracks(trackIds) {
20675
- await this.mirror?.remove(trackIds);
20676
- for (const trackId of trackIds) try {
20677
- await this.store.delete.mutate({
20678
- collection: OBJECT_EMBEDDINGS_COLLECTION,
20679
- key: trackId
20434
+ if (trackIds.length === 0) return;
20435
+ try {
20436
+ await this.vectorStore.deleteByIds.mutate({
20437
+ index: OBJECT_CLIP_INDEX,
20438
+ ids: [...trackIds]
20680
20439
  });
20681
20440
  } catch (err) {
20682
20441
  this.logger.warn("ObjectEmbeddingStore.deleteByTracks failed", { meta: {
20683
- trackId,
20442
+ count: trackIds.length,
20684
20443
  error: String(err)
20685
20444
  } });
20686
20445
  }
20687
20446
  }
20688
20447
  };
20448
+ /** Build the vector-store metadata for one embedding. */
20449
+ function metadataFor(input) {
20450
+ return {
20451
+ deviceId: input.deviceId,
20452
+ timestamp: input.timestamp,
20453
+ className: input.className,
20454
+ modelId: input.modelId,
20455
+ trackId: input.trackId,
20456
+ confidence: input.confidence,
20457
+ ...input.mediaKey !== void 0 ? { mediaKey: input.mediaKey } : {},
20458
+ ...input.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: input.keyFrameMediaKey } : {}
20459
+ };
20460
+ }
20461
+ /**
20462
+ * Rebuild a stored row from vector-store metadata.
20463
+ *
20464
+ * Returns null when the row lacks its identity fields rather than filling them
20465
+ * with defaults: a hit whose `deviceId` silently became 0 would be joined to
20466
+ * the wrong camera's events.
20467
+ */
20468
+ function rowFromMetadata(id, metadata) {
20469
+ const deviceId = metadata["deviceId"];
20470
+ const timestamp = metadata["timestamp"];
20471
+ const trackId = metadata["trackId"] ?? id;
20472
+ if (typeof deviceId !== "number" || typeof timestamp !== "number") return null;
20473
+ if (typeof trackId !== "string") return null;
20474
+ const className = metadata["className"];
20475
+ const modelId = metadata["modelId"];
20476
+ const confidence = metadata["confidence"];
20477
+ const mediaKey = metadata["mediaKey"];
20478
+ const keyFrameMediaKey = metadata["keyFrameMediaKey"];
20479
+ return {
20480
+ id,
20481
+ trackId,
20482
+ deviceId,
20483
+ timestamp,
20484
+ className: typeof className === "string" ? className : "unknown",
20485
+ modelId: typeof modelId === "string" ? modelId : "unknown",
20486
+ confidence: typeof confidence === "number" ? confidence : 0,
20487
+ ...typeof mediaKey === "string" ? { mediaKey } : {},
20488
+ ...typeof keyFrameMediaKey === "string" ? { keyFrameMediaKey } : {}
20489
+ };
20490
+ }
20689
20491
  //#endregion
20690
20492
  //#region src/pipeline-analytics/store/ops-log-store.ts
20691
20493
  /**
@@ -23680,7 +23482,6 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
23680
23482
  await FaceStore.declare(api.settingsStore);
23681
23483
  await PlateStore.declare(api.settingsStore);
23682
23484
  await VehicleStore.declare(api.settingsStore);
23683
- await ObjectEmbeddingStore.declare(api.settingsStore);
23684
23485
  await StationaryObjectRegistry.declare(api.settingsStore);
23685
23486
  await OpsLogStore.declare(api.settingsStore);
23686
23487
  await NotificationCenter.declare(api.settingsStore);
@@ -23939,14 +23740,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
23939
23740
  });
23940
23741
  this.plateRecognizer.refreshGallery();
23941
23742
  const vectorStore = api.vectorStore;
23942
- const mirror = vectorStore !== void 0 ? new ObjectEmbeddingMirror({
23743
+ if (vectorStore === void 0) logger.warn("vector-store capability unavailable — CLIP semantic search is disabled", { meta: { addon: "@camstack/addon-vector-store" } });
23744
+ else this.objectEmbeddingStore = new ObjectEmbeddingStore({
23943
23745
  vectorStore,
23944
- logger: logger.child("ObjectEmbeddingMirror")
23945
- }) : void 0;
23946
- this.objectEmbeddingStore = new ObjectEmbeddingStore({
23947
- store: api.settingsStore,
23948
- logger: logger.child("ObjectEmbeddingStore"),
23949
- ...mirror !== void 0 ? { mirror } : {}
23746
+ logger: logger.child("ObjectEmbeddingStore")
23950
23747
  });
23951
23748
  }
23952
23749
  /** Two-plane detail dispatcher: the per-frame executor runs ONLY the root
@@ -26159,8 +25956,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26159
25956
  this.ctx.logger.debug("plate buffer prune failed", { meta: { error: String(err) } });
26160
25957
  }
26161
25958
  if (this.objectEmbeddingStore && Number.isFinite(minObjectCutoffMs)) try {
26162
- const deletedEmbIds = await this.objectEmbeddingStore.pruneAll(minObjectCutoffMs);
26163
- if (deletedEmbIds.length > 0) this.ctx.logger.info("object embedding retention prune", { meta: { deleted: deletedEmbIds.length } });
25959
+ const deletedEmbeddings = await this.objectEmbeddingStore.pruneAll(minObjectCutoffMs);
25960
+ if (deletedEmbeddings > 0) this.ctx.logger.info("object embedding retention prune", { meta: { deleted: deletedEmbeddings } });
26164
25961
  } catch (err) {
26165
25962
  this.ctx.logger.debug("object embedding prune failed", { meta: { error: String(err) } });
26166
25963
  }
@@ -26701,9 +26498,9 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26701
26498
  *
26702
26499
  * Flow:
26703
26500
  * 1. Call `embedding-encoder.encodeText` to embed `text` → `queryVec`.
26704
- * 2. Query `objectEmbeddingStore` with optional prefilters (deviceId,
26705
- * since/until, classFilter, modelId from encoder).
26706
- * 3. Rank candidates via `rankByText` (same-model/dim gate + cosine).
26501
+ * 2. Resolve the active encoder's modelId (same-model gate).
26502
+ * 3. Rank IN THE VECTOR STORE — prefilters pushed into SQL, cosine over
26503
+ * compact rows in the provider.
26707
26504
  * 4. Fetch the corresponding ObjectEvents by trackId (via getObjectEvents
26708
26505
  * with each winner's deviceId + trackId filter) and join score.
26709
26506
  * 5. Return at most `limit` ScoredObjectEvents sorted descending by score.
@@ -26719,39 +26516,37 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
26719
26516
  });
26720
26517
  if (!encodeResult) return [];
26721
26518
  const queryVec = new Float32Array(encodeResult.embedding);
26722
- const embeddingRows = await store.query({
26723
- deviceId: input.deviceId,
26724
- since: input.since,
26725
- until: input.until,
26726
- className: input.classFilter,
26727
- limit: EMBEDDING_SEARCH_CANDIDATE_CAP
26519
+ const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
26520
+ this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
26521
+ return null;
26728
26522
  });
26729
- if (embeddingRows.length >= 5e3) this.ctx.logger.warn("searchObjectEvents: candidate pool truncated", {
26523
+ const search = await store.searchByVector({
26524
+ vector: queryVec,
26525
+ limit: input.limit,
26526
+ minScore: input.minScore,
26527
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
26528
+ ...input.since !== void 0 ? { since: input.since } : {},
26529
+ ...input.until !== void 0 ? { until: input.until } : {},
26530
+ ...input.classFilter !== void 0 ? { className: input.classFilter } : {},
26531
+ ...encoderInfo !== null ? { modelId: encoderInfo.modelId } : {}
26532
+ });
26533
+ if (search.truncated) this.ctx.logger.warn("searchObjectEvents: candidate pool truncated", {
26730
26534
  meta: {
26731
- cap: EMBEDDING_SEARCH_CANDIDATE_CAP,
26535
+ scanned: search.scanned,
26732
26536
  since: input.since,
26733
26537
  until: input.until,
26734
26538
  className: input.classFilter
26735
26539
  },
26736
- tags: input.deviceId !== void 0 ? { deviceId: input.deviceId } : void 0
26540
+ ...input.deviceId !== void 0 ? { tags: { deviceId: input.deviceId } } : {}
26737
26541
  });
26738
- if (embeddingRows.length === 0) return [];
26542
+ const ranked = search.hits;
26543
+ if (ranked.length === 0) return [];
26739
26544
  const embeddingMediaKeyByTrackId = /* @__PURE__ */ new Map();
26740
26545
  const keyFrameKeyByTrackId = /* @__PURE__ */ new Map();
26741
- for (const row of embeddingRows) {
26546
+ for (const { row } of ranked) {
26742
26547
  if (row.mediaKey !== void 0) embeddingMediaKeyByTrackId.set(row.trackId, row.mediaKey);
26743
26548
  if (row.keyFrameMediaKey !== void 0) keyFrameKeyByTrackId.set(row.trackId, row.keyFrameMediaKey);
26744
26549
  }
26745
- const encoderInfo = await api.embeddingEncoder.getInfo.query().catch((err) => {
26746
- this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
26747
- return null;
26748
- });
26749
- const ranked = rankByText(queryVec, embeddingRows, {
26750
- minScore: input.minScore,
26751
- limit: input.limit,
26752
- ...encoderInfo !== null ? { modelId: encoderInfo.modelId } : {}
26753
- });
26754
- if (ranked.length === 0) return [];
26755
26550
  const scoreByTrackId = /* @__PURE__ */ new Map();
26756
26551
  for (const { row, score } of ranked) scoreByTrackId.set(row.trackId, score);
26757
26552
  const events = await (this.eventStore?.queryObjectByTrackIds([...scoreByTrackId.keys()]) ?? Promise.resolve([]));