@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.
- package/dist/{dist-B-2HqwZV.mjs → dist-BdBTDQxw.mjs} +25 -1
- package/dist/{dist-cXcI2z-g.js → dist-DQvvZ5_E.js} +25 -1
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DmmNcR6B.js → node-CJcK55RY.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CyRodtV2.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrKQwfNe.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-D2GwK89q.mjs → hostInit-DyfCBQE8.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +190 -395
- package/dist/pipeline-analytics/index.mjs +189 -394
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
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-
|
|
20253
|
+
//#region src/pipeline-analytics/store/object-embedding-store.ts
|
|
20289
20254
|
/**
|
|
20290
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
20300
|
-
*
|
|
20301
|
-
*
|
|
20302
|
-
*
|
|
20303
|
-
*
|
|
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
|
-
*
|
|
20306
|
-
*
|
|
20307
|
-
*
|
|
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
|
-
* ##
|
|
20272
|
+
* ## What did NOT change
|
|
20310
20273
|
*
|
|
20311
|
-
* One
|
|
20312
|
-
*
|
|
20313
|
-
*
|
|
20314
|
-
*
|
|
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
|
|
20279
|
+
/** The vector-store index this store owns. */
|
|
20317
20280
|
var OBJECT_CLIP_INDEX = "object-clip";
|
|
20318
|
-
|
|
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
|
|
20341
|
-
*
|
|
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
|
|
20344
|
-
*
|
|
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("
|
|
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
|
-
/**
|
|
20366
|
-
|
|
20367
|
-
|
|
20368
|
-
|
|
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:
|
|
20374
|
-
vector: encodeVectorBase64(
|
|
20375
|
-
metadata: metadataFor(
|
|
20334
|
+
id: input.trackId,
|
|
20335
|
+
vector: encodeVectorBase64(input.embedding),
|
|
20336
|
+
metadata: metadataFor(input)
|
|
20376
20337
|
}]
|
|
20377
|
-
})).rejected > 0) this.logger.warn("
|
|
20378
|
-
tags: { 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:
|
|
20389
|
-
|
|
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
|
-
*
|
|
20603
|
-
*
|
|
20604
|
-
|
|
20605
|
-
|
|
20606
|
-
|
|
20607
|
-
|
|
20608
|
-
|
|
20609
|
-
|
|
20610
|
-
|
|
20611
|
-
if (input.
|
|
20612
|
-
if (input.
|
|
20613
|
-
|
|
20614
|
-
|
|
20615
|
-
|
|
20616
|
-
|
|
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
|
-
|
|
20620
|
-
|
|
20621
|
-
|
|
20622
|
-
|
|
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
|
-
|
|
20626
|
-
|
|
20627
|
-
|
|
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:
|
|
20634
|
-
*
|
|
20635
|
-
*
|
|
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
|
-
|
|
20639
|
-
|
|
20640
|
-
|
|
20641
|
-
|
|
20642
|
-
|
|
20643
|
-
|
|
20644
|
-
|
|
20645
|
-
|
|
20646
|
-
|
|
20647
|
-
|
|
20648
|
-
|
|
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.
|
|
20669
|
-
*
|
|
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).
|
|
20431
|
+
* breaks recognition (design §2/§4).
|
|
20673
20432
|
*/
|
|
20674
20433
|
async deleteByTracks(trackIds) {
|
|
20675
|
-
|
|
20676
|
-
|
|
20677
|
-
await this.
|
|
20678
|
-
|
|
20679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
26163
|
-
if (
|
|
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.
|
|
26705
|
-
*
|
|
26706
|
-
*
|
|
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
|
|
26723
|
-
|
|
26724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26535
|
+
scanned: search.scanned,
|
|
26732
26536
|
since: input.since,
|
|
26733
26537
|
until: input.until,
|
|
26734
26538
|
className: input.classFilter
|
|
26735
26539
|
},
|
|
26736
|
-
|
|
26540
|
+
...input.deviceId !== void 0 ? { tags: { deviceId: input.deviceId } } : {}
|
|
26737
26541
|
});
|
|
26738
|
-
|
|
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
|
|
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([]));
|