@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-B5O4mNmd.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_dist.__toESM(node_path);
|
|
@@ -5845,14 +5845,6 @@ function makeNcActionHandlers(deps) {
|
|
|
5845
5845
|
var RebuildInputSchema = require_dist.object({
|
|
5846
5846
|
/** Restrict to one camera; omit for the whole fleet. */
|
|
5847
5847
|
deviceId: require_dist.number().optional(),
|
|
5848
|
-
/**
|
|
5849
|
-
* Fraction added on EACH side of the detection box before cropping.
|
|
5850
|
-
* ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
|
|
5851
|
-
* the pixel-tight crop.
|
|
5852
|
-
*/
|
|
5853
|
-
cropMargin: require_dist.number().min(0).max(4).optional(),
|
|
5854
|
-
/** Square the window so a tall subject is not squashed into CLIP's input. */
|
|
5855
|
-
square: require_dist.boolean().optional(),
|
|
5856
5848
|
/** Stop after this many tracks; the status reports whether more remain. */
|
|
5857
5849
|
maxTracks: require_dist.number().int().positive().optional()
|
|
5858
5850
|
});
|
|
@@ -5866,6 +5858,8 @@ var RebuildStatusSchema = require_dist.object({
|
|
|
5866
5858
|
rebuilt: require_dist.number(),
|
|
5867
5859
|
missingKeyFrame: require_dist.number(),
|
|
5868
5860
|
missingBbox: require_dist.number(),
|
|
5861
|
+
/** Refused by the pipeline (camera detached / step disabled), not broken. */
|
|
5862
|
+
notRunnable: require_dist.number(),
|
|
5869
5863
|
failed: require_dist.number(),
|
|
5870
5864
|
complete: require_dist.boolean().nullable(),
|
|
5871
5865
|
startedAtMs: require_dist.number().nullable(),
|
|
@@ -12024,88 +12018,6 @@ var EmbeddingDispatcher = class {
|
|
|
12024
12018
|
}
|
|
12025
12019
|
}
|
|
12026
12020
|
};
|
|
12027
|
-
//#endregion
|
|
12028
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
12029
|
-
/**
|
|
12030
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
12031
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
12032
|
-
*
|
|
12033
|
-
* Formula (before clamping):
|
|
12034
|
-
* newX = x - padding * w
|
|
12035
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
12036
|
-
* newY = y - padding * h
|
|
12037
|
-
* newH = h + 2 * padding * h
|
|
12038
|
-
*
|
|
12039
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
12040
|
-
* new object.
|
|
12041
|
-
*
|
|
12042
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
12043
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
12044
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
12045
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
12046
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
12047
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
12048
|
-
*/
|
|
12049
|
-
function padBbox(bbox, padding) {
|
|
12050
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
12051
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
12052
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
12053
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
12054
|
-
const w = Math.min(rawW, 1);
|
|
12055
|
-
const h = Math.min(rawH, 1);
|
|
12056
|
-
return {
|
|
12057
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
12058
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
12059
|
-
w,
|
|
12060
|
-
h
|
|
12061
|
-
};
|
|
12062
|
-
}
|
|
12063
|
-
/**
|
|
12064
|
-
* Widen a bbox and optionally square it, staying inside the unit frame.
|
|
12065
|
-
*
|
|
12066
|
-
* Squaring uses the frame's aspect so the window is square in PIXELS, not in
|
|
12067
|
-
* normalised units — those are only the same on a square frame, and no camera
|
|
12068
|
-
* produces one.
|
|
12069
|
-
*/
|
|
12070
|
-
function cropWindow(bbox, frameWidth, frameHeight, options) {
|
|
12071
|
-
const padded = padBbox(bbox, options.margin);
|
|
12072
|
-
if (!options.square) return padded;
|
|
12073
|
-
const pxW = padded.w * frameWidth;
|
|
12074
|
-
const pxH = padded.h * frameHeight;
|
|
12075
|
-
const side = Math.max(pxW, pxH);
|
|
12076
|
-
const w = Math.min(side / frameWidth, 1);
|
|
12077
|
-
const h = Math.min(side / frameHeight, 1);
|
|
12078
|
-
const cx = padded.x + padded.w / 2;
|
|
12079
|
-
const cy = padded.y + padded.h / 2;
|
|
12080
|
-
return {
|
|
12081
|
-
w,
|
|
12082
|
-
h,
|
|
12083
|
-
x: Math.min(Math.max(0, cx - w / 2), 1 - w),
|
|
12084
|
-
y: Math.min(Math.max(0, cy - h / 2), 1 - h)
|
|
12085
|
-
};
|
|
12086
|
-
}
|
|
12087
|
-
/** Cut a JPEG crop out of a JPEG frame using a normalised window. */
|
|
12088
|
-
async function cropFromJpeg(jpeg, window) {
|
|
12089
|
-
const image = (0, sharp.default)(jpeg);
|
|
12090
|
-
const meta = await image.metadata();
|
|
12091
|
-
const frameWidth = meta.width;
|
|
12092
|
-
const frameHeight = meta.height;
|
|
12093
|
-
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12094
|
-
const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
|
|
12095
|
-
const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
|
|
12096
|
-
const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
|
|
12097
|
-
const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
|
|
12098
|
-
return {
|
|
12099
|
-
crop: await image.extract({
|
|
12100
|
-
left,
|
|
12101
|
-
top,
|
|
12102
|
-
width,
|
|
12103
|
-
height
|
|
12104
|
-
}).jpeg({ quality: 90 }).toBuffer(),
|
|
12105
|
-
width,
|
|
12106
|
-
height
|
|
12107
|
-
};
|
|
12108
|
-
}
|
|
12109
12021
|
/**
|
|
12110
12022
|
* Re-embed every listed track from its key frame.
|
|
12111
12023
|
*
|
|
@@ -12114,12 +12026,11 @@ async function cropFromJpeg(jpeg, window) {
|
|
|
12114
12026
|
* knowing how much was not rebuilt, not just how much was.
|
|
12115
12027
|
*/
|
|
12116
12028
|
async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
12117
|
-
const margin = options.cropMargin ?? .2;
|
|
12118
|
-
const square = options.square ?? true;
|
|
12119
12029
|
const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
|
|
12120
12030
|
let rebuilt = 0;
|
|
12121
12031
|
let missingKeyFrame = 0;
|
|
12122
12032
|
let missingBbox = 0;
|
|
12033
|
+
let notRunnable = 0;
|
|
12123
12034
|
let failed = 0;
|
|
12124
12035
|
let scanned = 0;
|
|
12125
12036
|
let cursor = null;
|
|
@@ -12150,27 +12061,41 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12150
12061
|
missingBbox += 1;
|
|
12151
12062
|
continue;
|
|
12152
12063
|
}
|
|
12153
|
-
const
|
|
12154
|
-
if (
|
|
12155
|
-
|
|
12064
|
+
const outcome = await deps.embed(source);
|
|
12065
|
+
if (outcome.kind === "not-runnable") {
|
|
12066
|
+
notRunnable += 1;
|
|
12067
|
+
deps.logger.warn("embedding rebuild: track could not be re-embedded by the pipeline", {
|
|
12068
|
+
tags: { deviceId: source.deviceId },
|
|
12069
|
+
meta: {
|
|
12070
|
+
trackId,
|
|
12071
|
+
reason: outcome.reason
|
|
12072
|
+
}
|
|
12073
|
+
});
|
|
12156
12074
|
continue;
|
|
12157
12075
|
}
|
|
12158
|
-
|
|
12159
|
-
margin,
|
|
12160
|
-
square
|
|
12161
|
-
});
|
|
12162
|
-
const cut = await cropFromJpeg(source.keyFrame, window);
|
|
12163
|
-
if (cut === null) {
|
|
12076
|
+
if (outcome.kind === "failed") {
|
|
12164
12077
|
failed += 1;
|
|
12078
|
+
deps.logger.warn("embedding rebuild: the pipeline returned no vector for a track", {
|
|
12079
|
+
tags: { deviceId: source.deviceId },
|
|
12080
|
+
meta: {
|
|
12081
|
+
trackId,
|
|
12082
|
+
reason: outcome.reason
|
|
12083
|
+
}
|
|
12084
|
+
});
|
|
12165
12085
|
continue;
|
|
12166
12086
|
}
|
|
12167
|
-
const
|
|
12168
|
-
if (
|
|
12087
|
+
const written = await deps.store(source, outcome.embeddingBase64);
|
|
12088
|
+
if (written === "persisted") rebuilt += 1;
|
|
12089
|
+
else {
|
|
12169
12090
|
failed += 1;
|
|
12170
|
-
|
|
12091
|
+
deps.logger.warn("embedding rebuild wrote nothing for a track", {
|
|
12092
|
+
tags: { deviceId: source.deviceId },
|
|
12093
|
+
meta: {
|
|
12094
|
+
trackId,
|
|
12095
|
+
outcome: written
|
|
12096
|
+
}
|
|
12097
|
+
});
|
|
12171
12098
|
}
|
|
12172
|
-
await deps.store(source, embedding);
|
|
12173
|
-
rebuilt += 1;
|
|
12174
12099
|
} catch (err) {
|
|
12175
12100
|
failed += 1;
|
|
12176
12101
|
deps.logger.warn("embedding rebuild failed for a track", { meta: {
|
|
@@ -12183,6 +12108,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12183
12108
|
rebuilt,
|
|
12184
12109
|
missingKeyFrame,
|
|
12185
12110
|
missingBbox,
|
|
12111
|
+
notRunnable,
|
|
12186
12112
|
failed
|
|
12187
12113
|
});
|
|
12188
12114
|
}
|
|
@@ -12198,16 +12124,16 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12198
12124
|
rebuilt,
|
|
12199
12125
|
missingKeyFrame,
|
|
12200
12126
|
missingBbox,
|
|
12127
|
+
notRunnable,
|
|
12201
12128
|
failed,
|
|
12202
|
-
complete
|
|
12203
|
-
cropMargin: margin,
|
|
12204
|
-
square
|
|
12129
|
+
complete
|
|
12205
12130
|
} });
|
|
12206
12131
|
return {
|
|
12207
12132
|
scanned,
|
|
12208
12133
|
rebuilt,
|
|
12209
12134
|
missingKeyFrame,
|
|
12210
12135
|
missingBbox,
|
|
12136
|
+
notRunnable,
|
|
12211
12137
|
failed,
|
|
12212
12138
|
complete
|
|
12213
12139
|
};
|
|
@@ -12375,6 +12301,69 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
12375
12301
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
12376
12302
|
}
|
|
12377
12303
|
//#endregion
|
|
12304
|
+
//#region src/pipeline-analytics/rebuild-detail-request.ts
|
|
12305
|
+
/** The one step a rebuild runs. */
|
|
12306
|
+
var REBUILD_DETAIL_STEP_ID = "clip-embedding";
|
|
12307
|
+
/**
|
|
12308
|
+
* Build the `runDetailSubtree` input for one stored track.
|
|
12309
|
+
*
|
|
12310
|
+
* The box is stored NORMALISED because the key frame is a native-resolution
|
|
12311
|
+
* capture while the detection ran on a downscaled analysis frame; the two have
|
|
12312
|
+
* different pixel dimensions. `parent.bbox` is frame pixels (the live path's
|
|
12313
|
+
* convention), so it is denormalised here against the KEY FRAME's own
|
|
12314
|
+
* dimensions — the image the runner is about to cut. This is coordinate
|
|
12315
|
+
* mapping, not crop derivation: no padding, no squaring, no rectangle choice.
|
|
12316
|
+
*/
|
|
12317
|
+
function buildRebuildDetailInput(source, keyFrame) {
|
|
12318
|
+
return {
|
|
12319
|
+
deviceId: source.deviceId,
|
|
12320
|
+
frameJpeg: source.keyFrame.toString("base64"),
|
|
12321
|
+
parent: {
|
|
12322
|
+
bbox: {
|
|
12323
|
+
x: source.bbox.x * keyFrame.width,
|
|
12324
|
+
y: source.bbox.y * keyFrame.height,
|
|
12325
|
+
w: source.bbox.w * keyFrame.width,
|
|
12326
|
+
h: source.bbox.h * keyFrame.height
|
|
12327
|
+
},
|
|
12328
|
+
className: source.className
|
|
12329
|
+
},
|
|
12330
|
+
steps: [REBUILD_DETAIL_STEP_ID]
|
|
12331
|
+
};
|
|
12332
|
+
}
|
|
12333
|
+
/**
|
|
12334
|
+
* Turn one `runDetailSubtree` answer into a rebuild outcome.
|
|
12335
|
+
*
|
|
12336
|
+
* The three cases lead to three different counters:
|
|
12337
|
+
*
|
|
12338
|
+
* - `null` — the runner DECLINED: the camera is not attached, `clip-embedding`
|
|
12339
|
+
* is not enabled in its tree, or the frame was unreadable. It logs which,
|
|
12340
|
+
* tagged with the deviceId. That is `not-runnable`, not a failure: the remedy
|
|
12341
|
+
* is a configuration change, and folding it into `failed` would point the
|
|
12342
|
+
* operator at the inference engine instead.
|
|
12343
|
+
* - details with no `clip-embedding` embedding — the step RAN and produced
|
|
12344
|
+
* nothing usable. That is a failure.
|
|
12345
|
+
* - an embedding — passed through as base64, the form the vector store stores.
|
|
12346
|
+
*/
|
|
12347
|
+
function readRebuildEmbedding(result) {
|
|
12348
|
+
if (result === null) return {
|
|
12349
|
+
kind: "not-runnable",
|
|
12350
|
+
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)"
|
|
12351
|
+
};
|
|
12352
|
+
const embedding = result.details.find((d) => d.stepId === "clip-embedding" && d.embedding !== void 0)?.embedding;
|
|
12353
|
+
if (embedding === void 0) return {
|
|
12354
|
+
kind: "failed",
|
|
12355
|
+
reason: `the detail subtree ran but returned no ${REBUILD_DETAIL_STEP_ID} embedding (${result.details.length} detail(s))`
|
|
12356
|
+
};
|
|
12357
|
+
return {
|
|
12358
|
+
kind: "embedded",
|
|
12359
|
+
embeddingBase64: embedding
|
|
12360
|
+
};
|
|
12361
|
+
}
|
|
12362
|
+
/** Narrowing helper — a source without a key frame can never build a request. */
|
|
12363
|
+
function hasKeyFrame(source) {
|
|
12364
|
+
return source.keyFrame !== null && source.bbox !== null;
|
|
12365
|
+
}
|
|
12366
|
+
//#endregion
|
|
12378
12367
|
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
12379
12368
|
/**
|
|
12380
12369
|
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
@@ -14566,6 +14555,42 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14566
14555
|
};
|
|
14567
14556
|
}
|
|
14568
14557
|
//#endregion
|
|
14558
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
14559
|
+
/**
|
|
14560
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
14561
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
14562
|
+
*
|
|
14563
|
+
* Formula (before clamping):
|
|
14564
|
+
* newX = x - padding * w
|
|
14565
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
14566
|
+
* newY = y - padding * h
|
|
14567
|
+
* newH = h + 2 * padding * h
|
|
14568
|
+
*
|
|
14569
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
14570
|
+
* new object.
|
|
14571
|
+
*
|
|
14572
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
14573
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
14574
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
14575
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
14576
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
14577
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
14578
|
+
*/
|
|
14579
|
+
function padBbox(bbox, padding) {
|
|
14580
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
14581
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
14582
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
14583
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
14584
|
+
const w = Math.min(rawW, 1);
|
|
14585
|
+
const h = Math.min(rawH, 1);
|
|
14586
|
+
return {
|
|
14587
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
14588
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
14589
|
+
w,
|
|
14590
|
+
h
|
|
14591
|
+
};
|
|
14592
|
+
}
|
|
14593
|
+
//#endregion
|
|
14569
14594
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14570
14595
|
function createKeyFrameCrop(deps) {
|
|
14571
14596
|
return async (input) => {
|
|
@@ -18555,30 +18580,18 @@ function buildGlobalSettingsSchema() {
|
|
|
18555
18580
|
{
|
|
18556
18581
|
id: "embedding-index",
|
|
18557
18582
|
title: "Semantic search index",
|
|
18558
|
-
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
|
|
18583
|
+
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.",
|
|
18559
18584
|
columns: 1,
|
|
18560
18585
|
fields: [
|
|
18561
|
-
{
|
|
18562
|
-
type: "number",
|
|
18563
|
-
key: "rebuildCropMargin",
|
|
18564
|
-
label: "Rebuild crop margin",
|
|
18565
|
-
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.",
|
|
18566
|
-
default: .2,
|
|
18567
|
-
min: 0,
|
|
18568
|
-
max: 2,
|
|
18569
|
-
step: .1
|
|
18570
|
-
},
|
|
18571
18586
|
{
|
|
18572
18587
|
type: "addon-action-button",
|
|
18573
18588
|
key: "rebuildEmbeddingsButton",
|
|
18574
18589
|
label: "Rebuild index",
|
|
18575
|
-
description: "Re-embed every stored track from its key frame, using the
|
|
18590
|
+
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.",
|
|
18576
18591
|
addonId: "pipeline-analytics",
|
|
18577
18592
|
action: "embedding.rebuild",
|
|
18578
18593
|
buttonLabel: "Rebuild",
|
|
18579
18594
|
variant: "primary",
|
|
18580
|
-
paramsFromForm: { cropMargin: "rebuildCropMargin" },
|
|
18581
|
-
paramsStatic: { square: true },
|
|
18582
18595
|
successMessage: "Rebuild started — press Progress to follow it."
|
|
18583
18596
|
},
|
|
18584
18597
|
{
|
|
@@ -18589,7 +18602,7 @@ function buildGlobalSettingsSchema() {
|
|
|
18589
18602
|
addonId: "pipeline-analytics",
|
|
18590
18603
|
action: "embedding.rebuildStatus",
|
|
18591
18604
|
buttonLabel: "Check progress",
|
|
18592
|
-
successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · failed {failed}"
|
|
18605
|
+
successMessage: "Indexed {indexed} · scanned {scanned} · rebuilt {rebuilt} · no key frame {missingKeyFrame} · not runnable {notRunnable} · failed {failed}"
|
|
18593
18606
|
},
|
|
18594
18607
|
{
|
|
18595
18608
|
type: "addon-action-button",
|
|
@@ -20581,6 +20594,29 @@ var IdentityStore = class {
|
|
|
20581
20594
|
*/
|
|
20582
20595
|
/** The vector-store index this store owns. */
|
|
20583
20596
|
var OBJECT_CLIP_INDEX = "object-clip";
|
|
20597
|
+
/**
|
|
20598
|
+
* Reduce either input form to the stored form.
|
|
20599
|
+
*
|
|
20600
|
+
* A base64 payload passes through untouched; its dimension is read from the
|
|
20601
|
+
* byte length. A length that is not a whole number of float32s cannot be a
|
|
20602
|
+
* vector, and storing it would produce a row whose dimension silently
|
|
20603
|
+
* disagrees with the declared index.
|
|
20604
|
+
*/
|
|
20605
|
+
function resolveVectorPayload(input) {
|
|
20606
|
+
if (input.embeddingBase64 !== void 0) {
|
|
20607
|
+
const byteLength = Buffer.from(input.embeddingBase64, "base64").byteLength;
|
|
20608
|
+
if (byteLength === 0 || byteLength % 4 !== 0) return null;
|
|
20609
|
+
return {
|
|
20610
|
+
base64: input.embeddingBase64,
|
|
20611
|
+
dim: require_dist.vectorDimFromBase64(input.embeddingBase64)
|
|
20612
|
+
};
|
|
20613
|
+
}
|
|
20614
|
+
if (input.embedding.length === 0) return null;
|
|
20615
|
+
return {
|
|
20616
|
+
base64: require_dist.encodeVectorBase64(input.embedding),
|
|
20617
|
+
dim: input.embedding.length
|
|
20618
|
+
};
|
|
20619
|
+
}
|
|
20584
20620
|
var ObjectEmbeddingStore = class {
|
|
20585
20621
|
vectorStore;
|
|
20586
20622
|
logger;
|
|
@@ -20597,6 +20633,14 @@ var ObjectEmbeddingStore = class {
|
|
|
20597
20633
|
* write: the provider rejects an upsert into an index it has never seen. A
|
|
20598
20634
|
* dim change against a populated index is REFUSED rather than wiping it, so
|
|
20599
20635
|
* this failing is the signal that the encoder model changed.
|
|
20636
|
+
*
|
|
20637
|
+
* `declaredDim` caches the declaration, and that cache is per-PROCESS while
|
|
20638
|
+
* the index lives in whichever addon currently provides `vector-store`. When
|
|
20639
|
+
* the active provider changed under a running hub the cache stayed warm
|
|
20640
|
+
* against an index that no longer existed, and every write from then on threw
|
|
20641
|
+
* `index "object-clip" was never declared` — for hours, until the addon was
|
|
20642
|
+
* restarted by hand. So a failed write clears the cache (see
|
|
20643
|
+
* {@link upsertIfBetter}) and the next one re-declares.
|
|
20600
20644
|
*/
|
|
20601
20645
|
async ensureIndex(dim) {
|
|
20602
20646
|
if (this.declaredDim === dim) return true;
|
|
@@ -20628,33 +20672,50 @@ var ObjectEmbeddingStore = class {
|
|
|
20628
20672
|
* short-circuited, and every later pass silently wrote nothing while
|
|
20629
20673
|
* reporting hundreds rebuilt. Observed live — 214 "rebuilt" moved the index
|
|
20630
20674
|
* by 4 rows.
|
|
20675
|
+
*
|
|
20676
|
+
* ## Why this returns an outcome instead of `void`
|
|
20677
|
+
*
|
|
20678
|
+
* It used to return `void` and turn every failure into a warn. The rebuild
|
|
20679
|
+
* therefore counted a write the instant the call returned, and the second time
|
|
20680
|
+
* this class of bug appeared it produced `rebuilt: 704` against `indexed: 0` —
|
|
20681
|
+
* a progress number that was pure fiction while the real cause sat in a warn
|
|
20682
|
+
* nobody was reading. The caller now has to distinguish "written" from "logged
|
|
20683
|
+
* and dropped", because a counter that cannot reads as verification.
|
|
20631
20684
|
*/
|
|
20632
20685
|
async upsertIfBetter(input) {
|
|
20633
|
-
|
|
20634
|
-
if (
|
|
20686
|
+
const vector = resolveVectorPayload(input);
|
|
20687
|
+
if (vector === null) return "failed";
|
|
20688
|
+
if (!await this.ensureIndex(vector.dim)) return "failed";
|
|
20635
20689
|
try {
|
|
20636
20690
|
if (input.force !== true) {
|
|
20637
20691
|
const stored = (await this.vectorStore.getByIds.query({
|
|
20638
20692
|
index: OBJECT_CLIP_INDEX,
|
|
20639
20693
|
ids: [input.trackId]
|
|
20640
20694
|
})).items[0]?.metadata["confidence"];
|
|
20641
|
-
if (typeof stored === "number" && input.confidence <= stored) return;
|
|
20695
|
+
if (typeof stored === "number" && input.confidence <= stored) return "skipped";
|
|
20642
20696
|
}
|
|
20643
|
-
|
|
20644
|
-
index:
|
|
20697
|
+
const result = await this.vectorStore.upsert.mutate({
|
|
20698
|
+
index: OBJECT_CLIP_INDEX,
|
|
20645
20699
|
items: [{
|
|
20646
20700
|
id: input.trackId,
|
|
20647
|
-
vector:
|
|
20701
|
+
vector: vector.base64,
|
|
20648
20702
|
metadata: metadataFor(input)
|
|
20649
20703
|
}]
|
|
20650
|
-
})).rejected > 0) this.logger.warn("object-embedding upsert rejected", {
|
|
20651
|
-
tags: { deviceId: input.deviceId },
|
|
20652
|
-
meta: {
|
|
20653
|
-
trackId: input.trackId,
|
|
20654
|
-
dim: input.embedding.length
|
|
20655
|
-
}
|
|
20656
20704
|
});
|
|
20705
|
+
if (result.upserted === 0) {
|
|
20706
|
+
this.logger.warn("object-embedding upsert rejected", {
|
|
20707
|
+
tags: { deviceId: input.deviceId },
|
|
20708
|
+
meta: {
|
|
20709
|
+
trackId: input.trackId,
|
|
20710
|
+
dim: vector.dim,
|
|
20711
|
+
rejected: result.rejected
|
|
20712
|
+
}
|
|
20713
|
+
});
|
|
20714
|
+
return "failed";
|
|
20715
|
+
}
|
|
20716
|
+
return "persisted";
|
|
20657
20717
|
} catch (err) {
|
|
20718
|
+
this.declaredDim = null;
|
|
20658
20719
|
this.logger.warn("ObjectEmbeddingStore.upsertIfBetter failed", {
|
|
20659
20720
|
tags: { deviceId: input.deviceId },
|
|
20660
20721
|
meta: {
|
|
@@ -20662,6 +20723,7 @@ var ObjectEmbeddingStore = class {
|
|
|
20662
20723
|
error: String(err)
|
|
20663
20724
|
}
|
|
20664
20725
|
});
|
|
20726
|
+
return "failed";
|
|
20665
20727
|
}
|
|
20666
20728
|
}
|
|
20667
20729
|
/**
|
|
@@ -23947,7 +24009,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
23947
24009
|
let storage = this.ctx.kernel.storage;
|
|
23948
24010
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
23949
24011
|
if (mediaRoot) {
|
|
23950
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
24012
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-m6HS7TOO.js"));
|
|
23951
24013
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
23952
24014
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
23953
24015
|
}
|
|
@@ -27011,6 +27073,43 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27011
27073
|
return info.modelId;
|
|
27012
27074
|
}
|
|
27013
27075
|
/**
|
|
27076
|
+
* Re-embed ONE stored track by running its `clip-embedding` step through the
|
|
27077
|
+
* camera's runner.
|
|
27078
|
+
*
|
|
27079
|
+
* The whole point: the runner cuts. This method reads the key frame's pixel
|
|
27080
|
+
* dimensions (metadata only — no pixel work, no rectangle chosen here) so the
|
|
27081
|
+
* stored normalised box can be expressed in the key frame's own pixels, ships
|
|
27082
|
+
* the full frame, and reads the vector back. Where the crop boundary falls is
|
|
27083
|
+
* decided inside `runDetailSubtree` by the one cluster-wide convention, the
|
|
27084
|
+
* same call the live detail plane makes.
|
|
27085
|
+
*
|
|
27086
|
+
* NOT node-pinned: `runDetailSubtree` is device-bound, so the cap router
|
|
27087
|
+
* sends it to the runner that owns this camera. A pin would be a guess about
|
|
27088
|
+
* placement that the binding already knows.
|
|
27089
|
+
*/
|
|
27090
|
+
async embedRebuildSourceViaPipeline(source) {
|
|
27091
|
+
if (!hasKeyFrame(source)) return {
|
|
27092
|
+
kind: "failed",
|
|
27093
|
+
reason: "no key frame or no bbox"
|
|
27094
|
+
};
|
|
27095
|
+
const runner = this.ctx.api?.pipelineRunner;
|
|
27096
|
+
if (!runner?.runDetailSubtree) return {
|
|
27097
|
+
kind: "not-runnable",
|
|
27098
|
+
reason: "pipelineRunner.runDetailSubtree is unavailable on this cluster"
|
|
27099
|
+
};
|
|
27100
|
+
const sharp$7 = (await import("sharp")).default;
|
|
27101
|
+
const meta = await sharp$7(source.keyFrame).metadata();
|
|
27102
|
+
if (meta.width === void 0 || meta.height === void 0) return {
|
|
27103
|
+
kind: "failed",
|
|
27104
|
+
reason: "key frame has no readable dimensions"
|
|
27105
|
+
};
|
|
27106
|
+
const input = buildRebuildDetailInput(source, {
|
|
27107
|
+
width: meta.width,
|
|
27108
|
+
height: meta.height
|
|
27109
|
+
});
|
|
27110
|
+
return readRebuildEmbedding(await runner.runDetailSubtree.mutate(input));
|
|
27111
|
+
}
|
|
27112
|
+
/**
|
|
27014
27113
|
* Every track id the track store still holds, across the given devices.
|
|
27015
27114
|
*
|
|
27016
27115
|
* Returned as a Set for the orphan sweep, which REFUSES an empty one — a
|
|
@@ -27049,6 +27148,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27049
27148
|
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
27050
27149
|
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
27051
27150
|
* widened after the fact.
|
|
27151
|
+
*
|
|
27152
|
+
* The frame and the box are shipped to the camera's RUNNER, which cuts and
|
|
27153
|
+
* encodes them through `runDetailSubtree` — the same code path, the same
|
|
27154
|
+
* crop convention and the same step catalog the live detail plane uses. This
|
|
27155
|
+
* side no longer chooses a rectangle, and there is no `cropMargin` argument
|
|
27156
|
+
* to choose one with: see `embedding-rebuild.ts` for what that cost.
|
|
27052
27157
|
*/
|
|
27053
27158
|
async rebuildObjectEmbeddings(input) {
|
|
27054
27159
|
if (this.rebuildState.running) {
|
|
@@ -27106,27 +27211,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27106
27211
|
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
27107
27212
|
};
|
|
27108
27213
|
},
|
|
27109
|
-
|
|
27110
|
-
|
|
27111
|
-
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27116
|
-
|
|
27117
|
-
|
|
27118
|
-
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
embedding,
|
|
27123
|
-
modelId,
|
|
27124
|
-
confidence: source.confidence,
|
|
27125
|
-
force: true,
|
|
27126
|
-
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
27127
|
-
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
27128
|
-
});
|
|
27129
|
-
},
|
|
27214
|
+
embed: (source) => this.embedRebuildSourceViaPipeline(source),
|
|
27215
|
+
store: async (source, embeddingBase64) => store.upsertIfBetter({
|
|
27216
|
+
trackId: source.trackId,
|
|
27217
|
+
deviceId: source.deviceId,
|
|
27218
|
+
timestamp: source.timestamp,
|
|
27219
|
+
className: source.className,
|
|
27220
|
+
embeddingBase64,
|
|
27221
|
+
modelId,
|
|
27222
|
+
confidence: source.confidence,
|
|
27223
|
+
force: true,
|
|
27224
|
+
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
27225
|
+
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
27226
|
+
}),
|
|
27130
27227
|
logger: {
|
|
27131
27228
|
info: (message, extras) => this.ctx.logger.info(message, extras),
|
|
27132
27229
|
warn: (message, extras) => this.ctx.logger.warn(message, extras)
|
|
@@ -27140,12 +27237,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27140
27237
|
rebuilt: 0,
|
|
27141
27238
|
missingKeyFrame: 0,
|
|
27142
27239
|
missingBbox: 0,
|
|
27240
|
+
notRunnable: 0,
|
|
27143
27241
|
failed: 0
|
|
27144
27242
|
}
|
|
27145
27243
|
};
|
|
27146
27244
|
rebuildObjectEmbeddings(deps, {
|
|
27147
|
-
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
27148
|
-
...input.square !== void 0 ? { square: input.square } : {},
|
|
27149
27245
|
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
|
|
27150
27246
|
onProgress: (progress) => {
|
|
27151
27247
|
if (this.rebuildState.running) this.rebuildState.progress = progress;
|
|
@@ -27170,7 +27266,6 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27170
27266
|
});
|
|
27171
27267
|
this.ctx.logger.info("object embedding rebuild started", { meta: {
|
|
27172
27268
|
deviceId: input.deviceId ?? "all",
|
|
27173
|
-
cropMargin: input.cropMargin ?? "default",
|
|
27174
27269
|
maxTracks: input.maxTracks ?? "unbounded"
|
|
27175
27270
|
} });
|
|
27176
27271
|
return {
|
|
@@ -27212,6 +27307,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27212
27307
|
rebuilt: p?.rebuilt ?? 0,
|
|
27213
27308
|
missingKeyFrame: p?.missingKeyFrame ?? 0,
|
|
27214
27309
|
missingBbox: p?.missingBbox ?? 0,
|
|
27310
|
+
notRunnable: p?.notRunnable ?? 0,
|
|
27215
27311
|
failed: p?.failed ?? 0,
|
|
27216
27312
|
complete: state.complete ?? null,
|
|
27217
27313
|
startedAtMs: state.startedAtMs ?? null,
|