@camstack/addon-post-analysis 1.2.38 → 1.2.40
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-DQvvZ5_E.js → dist-C09kySqI.js} +67 -1
- package/dist/{dist-BdBTDQxw.mjs → dist-CrCaHNC2.mjs} +67 -1
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-CJcK55RY.js → node-DL2FM8_E.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrKQwfNe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dt_25eeS.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-PQKnzGEk.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DbdKLyhB.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DyfCBQE8.mjs → hostInit-BfGl-Bnl.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +432 -50
- package/dist/pipeline-analytics/index.mjs +429 -47
- 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-CrCaHNC2.mjs";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -11949,6 +11949,189 @@ var EmbeddingDispatcher = class {
|
|
|
11949
11949
|
}
|
|
11950
11950
|
};
|
|
11951
11951
|
//#endregion
|
|
11952
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
11953
|
+
/**
|
|
11954
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
11955
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
11956
|
+
*
|
|
11957
|
+
* Formula (before clamping):
|
|
11958
|
+
* newX = x - padding * w
|
|
11959
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
11960
|
+
* newY = y - padding * h
|
|
11961
|
+
* newH = h + 2 * padding * h
|
|
11962
|
+
*
|
|
11963
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
11964
|
+
* new object.
|
|
11965
|
+
*
|
|
11966
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
11967
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
11968
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
11969
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
11970
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
11971
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
11972
|
+
*/
|
|
11973
|
+
function padBbox(bbox, padding) {
|
|
11974
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
11975
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
11976
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
11977
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
11978
|
+
const w = Math.min(rawW, 1);
|
|
11979
|
+
const h = Math.min(rawH, 1);
|
|
11980
|
+
return {
|
|
11981
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
11982
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
11983
|
+
w,
|
|
11984
|
+
h
|
|
11985
|
+
};
|
|
11986
|
+
}
|
|
11987
|
+
/**
|
|
11988
|
+
* Widen a bbox and optionally square it, staying inside the unit frame.
|
|
11989
|
+
*
|
|
11990
|
+
* Squaring uses the frame's aspect so the window is square in PIXELS, not in
|
|
11991
|
+
* normalised units — those are only the same on a square frame, and no camera
|
|
11992
|
+
* produces one.
|
|
11993
|
+
*/
|
|
11994
|
+
function cropWindow(bbox, frameWidth, frameHeight, options) {
|
|
11995
|
+
const padded = padBbox(bbox, options.margin);
|
|
11996
|
+
if (!options.square) return padded;
|
|
11997
|
+
const pxW = padded.w * frameWidth;
|
|
11998
|
+
const pxH = padded.h * frameHeight;
|
|
11999
|
+
const side = Math.max(pxW, pxH);
|
|
12000
|
+
const w = Math.min(side / frameWidth, 1);
|
|
12001
|
+
const h = Math.min(side / frameHeight, 1);
|
|
12002
|
+
const cx = padded.x + padded.w / 2;
|
|
12003
|
+
const cy = padded.y + padded.h / 2;
|
|
12004
|
+
return {
|
|
12005
|
+
w,
|
|
12006
|
+
h,
|
|
12007
|
+
x: Math.min(Math.max(0, cx - w / 2), 1 - w),
|
|
12008
|
+
y: Math.min(Math.max(0, cy - h / 2), 1 - h)
|
|
12009
|
+
};
|
|
12010
|
+
}
|
|
12011
|
+
/** Cut a JPEG crop out of a JPEG frame using a normalised window. */
|
|
12012
|
+
async function cropFromJpeg(jpeg, window) {
|
|
12013
|
+
const image = sharp(jpeg);
|
|
12014
|
+
const meta = await image.metadata();
|
|
12015
|
+
const frameWidth = meta.width;
|
|
12016
|
+
const frameHeight = meta.height;
|
|
12017
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12018
|
+
const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
|
|
12019
|
+
const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
|
|
12020
|
+
const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
|
|
12021
|
+
const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
|
|
12022
|
+
return {
|
|
12023
|
+
crop: await image.extract({
|
|
12024
|
+
left,
|
|
12025
|
+
top,
|
|
12026
|
+
width,
|
|
12027
|
+
height
|
|
12028
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
12029
|
+
width,
|
|
12030
|
+
height
|
|
12031
|
+
};
|
|
12032
|
+
}
|
|
12033
|
+
/**
|
|
12034
|
+
* Re-embed every listed track from its key frame.
|
|
12035
|
+
*
|
|
12036
|
+
* Every skip is COUNTED and the counts are returned, because the caller's next
|
|
12037
|
+
* decision — whether the index is now consistent enough to trust — depends on
|
|
12038
|
+
* knowing how much was not rebuilt, not just how much was.
|
|
12039
|
+
*/
|
|
12040
|
+
async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
12041
|
+
const margin = options.cropMargin ?? .2;
|
|
12042
|
+
const square = options.square ?? true;
|
|
12043
|
+
const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
|
|
12044
|
+
const trackIds = await deps.listTrackIds();
|
|
12045
|
+
const considered = trackIds.slice(0, Number.isFinite(maxTracks) ? maxTracks : void 0);
|
|
12046
|
+
let rebuilt = 0;
|
|
12047
|
+
let missingKeyFrame = 0;
|
|
12048
|
+
let missingBbox = 0;
|
|
12049
|
+
let failed = 0;
|
|
12050
|
+
for (const trackId of considered) try {
|
|
12051
|
+
const source = await deps.loadSource(trackId);
|
|
12052
|
+
if (source === null) {
|
|
12053
|
+
missingKeyFrame += 1;
|
|
12054
|
+
continue;
|
|
12055
|
+
}
|
|
12056
|
+
if (source.keyFrame === null) {
|
|
12057
|
+
missingKeyFrame += 1;
|
|
12058
|
+
continue;
|
|
12059
|
+
}
|
|
12060
|
+
if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
|
|
12061
|
+
missingBbox += 1;
|
|
12062
|
+
continue;
|
|
12063
|
+
}
|
|
12064
|
+
const meta = await sharp(source.keyFrame).metadata();
|
|
12065
|
+
if (meta.width === void 0 || meta.height === void 0) {
|
|
12066
|
+
failed += 1;
|
|
12067
|
+
continue;
|
|
12068
|
+
}
|
|
12069
|
+
const window = cropWindow(source.bbox, meta.width, meta.height, {
|
|
12070
|
+
margin,
|
|
12071
|
+
square
|
|
12072
|
+
});
|
|
12073
|
+
const cut = await cropFromJpeg(source.keyFrame, window);
|
|
12074
|
+
if (cut === null) {
|
|
12075
|
+
failed += 1;
|
|
12076
|
+
continue;
|
|
12077
|
+
}
|
|
12078
|
+
const embedding = await deps.encode(cut.crop, cut.width, cut.height);
|
|
12079
|
+
if (embedding.length === 0) {
|
|
12080
|
+
failed += 1;
|
|
12081
|
+
continue;
|
|
12082
|
+
}
|
|
12083
|
+
await deps.store(source, embedding);
|
|
12084
|
+
rebuilt += 1;
|
|
12085
|
+
} catch (err) {
|
|
12086
|
+
failed += 1;
|
|
12087
|
+
deps.logger.warn("embedding rebuild failed for a track", { meta: {
|
|
12088
|
+
trackId,
|
|
12089
|
+
error: String(err)
|
|
12090
|
+
} });
|
|
12091
|
+
}
|
|
12092
|
+
const complete = considered.length === trackIds.length;
|
|
12093
|
+
deps.logger.info("object embedding rebuild finished", { meta: {
|
|
12094
|
+
scanned: considered.length,
|
|
12095
|
+
rebuilt,
|
|
12096
|
+
missingKeyFrame,
|
|
12097
|
+
missingBbox,
|
|
12098
|
+
failed,
|
|
12099
|
+
complete,
|
|
12100
|
+
cropMargin: margin,
|
|
12101
|
+
square
|
|
12102
|
+
} });
|
|
12103
|
+
return {
|
|
12104
|
+
scanned: considered.length,
|
|
12105
|
+
rebuilt,
|
|
12106
|
+
missingKeyFrame,
|
|
12107
|
+
missingBbox,
|
|
12108
|
+
failed,
|
|
12109
|
+
complete
|
|
12110
|
+
};
|
|
12111
|
+
}
|
|
12112
|
+
/**
|
|
12113
|
+
* Read a normalised bbox off an object event.
|
|
12114
|
+
*
|
|
12115
|
+
* Events carry the box in FRAME PIXELS alongside `frameWidth`/`frameHeight`,
|
|
12116
|
+
* not normalised — normalising here rather than at the call site keeps the
|
|
12117
|
+
* conversion in one place, next to the code that consumes it.
|
|
12118
|
+
* Returns null when the frame dimensions are missing, because a box without
|
|
12119
|
+
* them cannot be placed on a differently-sized key frame.
|
|
12120
|
+
*/
|
|
12121
|
+
function normaliseEventBbox(event) {
|
|
12122
|
+
const { bbox, frameWidth, frameHeight } = event;
|
|
12123
|
+
if (bbox === void 0) return null;
|
|
12124
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12125
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
12126
|
+
if (bbox.w <= 0 || bbox.h <= 0) return null;
|
|
12127
|
+
return {
|
|
12128
|
+
x: bbox.x / frameWidth,
|
|
12129
|
+
y: bbox.y / frameHeight,
|
|
12130
|
+
w: bbox.w / frameWidth,
|
|
12131
|
+
h: bbox.h / frameHeight
|
|
12132
|
+
};
|
|
12133
|
+
}
|
|
12134
|
+
//#endregion
|
|
11952
12135
|
//#region src/pipeline-analytics/event-media-handler.ts
|
|
11953
12136
|
var CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
11954
12137
|
/** Default / clamp bounds for the `thumb` variant edge (px). Mirrors
|
|
@@ -14280,42 +14463,6 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14280
14463
|
};
|
|
14281
14464
|
}
|
|
14282
14465
|
//#endregion
|
|
14283
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
14284
|
-
/**
|
|
14285
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
14286
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
14287
|
-
*
|
|
14288
|
-
* Formula (before clamping):
|
|
14289
|
-
* newX = x - padding * w
|
|
14290
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
14291
|
-
* newY = y - padding * h
|
|
14292
|
-
* newH = h + 2 * padding * h
|
|
14293
|
-
*
|
|
14294
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
14295
|
-
* new object.
|
|
14296
|
-
*
|
|
14297
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
14298
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
14299
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
14300
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
14301
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
14302
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
14303
|
-
*/
|
|
14304
|
-
function padBbox(bbox, padding) {
|
|
14305
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
14306
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
14307
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
14308
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
14309
|
-
const w = Math.min(rawW, 1);
|
|
14310
|
-
const h = Math.min(rawH, 1);
|
|
14311
|
-
return {
|
|
14312
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
14313
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
14314
|
-
w,
|
|
14315
|
-
h
|
|
14316
|
-
};
|
|
14317
|
-
}
|
|
14318
|
-
//#endregion
|
|
14319
14466
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14320
14467
|
function createKeyFrameCrop(deps) {
|
|
14321
14468
|
return async (input) => {
|
|
@@ -20424,6 +20571,96 @@ var ObjectEmbeddingStore = class {
|
|
|
20424
20571
|
}
|
|
20425
20572
|
}
|
|
20426
20573
|
/**
|
|
20574
|
+
* Retention sweep for ONE device.
|
|
20575
|
+
*
|
|
20576
|
+
* Replaces the fleet-global prune this store used to get. Events are evicted
|
|
20577
|
+
* per device at that device's own cutoff, but embeddings were pruned at the
|
|
20578
|
+
* MINIMUM cutoff across the fleet — "never prune anything a longer-retention
|
|
20579
|
+
* device still owns" — which left a short-retention camera with weeks of
|
|
20580
|
+
* vectors whose events were long gone. Measured on device 590: events back
|
|
20581
|
+
* 1.0 day, embeddings back 22.1, and 1,446 of 2,222 rows unrenderable.
|
|
20582
|
+
*
|
|
20583
|
+
* The fleet bound existed because the old store could not filter by device.
|
|
20584
|
+
* `deviceId` is a promoted, indexed column here, so this is one filter.
|
|
20585
|
+
*/
|
|
20586
|
+
async pruneDeviceBefore(deviceId, cutoffMs) {
|
|
20587
|
+
try {
|
|
20588
|
+
return (await this.vectorStore.deleteByFilter.mutate({
|
|
20589
|
+
index: OBJECT_CLIP_INDEX,
|
|
20590
|
+
filter: {
|
|
20591
|
+
equals: { deviceId },
|
|
20592
|
+
between: { timestamp: [0, cutoffMs] }
|
|
20593
|
+
}
|
|
20594
|
+
})).deleted;
|
|
20595
|
+
} catch (err) {
|
|
20596
|
+
this.logger.warn("ObjectEmbeddingStore.pruneDeviceBefore failed", {
|
|
20597
|
+
tags: { deviceId },
|
|
20598
|
+
meta: {
|
|
20599
|
+
cutoffMs,
|
|
20600
|
+
error: String(err)
|
|
20601
|
+
}
|
|
20602
|
+
});
|
|
20603
|
+
return 0;
|
|
20604
|
+
}
|
|
20605
|
+
}
|
|
20606
|
+
/**
|
|
20607
|
+
* Drop every vector whose track no longer exists.
|
|
20608
|
+
*
|
|
20609
|
+
* The cascade already deletes a track's vector when the track is deleted, so
|
|
20610
|
+
* in principle this finds nothing. In practice it found 1,446 orphans on one
|
|
20611
|
+
* camera, because retention pruned by two different rules. A reconcile that
|
|
20612
|
+
* cannot explain how it got work to do is still the right shape — the
|
|
20613
|
+
* alternative is trusting that every deletion path remembered this store,
|
|
20614
|
+
* which is exactly the assumption that failed.
|
|
20615
|
+
*
|
|
20616
|
+
* `liveTrackIds` is the authority. An EMPTY set is refused rather than
|
|
20617
|
+
* treated as "delete everything": a track store that failed to answer would
|
|
20618
|
+
* otherwise wipe the whole index.
|
|
20619
|
+
*/
|
|
20620
|
+
async pruneOrphans(input) {
|
|
20621
|
+
if (input.liveTrackIds.size === 0) {
|
|
20622
|
+
this.logger.warn("ObjectEmbeddingStore.pruneOrphans refused an empty live-track set", { meta: { candidates: input.candidates.length } });
|
|
20623
|
+
return {
|
|
20624
|
+
deleted: 0,
|
|
20625
|
+
refused: true
|
|
20626
|
+
};
|
|
20627
|
+
}
|
|
20628
|
+
const orphans = input.candidates.filter((id) => !input.liveTrackIds.has(id));
|
|
20629
|
+
if (orphans.length === 0) return {
|
|
20630
|
+
deleted: 0,
|
|
20631
|
+
refused: false
|
|
20632
|
+
};
|
|
20633
|
+
await this.deleteByTracks(orphans);
|
|
20634
|
+
this.logger.info("object embedding orphans pruned", { meta: {
|
|
20635
|
+
orphans: orphans.length,
|
|
20636
|
+
candidates: input.candidates.length
|
|
20637
|
+
} });
|
|
20638
|
+
return {
|
|
20639
|
+
deleted: orphans.length,
|
|
20640
|
+
refused: false
|
|
20641
|
+
};
|
|
20642
|
+
}
|
|
20643
|
+
/**
|
|
20644
|
+
* Empty the index completely — the clean slate before a policy change.
|
|
20645
|
+
*
|
|
20646
|
+
* Changing the crop margin or the encoder model leaves two feature spaces in
|
|
20647
|
+
* one index whose cosine scores are not comparable. Wiping first, then
|
|
20648
|
+
* rebuilding, is the only way to be sure every vector means the same thing.
|
|
20649
|
+
*/
|
|
20650
|
+
async wipeAll() {
|
|
20651
|
+
try {
|
|
20652
|
+
const result = await this.vectorStore.deleteByFilter.mutate({
|
|
20653
|
+
index: OBJECT_CLIP_INDEX,
|
|
20654
|
+
filter: { between: { timestamp: [0, Number.MAX_SAFE_INTEGER] } }
|
|
20655
|
+
});
|
|
20656
|
+
this.logger.info("object embedding index wiped", { meta: { deleted: result.deleted } });
|
|
20657
|
+
return result.deleted;
|
|
20658
|
+
} catch (err) {
|
|
20659
|
+
this.logger.warn("ObjectEmbeddingStore.wipeAll failed", { meta: { error: String(err) } });
|
|
20660
|
+
return 0;
|
|
20661
|
+
}
|
|
20662
|
+
}
|
|
20663
|
+
/**
|
|
20427
20664
|
* `TrackScopedStore` contract for the retention cascade: delete the per-track
|
|
20428
20665
|
* CLIP search vector for the given tracks. Pure EPHEMERAL track state — the
|
|
20429
20666
|
* identity MATCHING embeddings (ArcFace) live in a SEPARATE durable store
|
|
@@ -22687,6 +22924,15 @@ function buildStationarySnapshot(input) {
|
|
|
22687
22924
|
//#region src/pipeline-analytics/index.ts
|
|
22688
22925
|
/** This addon's manifest id — the owner of the fixed Notification Center
|
|
22689
22926
|
* integration and of the alarm device under it. */
|
|
22927
|
+
/**
|
|
22928
|
+
* How long the active encoder's model id is trusted without re-asking.
|
|
22929
|
+
*
|
|
22930
|
+
* `embeddingEncoder.getInfo` measured 1.42s on the live hub — a cross-process
|
|
22931
|
+
* call into an addon busy with live inference — for a value that changes only
|
|
22932
|
+
* when an operator swaps encoders. A minute is short enough that a swap takes
|
|
22933
|
+
* effect without a restart and long enough that no search pays for it twice.
|
|
22934
|
+
*/
|
|
22935
|
+
var ENCODER_INFO_TTL_MS = 6e4;
|
|
22690
22936
|
var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
22691
22937
|
/** Operator-visible name of the fixed integration. */
|
|
22692
22938
|
var NC_INTEGRATION_NAME = "Notification Center";
|
|
@@ -22874,6 +23120,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
22874
23120
|
plateStore = null;
|
|
22875
23121
|
plateRecognizer = null;
|
|
22876
23122
|
objectEmbeddingStore = null;
|
|
23123
|
+
/** Cached active-encoder model id — see `resolveEncoderModelId`. */
|
|
23124
|
+
encoderInfoCache = null;
|
|
22877
23125
|
/** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
|
|
22878
23126
|
* classifier/plate enrichment via `pipelineRunner.runDetailSubtree`. Replaces
|
|
22879
23127
|
* the per-frame child consumption the executor no longer emits. */
|
|
@@ -25210,6 +25458,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
25210
25458
|
});
|
|
25211
25459
|
return;
|
|
25212
25460
|
}
|
|
25461
|
+
const keyFrameMediaKey = this.residents.keyFrameKey(trackId);
|
|
25213
25462
|
await store.upsertIfBetter({
|
|
25214
25463
|
trackId,
|
|
25215
25464
|
deviceId,
|
|
@@ -25217,7 +25466,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
25217
25466
|
className: detail.className,
|
|
25218
25467
|
embedding: decodeEmbeddingBase64(detail.embedding),
|
|
25219
25468
|
modelId,
|
|
25220
|
-
confidence: detail.score
|
|
25469
|
+
confidence: detail.score,
|
|
25470
|
+
...keyFrameMediaKey !== void 0 ? { keyFrameMediaKey } : {}
|
|
25221
25471
|
});
|
|
25222
25472
|
}
|
|
25223
25473
|
/** Classifier answer / plate OCR text → the track's enrichment label
|
|
@@ -25955,11 +26205,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
25955
26205
|
} catch (err) {
|
|
25956
26206
|
this.ctx.logger.debug("plate buffer prune failed", { meta: { error: String(err) } });
|
|
25957
26207
|
}
|
|
25958
|
-
if (this.objectEmbeddingStore
|
|
25959
|
-
const
|
|
26208
|
+
if (this.objectEmbeddingStore) {
|
|
26209
|
+
const embeddingStore = this.objectEmbeddingStore;
|
|
26210
|
+
let deletedEmbeddings = 0;
|
|
26211
|
+
for (const deviceId of deviceIds) {
|
|
26212
|
+
if (this.shuttingDown) return;
|
|
26213
|
+
try {
|
|
26214
|
+
const cutoffs = await this.deviceRetentionCutoffs(deviceId, now);
|
|
26215
|
+
if (cutoffs.objectCutoffMs === null) continue;
|
|
26216
|
+
deletedEmbeddings += await embeddingStore.pruneDeviceBefore(deviceId, cutoffs.objectCutoffMs);
|
|
26217
|
+
} catch (err) {
|
|
26218
|
+
this.ctx.logger.debug("object embedding prune failed", {
|
|
26219
|
+
tags: { deviceId },
|
|
26220
|
+
meta: { error: String(err) }
|
|
26221
|
+
});
|
|
26222
|
+
}
|
|
26223
|
+
}
|
|
25960
26224
|
if (deletedEmbeddings > 0) this.ctx.logger.info("object embedding retention prune", { meta: { deleted: deletedEmbeddings } });
|
|
25961
|
-
} catch (err) {
|
|
25962
|
-
this.ctx.logger.debug("object embedding prune failed", { meta: { error: String(err) } });
|
|
25963
26225
|
}
|
|
25964
26226
|
} catch (err) {
|
|
25965
26227
|
this.ctx.logger.debug("sweepRetention failed", { meta: { error: String(err) } });
|
|
@@ -26505,6 +26767,129 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26505
26767
|
* with each winner's deviceId + trackId filter) and join score.
|
|
26506
26768
|
* 5. Return at most `limit` ScoredObjectEvents sorted descending by score.
|
|
26507
26769
|
*/
|
|
26770
|
+
/**
|
|
26771
|
+
* The active encoder's model id, cached for {@link ENCODER_INFO_TTL_MS}.
|
|
26772
|
+
*
|
|
26773
|
+
* Refreshed rather than pinned forever so an operator swapping the encoder
|
|
26774
|
+
* does not need a restart to make search gate on the new model — the window
|
|
26775
|
+
* where the old id is used is bounded by the TTL, and during it a search
|
|
26776
|
+
* simply ranks against the previous model's vectors, which is what it would
|
|
26777
|
+
* have done anyway until the new ones exist.
|
|
26778
|
+
*/
|
|
26779
|
+
async resolveEncoderModelId(api) {
|
|
26780
|
+
const now = Date.now();
|
|
26781
|
+
if (this.encoderInfoCache !== null && now - this.encoderInfoCache.at < ENCODER_INFO_TTL_MS) return this.encoderInfoCache.modelId;
|
|
26782
|
+
const info = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
26783
|
+
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
26784
|
+
return null;
|
|
26785
|
+
});
|
|
26786
|
+
if (info === null) return this.encoderInfoCache?.modelId ?? null;
|
|
26787
|
+
this.encoderInfoCache = {
|
|
26788
|
+
modelId: info.modelId,
|
|
26789
|
+
at: now
|
|
26790
|
+
};
|
|
26791
|
+
return info.modelId;
|
|
26792
|
+
}
|
|
26793
|
+
/**
|
|
26794
|
+
* Empty the CLIP index — the clean slate before a crop-policy or encoder
|
|
26795
|
+
* change, to be followed by {@link rebuildObjectEmbeddings}.
|
|
26796
|
+
*/
|
|
26797
|
+
async wipeObjectEmbeddings() {
|
|
26798
|
+
const store = this.objectEmbeddingStore;
|
|
26799
|
+
if (!store) {
|
|
26800
|
+
this.ctx.logger.warn("wipeObjectEmbeddings: object embedding store unavailable");
|
|
26801
|
+
return { deleted: 0 };
|
|
26802
|
+
}
|
|
26803
|
+
return { deleted: await store.wipeAll() };
|
|
26804
|
+
}
|
|
26805
|
+
/**
|
|
26806
|
+
* Re-embed stored tracks from their key frames — the tool that makes the crop
|
|
26807
|
+
* policy tunable.
|
|
26808
|
+
*
|
|
26809
|
+
* Pixels come from each track's `keyFrame` (the FULL frame at native
|
|
26810
|
+
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
26811
|
+
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
26812
|
+
* widened after the fact.
|
|
26813
|
+
*/
|
|
26814
|
+
async rebuildObjectEmbeddings(input) {
|
|
26815
|
+
const store = this.objectEmbeddingStore;
|
|
26816
|
+
const trackStore = this.trackStore;
|
|
26817
|
+
const mediaStore = this.mediaStore;
|
|
26818
|
+
const eventStore = this.eventStore;
|
|
26819
|
+
const api = this.ctx.api;
|
|
26820
|
+
const empty = {
|
|
26821
|
+
scanned: 0,
|
|
26822
|
+
rebuilt: 0,
|
|
26823
|
+
missingKeyFrame: 0,
|
|
26824
|
+
missingBbox: 0,
|
|
26825
|
+
failed: 0,
|
|
26826
|
+
complete: true
|
|
26827
|
+
};
|
|
26828
|
+
if (!store || !trackStore || !mediaStore || !eventStore || !api) {
|
|
26829
|
+
this.ctx.logger.warn("rebuildObjectEmbeddings: analytics stores unavailable");
|
|
26830
|
+
return empty;
|
|
26831
|
+
}
|
|
26832
|
+
const modelId = await this.resolveEncoderModelId(api) ?? "unknown";
|
|
26833
|
+
const deviceIds = input.deviceId !== void 0 ? [input.deviceId] : await trackStore.listDeviceIds();
|
|
26834
|
+
return rebuildObjectEmbeddings({
|
|
26835
|
+
listTrackIds: async () => {
|
|
26836
|
+
return (await trackStore.queryRecent({
|
|
26837
|
+
deviceIds,
|
|
26838
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
26839
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
26840
|
+
limit: input.maxTracks ?? 500
|
|
26841
|
+
})).tracks.map((t) => t.trackId);
|
|
26842
|
+
},
|
|
26843
|
+
loadSource: async (trackId) => {
|
|
26844
|
+
const media = await mediaStore.listByOwner("track", trackId);
|
|
26845
|
+
const keyFrameFile = media.find((m) => m.kind === "keyFrame");
|
|
26846
|
+
const cropFile = media.find((m) => m.kind === "thumbnail");
|
|
26847
|
+
const bestEventId = (await eventStore.peakForTrack(trackId)).bestEventId;
|
|
26848
|
+
const events = await eventStore.queryObjectByTrackIds([trackId]);
|
|
26849
|
+
const best = events.find((e) => e.id === bestEventId) ?? events[0];
|
|
26850
|
+
if (best === void 0) return null;
|
|
26851
|
+
return {
|
|
26852
|
+
trackId,
|
|
26853
|
+
deviceId: best.deviceId,
|
|
26854
|
+
timestamp: best.timestamp,
|
|
26855
|
+
className: best.className,
|
|
26856
|
+
confidence: best.confidence ?? 0,
|
|
26857
|
+
keyFrame: keyFrameFile ? Buffer.from(keyFrameFile.base64, "base64") : null,
|
|
26858
|
+
bbox: normaliseEventBbox(best),
|
|
26859
|
+
...cropFile !== void 0 ? { mediaKey: cropFile.key } : {},
|
|
26860
|
+
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
26861
|
+
};
|
|
26862
|
+
},
|
|
26863
|
+
encode: async (crop, width, height) => {
|
|
26864
|
+
return (await api.embeddingEncoder.encode.query({
|
|
26865
|
+
crop: new Uint8Array(crop),
|
|
26866
|
+
width,
|
|
26867
|
+
height
|
|
26868
|
+
})).embedding;
|
|
26869
|
+
},
|
|
26870
|
+
store: async (source, embedding) => {
|
|
26871
|
+
await store.upsertIfBetter({
|
|
26872
|
+
trackId: source.trackId,
|
|
26873
|
+
deviceId: source.deviceId,
|
|
26874
|
+
timestamp: source.timestamp,
|
|
26875
|
+
className: source.className,
|
|
26876
|
+
embedding,
|
|
26877
|
+
modelId,
|
|
26878
|
+
confidence: Number.POSITIVE_INFINITY,
|
|
26879
|
+
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
26880
|
+
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
26881
|
+
});
|
|
26882
|
+
},
|
|
26883
|
+
logger: {
|
|
26884
|
+
info: (message, extras) => this.ctx.logger.info(message, extras),
|
|
26885
|
+
warn: (message, extras) => this.ctx.logger.warn(message, extras)
|
|
26886
|
+
}
|
|
26887
|
+
}, {
|
|
26888
|
+
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
26889
|
+
...input.square !== void 0 ? { square: input.square } : {},
|
|
26890
|
+
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {}
|
|
26891
|
+
});
|
|
26892
|
+
}
|
|
26508
26893
|
async searchObjectEvents(input) {
|
|
26509
26894
|
const store = this.objectEmbeddingStore;
|
|
26510
26895
|
if (!store) return [];
|
|
@@ -26516,10 +26901,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26516
26901
|
});
|
|
26517
26902
|
if (!encodeResult) return [];
|
|
26518
26903
|
const queryVec = new Float32Array(encodeResult.embedding);
|
|
26519
|
-
const
|
|
26520
|
-
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
26521
|
-
return null;
|
|
26522
|
-
});
|
|
26904
|
+
const encoderModelId = await this.resolveEncoderModelId(api);
|
|
26523
26905
|
const search = await store.searchByVector({
|
|
26524
26906
|
vector: queryVec,
|
|
26525
26907
|
limit: input.limit,
|
|
@@ -26528,7 +26910,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26528
26910
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
26529
26911
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
26530
26912
|
...input.classFilter !== void 0 ? { className: input.classFilter } : {},
|
|
26531
|
-
...
|
|
26913
|
+
...encoderModelId !== null ? { modelId: encoderModelId } : {}
|
|
26532
26914
|
});
|
|
26533
26915
|
if (search.truncated) this.ctx.logger.warn("searchObjectEvents: candidate pool truncated", {
|
|
26534
26916
|
meta: {
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dt_25eeS.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.40",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|