@camstack/addon-post-analysis 1.2.45 → 1.2.47
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-QzgUDrwU.js → dist-B5O4mNmd.js} +53 -8
- package/dist/{dist-BoV5RsRC.mjs → dist-BHtH4Obt.mjs} +48 -9
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-HrEN5lB7.js → node-m6HS7TOO.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DvfzPjZx.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cqisi0Ci.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BhH6N_nH.mjs → hostInit-DNsL0KwA.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +258 -162
- package/dist/pipeline-analytics/index.mjs +257 -161
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +7 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as string, A as kebabToCamel, B as BaseAddon, C as cosineSimilarity, D as encodeVectorBase64, F as subKindsOf, G as nodePin, H as createEvent, I as vectorDimFromBase64, J as boolean, K as _enum, L as videoclipsCapability, M as pipelineAnalyticsCapability, N as plateGalleryCapability, O as faceGalleryCapability, P as readDeviceStateFrom, Q as record, R as zoneAnalyticsCapability, S as buildEventKindDescriptor, T as defineCustomActions, U as hydrateSchema, V as DeviceType, W as isDeviceScopedCap, X as number, Y as literal, Z as object, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as unknown, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as notificationRulesCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as array, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, tt as EventCategory, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as errMsg } from "../dist-BHtH4Obt.mjs";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -5839,14 +5839,6 @@ function makeNcActionHandlers(deps) {
|
|
|
5839
5839
|
var RebuildInputSchema = object({
|
|
5840
5840
|
/** Restrict to one camera; omit for the whole fleet. */
|
|
5841
5841
|
deviceId: number().optional(),
|
|
5842
|
-
/**
|
|
5843
|
-
* Fraction added on EACH side of the detection box before cropping.
|
|
5844
|
-
* ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
|
|
5845
|
-
* the pixel-tight crop.
|
|
5846
|
-
*/
|
|
5847
|
-
cropMargin: number().min(0).max(4).optional(),
|
|
5848
|
-
/** Square the window so a tall subject is not squashed into CLIP's input. */
|
|
5849
|
-
square: boolean().optional(),
|
|
5850
5842
|
/** Stop after this many tracks; the status reports whether more remain. */
|
|
5851
5843
|
maxTracks: number().int().positive().optional()
|
|
5852
5844
|
});
|
|
@@ -5860,6 +5852,8 @@ var RebuildStatusSchema = object({
|
|
|
5860
5852
|
rebuilt: number(),
|
|
5861
5853
|
missingKeyFrame: number(),
|
|
5862
5854
|
missingBbox: number(),
|
|
5855
|
+
/** Refused by the pipeline (camera detached / step disabled), not broken. */
|
|
5856
|
+
notRunnable: number(),
|
|
5863
5857
|
failed: number(),
|
|
5864
5858
|
complete: boolean().nullable(),
|
|
5865
5859
|
startedAtMs: number().nullable(),
|
|
@@ -12018,88 +12012,6 @@ var EmbeddingDispatcher = class {
|
|
|
12018
12012
|
}
|
|
12019
12013
|
}
|
|
12020
12014
|
};
|
|
12021
|
-
//#endregion
|
|
12022
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
12023
|
-
/**
|
|
12024
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
12025
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
12026
|
-
*
|
|
12027
|
-
* Formula (before clamping):
|
|
12028
|
-
* newX = x - padding * w
|
|
12029
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
12030
|
-
* newY = y - padding * h
|
|
12031
|
-
* newH = h + 2 * padding * h
|
|
12032
|
-
*
|
|
12033
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
12034
|
-
* new object.
|
|
12035
|
-
*
|
|
12036
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
12037
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
12038
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
12039
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
12040
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
12041
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
12042
|
-
*/
|
|
12043
|
-
function padBbox(bbox, padding) {
|
|
12044
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
12045
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
12046
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
12047
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
12048
|
-
const w = Math.min(rawW, 1);
|
|
12049
|
-
const h = Math.min(rawH, 1);
|
|
12050
|
-
return {
|
|
12051
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
12052
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
12053
|
-
w,
|
|
12054
|
-
h
|
|
12055
|
-
};
|
|
12056
|
-
}
|
|
12057
|
-
/**
|
|
12058
|
-
* Widen a bbox and optionally square it, staying inside the unit frame.
|
|
12059
|
-
*
|
|
12060
|
-
* Squaring uses the frame's aspect so the window is square in PIXELS, not in
|
|
12061
|
-
* normalised units — those are only the same on a square frame, and no camera
|
|
12062
|
-
* produces one.
|
|
12063
|
-
*/
|
|
12064
|
-
function cropWindow(bbox, frameWidth, frameHeight, options) {
|
|
12065
|
-
const padded = padBbox(bbox, options.margin);
|
|
12066
|
-
if (!options.square) return padded;
|
|
12067
|
-
const pxW = padded.w * frameWidth;
|
|
12068
|
-
const pxH = padded.h * frameHeight;
|
|
12069
|
-
const side = Math.max(pxW, pxH);
|
|
12070
|
-
const w = Math.min(side / frameWidth, 1);
|
|
12071
|
-
const h = Math.min(side / frameHeight, 1);
|
|
12072
|
-
const cx = padded.x + padded.w / 2;
|
|
12073
|
-
const cy = padded.y + padded.h / 2;
|
|
12074
|
-
return {
|
|
12075
|
-
w,
|
|
12076
|
-
h,
|
|
12077
|
-
x: Math.min(Math.max(0, cx - w / 2), 1 - w),
|
|
12078
|
-
y: Math.min(Math.max(0, cy - h / 2), 1 - h)
|
|
12079
|
-
};
|
|
12080
|
-
}
|
|
12081
|
-
/** Cut a JPEG crop out of a JPEG frame using a normalised window. */
|
|
12082
|
-
async function cropFromJpeg(jpeg, window) {
|
|
12083
|
-
const image = sharp(jpeg);
|
|
12084
|
-
const meta = await image.metadata();
|
|
12085
|
-
const frameWidth = meta.width;
|
|
12086
|
-
const frameHeight = meta.height;
|
|
12087
|
-
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12088
|
-
const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
|
|
12089
|
-
const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
|
|
12090
|
-
const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
|
|
12091
|
-
const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
|
|
12092
|
-
return {
|
|
12093
|
-
crop: await image.extract({
|
|
12094
|
-
left,
|
|
12095
|
-
top,
|
|
12096
|
-
width,
|
|
12097
|
-
height
|
|
12098
|
-
}).jpeg({ quality: 90 }).toBuffer(),
|
|
12099
|
-
width,
|
|
12100
|
-
height
|
|
12101
|
-
};
|
|
12102
|
-
}
|
|
12103
12015
|
/**
|
|
12104
12016
|
* Re-embed every listed track from its key frame.
|
|
12105
12017
|
*
|
|
@@ -12108,12 +12020,11 @@ async function cropFromJpeg(jpeg, window) {
|
|
|
12108
12020
|
* knowing how much was not rebuilt, not just how much was.
|
|
12109
12021
|
*/
|
|
12110
12022
|
async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
12111
|
-
const margin = options.cropMargin ?? .2;
|
|
12112
|
-
const square = options.square ?? true;
|
|
12113
12023
|
const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
|
|
12114
12024
|
let rebuilt = 0;
|
|
12115
12025
|
let missingKeyFrame = 0;
|
|
12116
12026
|
let missingBbox = 0;
|
|
12027
|
+
let notRunnable = 0;
|
|
12117
12028
|
let failed = 0;
|
|
12118
12029
|
let scanned = 0;
|
|
12119
12030
|
let cursor = null;
|
|
@@ -12144,27 +12055,41 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12144
12055
|
missingBbox += 1;
|
|
12145
12056
|
continue;
|
|
12146
12057
|
}
|
|
12147
|
-
const
|
|
12148
|
-
if (
|
|
12149
|
-
|
|
12058
|
+
const outcome = await deps.embed(source);
|
|
12059
|
+
if (outcome.kind === "not-runnable") {
|
|
12060
|
+
notRunnable += 1;
|
|
12061
|
+
deps.logger.warn("embedding rebuild: track could not be re-embedded by the pipeline", {
|
|
12062
|
+
tags: { deviceId: source.deviceId },
|
|
12063
|
+
meta: {
|
|
12064
|
+
trackId,
|
|
12065
|
+
reason: outcome.reason
|
|
12066
|
+
}
|
|
12067
|
+
});
|
|
12150
12068
|
continue;
|
|
12151
12069
|
}
|
|
12152
|
-
|
|
12153
|
-
margin,
|
|
12154
|
-
square
|
|
12155
|
-
});
|
|
12156
|
-
const cut = await cropFromJpeg(source.keyFrame, window);
|
|
12157
|
-
if (cut === null) {
|
|
12070
|
+
if (outcome.kind === "failed") {
|
|
12158
12071
|
failed += 1;
|
|
12072
|
+
deps.logger.warn("embedding rebuild: the pipeline returned no vector for a track", {
|
|
12073
|
+
tags: { deviceId: source.deviceId },
|
|
12074
|
+
meta: {
|
|
12075
|
+
trackId,
|
|
12076
|
+
reason: outcome.reason
|
|
12077
|
+
}
|
|
12078
|
+
});
|
|
12159
12079
|
continue;
|
|
12160
12080
|
}
|
|
12161
|
-
const
|
|
12162
|
-
if (
|
|
12081
|
+
const written = await deps.store(source, outcome.embeddingBase64);
|
|
12082
|
+
if (written === "persisted") rebuilt += 1;
|
|
12083
|
+
else {
|
|
12163
12084
|
failed += 1;
|
|
12164
|
-
|
|
12085
|
+
deps.logger.warn("embedding rebuild wrote nothing for a track", {
|
|
12086
|
+
tags: { deviceId: source.deviceId },
|
|
12087
|
+
meta: {
|
|
12088
|
+
trackId,
|
|
12089
|
+
outcome: written
|
|
12090
|
+
}
|
|
12091
|
+
});
|
|
12165
12092
|
}
|
|
12166
|
-
await deps.store(source, embedding);
|
|
12167
|
-
rebuilt += 1;
|
|
12168
12093
|
} catch (err) {
|
|
12169
12094
|
failed += 1;
|
|
12170
12095
|
deps.logger.warn("embedding rebuild failed for a track", { meta: {
|
|
@@ -12177,6 +12102,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12177
12102
|
rebuilt,
|
|
12178
12103
|
missingKeyFrame,
|
|
12179
12104
|
missingBbox,
|
|
12105
|
+
notRunnable,
|
|
12180
12106
|
failed
|
|
12181
12107
|
});
|
|
12182
12108
|
}
|
|
@@ -12192,16 +12118,16 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12192
12118
|
rebuilt,
|
|
12193
12119
|
missingKeyFrame,
|
|
12194
12120
|
missingBbox,
|
|
12121
|
+
notRunnable,
|
|
12195
12122
|
failed,
|
|
12196
|
-
complete
|
|
12197
|
-
cropMargin: margin,
|
|
12198
|
-
square
|
|
12123
|
+
complete
|
|
12199
12124
|
} });
|
|
12200
12125
|
return {
|
|
12201
12126
|
scanned,
|
|
12202
12127
|
rebuilt,
|
|
12203
12128
|
missingKeyFrame,
|
|
12204
12129
|
missingBbox,
|
|
12130
|
+
notRunnable,
|
|
12205
12131
|
failed,
|
|
12206
12132
|
complete
|
|
12207
12133
|
};
|
|
@@ -12369,6 +12295,69 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
12369
12295
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
12370
12296
|
}
|
|
12371
12297
|
//#endregion
|
|
12298
|
+
//#region src/pipeline-analytics/rebuild-detail-request.ts
|
|
12299
|
+
/** The one step a rebuild runs. */
|
|
12300
|
+
var REBUILD_DETAIL_STEP_ID = "clip-embedding";
|
|
12301
|
+
/**
|
|
12302
|
+
* Build the `runDetailSubtree` input for one stored track.
|
|
12303
|
+
*
|
|
12304
|
+
* The box is stored NORMALISED because the key frame is a native-resolution
|
|
12305
|
+
* capture while the detection ran on a downscaled analysis frame; the two have
|
|
12306
|
+
* different pixel dimensions. `parent.bbox` is frame pixels (the live path's
|
|
12307
|
+
* convention), so it is denormalised here against the KEY FRAME's own
|
|
12308
|
+
* dimensions — the image the runner is about to cut. This is coordinate
|
|
12309
|
+
* mapping, not crop derivation: no padding, no squaring, no rectangle choice.
|
|
12310
|
+
*/
|
|
12311
|
+
function buildRebuildDetailInput(source, keyFrame) {
|
|
12312
|
+
return {
|
|
12313
|
+
deviceId: source.deviceId,
|
|
12314
|
+
frameJpeg: source.keyFrame.toString("base64"),
|
|
12315
|
+
parent: {
|
|
12316
|
+
bbox: {
|
|
12317
|
+
x: source.bbox.x * keyFrame.width,
|
|
12318
|
+
y: source.bbox.y * keyFrame.height,
|
|
12319
|
+
w: source.bbox.w * keyFrame.width,
|
|
12320
|
+
h: source.bbox.h * keyFrame.height
|
|
12321
|
+
},
|
|
12322
|
+
className: source.className
|
|
12323
|
+
},
|
|
12324
|
+
steps: [REBUILD_DETAIL_STEP_ID]
|
|
12325
|
+
};
|
|
12326
|
+
}
|
|
12327
|
+
/**
|
|
12328
|
+
* Turn one `runDetailSubtree` answer into a rebuild outcome.
|
|
12329
|
+
*
|
|
12330
|
+
* The three cases lead to three different counters:
|
|
12331
|
+
*
|
|
12332
|
+
* - `null` — the runner DECLINED: the camera is not attached, `clip-embedding`
|
|
12333
|
+
* is not enabled in its tree, or the frame was unreadable. It logs which,
|
|
12334
|
+
* tagged with the deviceId. That is `not-runnable`, not a failure: the remedy
|
|
12335
|
+
* is a configuration change, and folding it into `failed` would point the
|
|
12336
|
+
* operator at the inference engine instead.
|
|
12337
|
+
* - details with no `clip-embedding` embedding — the step RAN and produced
|
|
12338
|
+
* nothing usable. That is a failure.
|
|
12339
|
+
* - an embedding — passed through as base64, the form the vector store stores.
|
|
12340
|
+
*/
|
|
12341
|
+
function readRebuildEmbedding(result) {
|
|
12342
|
+
if (result === null) return {
|
|
12343
|
+
kind: "not-runnable",
|
|
12344
|
+
reason: "the runner returned no result — camera not attached, clip-embedding not enabled for it, or the key frame was unreadable (the runner logged which, tagged with the deviceId)"
|
|
12345
|
+
};
|
|
12346
|
+
const embedding = result.details.find((d) => d.stepId === "clip-embedding" && d.embedding !== void 0)?.embedding;
|
|
12347
|
+
if (embedding === void 0) return {
|
|
12348
|
+
kind: "failed",
|
|
12349
|
+
reason: `the detail subtree ran but returned no ${REBUILD_DETAIL_STEP_ID} embedding (${result.details.length} detail(s))`
|
|
12350
|
+
};
|
|
12351
|
+
return {
|
|
12352
|
+
kind: "embedded",
|
|
12353
|
+
embeddingBase64: embedding
|
|
12354
|
+
};
|
|
12355
|
+
}
|
|
12356
|
+
/** Narrowing helper — a source without a key frame can never build a request. */
|
|
12357
|
+
function hasKeyFrame(source) {
|
|
12358
|
+
return source.keyFrame !== null && source.bbox !== null;
|
|
12359
|
+
}
|
|
12360
|
+
//#endregion
|
|
12372
12361
|
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
12373
12362
|
/**
|
|
12374
12363
|
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
@@ -14560,6 +14549,42 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14560
14549
|
};
|
|
14561
14550
|
}
|
|
14562
14551
|
//#endregion
|
|
14552
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
14553
|
+
/**
|
|
14554
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
14555
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
14556
|
+
*
|
|
14557
|
+
* Formula (before clamping):
|
|
14558
|
+
* newX = x - padding * w
|
|
14559
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
14560
|
+
* newY = y - padding * h
|
|
14561
|
+
* newH = h + 2 * padding * h
|
|
14562
|
+
*
|
|
14563
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
14564
|
+
* new object.
|
|
14565
|
+
*
|
|
14566
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
14567
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
14568
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
14569
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
14570
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
14571
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
14572
|
+
*/
|
|
14573
|
+
function padBbox(bbox, padding) {
|
|
14574
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
14575
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
14576
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
14577
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
14578
|
+
const w = Math.min(rawW, 1);
|
|
14579
|
+
const h = Math.min(rawH, 1);
|
|
14580
|
+
return {
|
|
14581
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
14582
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
14583
|
+
w,
|
|
14584
|
+
h
|
|
14585
|
+
};
|
|
14586
|
+
}
|
|
14587
|
+
//#endregion
|
|
14563
14588
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14564
14589
|
function createKeyFrameCrop(deps) {
|
|
14565
14590
|
return async (input) => {
|
|
@@ -18549,30 +18574,18 @@ function buildGlobalSettingsSchema() {
|
|
|
18549
18574
|
{
|
|
18550
18575
|
id: "embedding-index",
|
|
18551
18576
|
title: "Semantic search index",
|
|
18552
|
-
description: "The CLIP vector index behind text search. Rebuild re-embeds stored tracks from their native key frames — the only way to change what a vector means, since a new crop
|
|
18577
|
+
description: "The CLIP vector index behind text search. Rebuild re-embeds stored tracks from their native key frames — the only way to change what a vector means, since a new crop convention or encoder model leaves two incomparable feature spaces in one index. Vectors live in the vector-store capability, alongside the events they are shown with.",
|
|
18553
18578
|
columns: 1,
|
|
18554
18579
|
fields: [
|
|
18555
|
-
{
|
|
18556
|
-
type: "number",
|
|
18557
|
-
key: "rebuildCropMargin",
|
|
18558
|
-
label: "Rebuild crop margin",
|
|
18559
|
-
description: "Fraction added on EACH side of the detection box when re-embedding. 0.2 gives a 1.4x window: CLIP is trained on natural images WITH surroundings, and a pixel-tight crop removes what it is strongest on. 0 reproduces the tight crop. Only affects Rebuild — it does not change live capture, and the thumbnail is never re-cropped.",
|
|
18560
|
-
default: .2,
|
|
18561
|
-
min: 0,
|
|
18562
|
-
max: 2,
|
|
18563
|
-
step: .1
|
|
18564
|
-
},
|
|
18565
18580
|
{
|
|
18566
18581
|
type: "addon-action-button",
|
|
18567
18582
|
key: "rebuildEmbeddingsButton",
|
|
18568
18583
|
label: "Rebuild index",
|
|
18569
|
-
description: "Re-embed every stored track from its key frame, using the
|
|
18584
|
+
description: "Re-embed every stored track from its key frame, through the detection pipeline, using the cluster Enrichment crop setting — so a rebuilt vector is comparable with a live one. Runs in the background; press Progress to watch it. Tracks whose key frame has aged out, or whose camera is detached, are reported as skipped rather than dropped.",
|
|
18570
18585
|
addonId: "pipeline-analytics",
|
|
18571
18586
|
action: "embedding.rebuild",
|
|
18572
18587
|
buttonLabel: "Rebuild",
|
|
18573
18588
|
variant: "primary",
|
|
18574
|
-
paramsFromForm: { cropMargin: "rebuildCropMargin" },
|
|
18575
|
-
paramsStatic: { square: true },
|
|
18576
18589
|
successMessage: "Rebuild started — press Progress to follow it."
|
|
18577
18590
|
},
|
|
18578
18591
|
{
|
|
@@ -18583,7 +18596,7 @@ function buildGlobalSettingsSchema() {
|
|
|
18583
18596
|
addonId: "pipeline-analytics",
|
|
18584
18597
|
action: "embedding.rebuildStatus",
|
|
18585
18598
|
buttonLabel: "Check progress",
|
|
18586
|
-
successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
|
|
18599
|
+
successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · not runnable {notRunnable} · failed {failed}"
|
|
18587
18600
|
},
|
|
18588
18601
|
{
|
|
18589
18602
|
type: "addon-action-button",
|
|
@@ -20575,6 +20588,29 @@ var IdentityStore = class {
|
|
|
20575
20588
|
*/
|
|
20576
20589
|
/** The vector-store index this store owns. */
|
|
20577
20590
|
var OBJECT_CLIP_INDEX = "object-clip";
|
|
20591
|
+
/**
|
|
20592
|
+
* Reduce either input form to the stored form.
|
|
20593
|
+
*
|
|
20594
|
+
* A base64 payload passes through untouched; its dimension is read from the
|
|
20595
|
+
* byte length. A length that is not a whole number of float32s cannot be a
|
|
20596
|
+
* vector, and storing it would produce a row whose dimension silently
|
|
20597
|
+
* disagrees with the declared index.
|
|
20598
|
+
*/
|
|
20599
|
+
function resolveVectorPayload(input) {
|
|
20600
|
+
if (input.embeddingBase64 !== void 0) {
|
|
20601
|
+
const byteLength = Buffer.from(input.embeddingBase64, "base64").byteLength;
|
|
20602
|
+
if (byteLength === 0 || byteLength % 4 !== 0) return null;
|
|
20603
|
+
return {
|
|
20604
|
+
base64: input.embeddingBase64,
|
|
20605
|
+
dim: vectorDimFromBase64(input.embeddingBase64)
|
|
20606
|
+
};
|
|
20607
|
+
}
|
|
20608
|
+
if (input.embedding.length === 0) return null;
|
|
20609
|
+
return {
|
|
20610
|
+
base64: encodeVectorBase64(input.embedding),
|
|
20611
|
+
dim: input.embedding.length
|
|
20612
|
+
};
|
|
20613
|
+
}
|
|
20578
20614
|
var ObjectEmbeddingStore = class {
|
|
20579
20615
|
vectorStore;
|
|
20580
20616
|
logger;
|
|
@@ -20591,6 +20627,14 @@ var ObjectEmbeddingStore = class {
|
|
|
20591
20627
|
* write: the provider rejects an upsert into an index it has never seen. A
|
|
20592
20628
|
* dim change against a populated index is REFUSED rather than wiping it, so
|
|
20593
20629
|
* this failing is the signal that the encoder model changed.
|
|
20630
|
+
*
|
|
20631
|
+
* `declaredDim` caches the declaration, and that cache is per-PROCESS while
|
|
20632
|
+
* the index lives in whichever addon currently provides `vector-store`. When
|
|
20633
|
+
* the active provider changed under a running hub the cache stayed warm
|
|
20634
|
+
* against an index that no longer existed, and every write from then on threw
|
|
20635
|
+
* `index "object-clip" was never declared` — for hours, until the addon was
|
|
20636
|
+
* restarted by hand. So a failed write clears the cache (see
|
|
20637
|
+
* {@link upsertIfBetter}) and the next one re-declares.
|
|
20594
20638
|
*/
|
|
20595
20639
|
async ensureIndex(dim) {
|
|
20596
20640
|
if (this.declaredDim === dim) return true;
|
|
@@ -20622,33 +20666,50 @@ var ObjectEmbeddingStore = class {
|
|
|
20622
20666
|
* short-circuited, and every later pass silently wrote nothing while
|
|
20623
20667
|
* reporting hundreds rebuilt. Observed live — 214 "rebuilt" moved the index
|
|
20624
20668
|
* by 4 rows.
|
|
20669
|
+
*
|
|
20670
|
+
* ## Why this returns an outcome instead of `void`
|
|
20671
|
+
*
|
|
20672
|
+
* It used to return `void` and turn every failure into a warn. The rebuild
|
|
20673
|
+
* therefore counted a write the instant the call returned, and the second time
|
|
20674
|
+
* this class of bug appeared it produced `rebuilt: 704` against `indexed: 0` —
|
|
20675
|
+
* a progress number that was pure fiction while the real cause sat in a warn
|
|
20676
|
+
* nobody was reading. The caller now has to distinguish "written" from "logged
|
|
20677
|
+
* and dropped", because a counter that cannot reads as verification.
|
|
20625
20678
|
*/
|
|
20626
20679
|
async upsertIfBetter(input) {
|
|
20627
|
-
|
|
20628
|
-
if (
|
|
20680
|
+
const vector = resolveVectorPayload(input);
|
|
20681
|
+
if (vector === null) return "failed";
|
|
20682
|
+
if (!await this.ensureIndex(vector.dim)) return "failed";
|
|
20629
20683
|
try {
|
|
20630
20684
|
if (input.force !== true) {
|
|
20631
20685
|
const stored = (await this.vectorStore.getByIds.query({
|
|
20632
20686
|
index: OBJECT_CLIP_INDEX,
|
|
20633
20687
|
ids: [input.trackId]
|
|
20634
20688
|
})).items[0]?.metadata["confidence"];
|
|
20635
|
-
if (typeof stored === "number" && input.confidence <= stored) return;
|
|
20689
|
+
if (typeof stored === "number" && input.confidence <= stored) return "skipped";
|
|
20636
20690
|
}
|
|
20637
|
-
|
|
20638
|
-
index:
|
|
20691
|
+
const result = await this.vectorStore.upsert.mutate({
|
|
20692
|
+
index: OBJECT_CLIP_INDEX,
|
|
20639
20693
|
items: [{
|
|
20640
20694
|
id: input.trackId,
|
|
20641
|
-
vector:
|
|
20695
|
+
vector: vector.base64,
|
|
20642
20696
|
metadata: metadataFor(input)
|
|
20643
20697
|
}]
|
|
20644
|
-
})).rejected > 0) this.logger.warn("object-embedding upsert rejected", {
|
|
20645
|
-
tags: { deviceId: input.deviceId },
|
|
20646
|
-
meta: {
|
|
20647
|
-
trackId: input.trackId,
|
|
20648
|
-
dim: input.embedding.length
|
|
20649
|
-
}
|
|
20650
20698
|
});
|
|
20699
|
+
if (result.upserted === 0) {
|
|
20700
|
+
this.logger.warn("object-embedding upsert rejected", {
|
|
20701
|
+
tags: { deviceId: input.deviceId },
|
|
20702
|
+
meta: {
|
|
20703
|
+
trackId: input.trackId,
|
|
20704
|
+
dim: vector.dim,
|
|
20705
|
+
rejected: result.rejected
|
|
20706
|
+
}
|
|
20707
|
+
});
|
|
20708
|
+
return "failed";
|
|
20709
|
+
}
|
|
20710
|
+
return "persisted";
|
|
20651
20711
|
} catch (err) {
|
|
20712
|
+
this.declaredDim = null;
|
|
20652
20713
|
this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", {
|
|
20653
20714
|
tags: { deviceId: input.deviceId },
|
|
20654
20715
|
meta: {
|
|
@@ -20656,6 +20717,7 @@ var ObjectEmbeddingStore = class {
|
|
|
20656
20717
|
error: String(err)
|
|
20657
20718
|
}
|
|
20658
20719
|
});
|
|
20720
|
+
return "failed";
|
|
20659
20721
|
}
|
|
20660
20722
|
}
|
|
20661
20723
|
/**
|
|
@@ -27005,6 +27067,43 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27005
27067
|
return info.modelId;
|
|
27006
27068
|
}
|
|
27007
27069
|
/**
|
|
27070
|
+
* Re-embed ONE stored track by running its `clip-embedding` step through the
|
|
27071
|
+
* camera's runner.
|
|
27072
|
+
*
|
|
27073
|
+
* The whole point: the runner cuts. This method reads the key frame's pixel
|
|
27074
|
+
* dimensions (metadata only — no pixel work, no rectangle chosen here) so the
|
|
27075
|
+
* stored normalised box can be expressed in the key frame's own pixels, ships
|
|
27076
|
+
* the full frame, and reads the vector back. Where the crop boundary falls is
|
|
27077
|
+
* decided inside `runDetailSubtree` by the one cluster-wide convention, the
|
|
27078
|
+
* same call the live detail plane makes.
|
|
27079
|
+
*
|
|
27080
|
+
* NOT node-pinned: `runDetailSubtree` is device-bound, so the cap router
|
|
27081
|
+
* sends it to the runner that owns this camera. A pin would be a guess about
|
|
27082
|
+
* placement that the binding already knows.
|
|
27083
|
+
*/
|
|
27084
|
+
async embedRebuildSourceViaPipeline(source) {
|
|
27085
|
+
if (!hasKeyFrame(source)) return {
|
|
27086
|
+
kind: "failed",
|
|
27087
|
+
reason: "no key frame or no bbox"
|
|
27088
|
+
};
|
|
27089
|
+
const runner = this.ctx.api?.pipelineRunner;
|
|
27090
|
+
if (!runner?.runDetailSubtree) return {
|
|
27091
|
+
kind: "not-runnable",
|
|
27092
|
+
reason: "pipelineRunner.runDetailSubtree is unavailable on this cluster"
|
|
27093
|
+
};
|
|
27094
|
+
const sharp = (await import("sharp")).default;
|
|
27095
|
+
const meta = await sharp(source.keyFrame).metadata();
|
|
27096
|
+
if (meta.width === void 0 || meta.height === void 0) return {
|
|
27097
|
+
kind: "failed",
|
|
27098
|
+
reason: "key frame has no readable dimensions"
|
|
27099
|
+
};
|
|
27100
|
+
const input = buildRebuildDetailInput(source, {
|
|
27101
|
+
width: meta.width,
|
|
27102
|
+
height: meta.height
|
|
27103
|
+
});
|
|
27104
|
+
return readRebuildEmbedding(await runner.runDetailSubtree.mutate(input));
|
|
27105
|
+
}
|
|
27106
|
+
/**
|
|
27008
27107
|
* Every track id the track store still holds, across the given devices.
|
|
27009
27108
|
*
|
|
27010
27109
|
* Returned as a Set for the orphan sweep, which REFUSES an empty one — a
|
|
@@ -27043,6 +27142,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27043
27142
|
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
27044
27143
|
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
27045
27144
|
* widened after the fact.
|
|
27145
|
+
*
|
|
27146
|
+
* The frame and the box are shipped to the camera's RUNNER, which cuts and
|
|
27147
|
+
* encodes them through `runDetailSubtree` — the same code path, the same
|
|
27148
|
+
* crop convention and the same step catalog the live detail plane uses. This
|
|
27149
|
+
* side no longer chooses a rectangle, and there is no `cropMargin` argument
|
|
27150
|
+
* to choose one with: see `embedding-rebuild.ts` for what that cost.
|
|
27046
27151
|
*/
|
|
27047
27152
|
async rebuildObjectEmbeddings(input) {
|
|
27048
27153
|
if (this.rebuildState.running) {
|
|
@@ -27100,27 +27205,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27100
27205
|
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
27101
27206
|
};
|
|
27102
27207
|
},
|
|
27103
|
-
|
|
27104
|
-
|
|
27105
|
-
|
|
27106
|
-
|
|
27107
|
-
|
|
27108
|
-
|
|
27109
|
-
|
|
27110
|
-
|
|
27111
|
-
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27116
|
-
embedding,
|
|
27117
|
-
modelId,
|
|
27118
|
-
confidence: source.confidence,
|
|
27119
|
-
force: true,
|
|
27120
|
-
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
27121
|
-
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
27122
|
-
});
|
|
27123
|
-
},
|
|
27208
|
+
embed: (source) => this.embedRebuildSourceViaPipeline(source),
|
|
27209
|
+
store: async (source, embeddingBase64) => store.upsertIfBetter({
|
|
27210
|
+
trackId: source.trackId,
|
|
27211
|
+
deviceId: source.deviceId,
|
|
27212
|
+
timestamp: source.timestamp,
|
|
27213
|
+
className: source.className,
|
|
27214
|
+
embeddingBase64,
|
|
27215
|
+
modelId,
|
|
27216
|
+
confidence: source.confidence,
|
|
27217
|
+
force: true,
|
|
27218
|
+
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
27219
|
+
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
27220
|
+
}),
|
|
27124
27221
|
logger: {
|
|
27125
27222
|
info: (message, extras) => this.ctx.logger.info(message, extras),
|
|
27126
27223
|
warn: (message, extras) => this.ctx.logger.warn(message, extras)
|
|
@@ -27134,12 +27231,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27134
27231
|
rebuilt: 0,
|
|
27135
27232
|
missingKeyFrame: 0,
|
|
27136
27233
|
missingBbox: 0,
|
|
27234
|
+
notRunnable: 0,
|
|
27137
27235
|
failed: 0
|
|
27138
27236
|
}
|
|
27139
27237
|
};
|
|
27140
27238
|
rebuildObjectEmbeddings(deps, {
|
|
27141
|
-
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
27142
|
-
...input.square !== void 0 ? { square: input.square } : {},
|
|
27143
27239
|
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
|
|
27144
27240
|
onProgress: (progress) => {
|
|
27145
27241
|
if (this.rebuildState.running) this.rebuildState.progress = progress;
|
|
@@ -27164,7 +27260,6 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27164
27260
|
});
|
|
27165
27261
|
this.ctx.logger.info("object embedding rebuild started", { meta: {
|
|
27166
27262
|
deviceId: input.deviceId ?? "all",
|
|
27167
|
-
cropMargin: input.cropMargin ?? "default",
|
|
27168
27263
|
maxTracks: input.maxTracks ?? "unbounded"
|
|
27169
27264
|
} });
|
|
27170
27265
|
return {
|
|
@@ -27206,6 +27301,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27206
27301
|
rebuilt: p?.rebuilt ?? 0,
|
|
27207
27302
|
missingKeyFrame: p?.missingKeyFrame ?? 0,
|
|
27208
27303
|
missingBbox: p?.missingBbox ?? 0,
|
|
27304
|
+
notRunnable: p?.notRunnable ?? 0,
|
|
27209
27305
|
failed: p?.failed ?? 0,
|
|
27210
27306
|
complete: state.complete ?? null,
|
|
27211
27307
|
startedAtMs: state.startedAtMs ?? null,
|
|
@@ -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-Cqisi0Ci.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.47",
|
|
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",
|
|
@@ -87,6 +87,12 @@
|
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "videoclips"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "face-gallery"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "plate-gallery"
|
|
90
96
|
}
|
|
91
97
|
],
|
|
92
98
|
"storageLocations": [
|