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