@camstack/addon-post-analysis 1.2.46 → 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 +214 -148
- package/dist/pipeline-analytics/index.mjs +213 -147
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -15508,14 +15508,6 @@ var RebuildObjectEmbeddingsInput = object({
|
|
|
15508
15508
|
deviceId: number().optional(),
|
|
15509
15509
|
since: number().optional(),
|
|
15510
15510
|
until: number().optional(),
|
|
15511
|
-
/**
|
|
15512
|
-
* Fraction added on EACH side of the detection box before cropping.
|
|
15513
|
-
* ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
|
|
15514
|
-
* the pixel-tight crop the first implementation used.
|
|
15515
|
-
*/
|
|
15516
|
-
cropMargin: number().min(0).max(4).optional(),
|
|
15517
|
-
/** Square the window before extracting, so a tall subject is not squashed. */
|
|
15518
|
-
square: boolean().optional(),
|
|
15519
15511
|
/** Stop after this many tracks; the result reports whether more remain. */
|
|
15520
15512
|
maxTracks: number().int().positive().optional()
|
|
15521
15513
|
});
|
|
@@ -15550,6 +15542,14 @@ var RebuildStatusSchema = object({
|
|
|
15550
15542
|
missingKeyFrame: number(),
|
|
15551
15543
|
/** Tracks with no usable detection box. */
|
|
15552
15544
|
missingBbox: number(),
|
|
15545
|
+
/**
|
|
15546
|
+
* Tracks the pipeline REFUSED rather than broke on: the camera is not
|
|
15547
|
+
* attached, or `clip-embedding` is not enabled in its step tree. Separate
|
|
15548
|
+
* from `failed` because the remedy is a configuration change, not an engine
|
|
15549
|
+
* investigation — and because a pass over decommissioned cameras would
|
|
15550
|
+
* otherwise read as a total engine outage.
|
|
15551
|
+
*/
|
|
15552
|
+
notRunnable: number(),
|
|
15553
15553
|
failed: number(),
|
|
15554
15554
|
/** Set once a pass ends: true only when EVERYTHING was covered. */
|
|
15555
15555
|
complete: boolean().nullable(),
|
|
@@ -16800,6 +16800,17 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
16800
16800
|
}), NativeCropResultSchema.nullable()), method(object({
|
|
16801
16801
|
deviceId: number(),
|
|
16802
16802
|
frameHandle: FrameHandleSchema.optional(),
|
|
16803
|
+
/**
|
|
16804
|
+
* FULL FRAME (base64 JPEG). The runner derives the crop rectangle from
|
|
16805
|
+
* `parent.bbox` with the cluster crop convention and cuts it itself —
|
|
16806
|
+
* do NOT pre-crop for this field, that is what `cropJpeg` is.
|
|
16807
|
+
*/
|
|
16808
|
+
frameJpeg: string().optional(),
|
|
16809
|
+
/**
|
|
16810
|
+
* PRE-CUT tile (base64 JPEG), used verbatim — NO padding is applied.
|
|
16811
|
+
* The fallback when the lease/session backing the frame is gone and the
|
|
16812
|
+
* caller already holds a crop.
|
|
16813
|
+
*/
|
|
16803
16814
|
cropJpeg: string().optional(),
|
|
16804
16815
|
parent: DetailParentSchema,
|
|
16805
16816
|
steps: array(string()).optional()
|
|
@@ -31391,6 +31402,34 @@ function encodeVectorBase64(vector) {
|
|
|
31391
31402
|
for (let i = 0; i < floats.length; i += 1) view.setFloat32(i * 4, floats[i] ?? 0, true);
|
|
31392
31403
|
return Buffer.from(bytes).toString("base64");
|
|
31393
31404
|
}
|
|
31405
|
+
/** Vector length implied by a base64 payload, without decoding it. */
|
|
31406
|
+
function vectorDimFromBase64(encoded) {
|
|
31407
|
+
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
31408
|
+
}
|
|
31409
|
+
object({
|
|
31410
|
+
/**
|
|
31411
|
+
* Fraction of the box's own size added on EACH side before cutting.
|
|
31412
|
+
*
|
|
31413
|
+
* CLIP is trained on natural images WITH surroundings; a pixel-tight crop
|
|
31414
|
+
* removes exactly the context it is strongest on (a dog cut to its outline
|
|
31415
|
+
* is a dark blob). The right value is an empirical question, which is why it
|
|
31416
|
+
* is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
|
|
31417
|
+
*/
|
|
31418
|
+
paddingRatio: number().min(0).max(4),
|
|
31419
|
+
/**
|
|
31420
|
+
* Square the window (in PIXELS) before cutting.
|
|
31421
|
+
*
|
|
31422
|
+
* CLIP's input is square, so a tall bbox resized straight to NxN is squashed
|
|
31423
|
+
* — a standing person becomes a shape the model never saw. Squaring costs
|
|
31424
|
+
* extra background, which is context the model wants anyway. Off by default
|
|
31425
|
+
* because the live path has never squared and the stored index reflects that.
|
|
31426
|
+
*/
|
|
31427
|
+
square: boolean()
|
|
31428
|
+
});
|
|
31429
|
+
({
|
|
31430
|
+
paddingRatio: .15,
|
|
31431
|
+
square: false
|
|
31432
|
+
}).paddingRatio;
|
|
31394
31433
|
//#endregion
|
|
31395
31434
|
Object.defineProperty(exports, "BaseAddon", {
|
|
31396
31435
|
enumerable: true,
|
|
@@ -31710,6 +31749,12 @@ Object.defineProperty(exports, "unknown", {
|
|
|
31710
31749
|
return unknown;
|
|
31711
31750
|
}
|
|
31712
31751
|
});
|
|
31752
|
+
Object.defineProperty(exports, "vectorDimFromBase64", {
|
|
31753
|
+
enumerable: true,
|
|
31754
|
+
get: function() {
|
|
31755
|
+
return vectorDimFromBase64;
|
|
31756
|
+
}
|
|
31757
|
+
});
|
|
31713
31758
|
Object.defineProperty(exports, "videoclipsCapability", {
|
|
31714
31759
|
enumerable: true,
|
|
31715
31760
|
get: function() {
|
|
@@ -15486,14 +15486,6 @@ var RebuildObjectEmbeddingsInput = object({
|
|
|
15486
15486
|
deviceId: number().optional(),
|
|
15487
15487
|
since: number().optional(),
|
|
15488
15488
|
until: number().optional(),
|
|
15489
|
-
/**
|
|
15490
|
-
* Fraction added on EACH side of the detection box before cropping.
|
|
15491
|
-
* ~0.2 (a 1.4x window) gives CLIP the surroundings it is trained on; 0 is
|
|
15492
|
-
* the pixel-tight crop the first implementation used.
|
|
15493
|
-
*/
|
|
15494
|
-
cropMargin: number().min(0).max(4).optional(),
|
|
15495
|
-
/** Square the window before extracting, so a tall subject is not squashed. */
|
|
15496
|
-
square: boolean().optional(),
|
|
15497
15489
|
/** Stop after this many tracks; the result reports whether more remain. */
|
|
15498
15490
|
maxTracks: number().int().positive().optional()
|
|
15499
15491
|
});
|
|
@@ -15528,6 +15520,14 @@ var RebuildStatusSchema = object({
|
|
|
15528
15520
|
missingKeyFrame: number(),
|
|
15529
15521
|
/** Tracks with no usable detection box. */
|
|
15530
15522
|
missingBbox: number(),
|
|
15523
|
+
/**
|
|
15524
|
+
* Tracks the pipeline REFUSED rather than broke on: the camera is not
|
|
15525
|
+
* attached, or `clip-embedding` is not enabled in its step tree. Separate
|
|
15526
|
+
* from `failed` because the remedy is a configuration change, not an engine
|
|
15527
|
+
* investigation — and because a pass over decommissioned cameras would
|
|
15528
|
+
* otherwise read as a total engine outage.
|
|
15529
|
+
*/
|
|
15530
|
+
notRunnable: number(),
|
|
15531
15531
|
failed: number(),
|
|
15532
15532
|
/** Set once a pass ends: true only when EVERYTHING was covered. */
|
|
15533
15533
|
complete: boolean().nullable(),
|
|
@@ -16778,6 +16778,17 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
|
|
|
16778
16778
|
}), NativeCropResultSchema.nullable()), method(object({
|
|
16779
16779
|
deviceId: number(),
|
|
16780
16780
|
frameHandle: FrameHandleSchema.optional(),
|
|
16781
|
+
/**
|
|
16782
|
+
* FULL FRAME (base64 JPEG). The runner derives the crop rectangle from
|
|
16783
|
+
* `parent.bbox` with the cluster crop convention and cuts it itself —
|
|
16784
|
+
* do NOT pre-crop for this field, that is what `cropJpeg` is.
|
|
16785
|
+
*/
|
|
16786
|
+
frameJpeg: string().optional(),
|
|
16787
|
+
/**
|
|
16788
|
+
* PRE-CUT tile (base64 JPEG), used verbatim — NO padding is applied.
|
|
16789
|
+
* The fallback when the lease/session backing the frame is gone and the
|
|
16790
|
+
* caller already holds a crop.
|
|
16791
|
+
*/
|
|
16781
16792
|
cropJpeg: string().optional(),
|
|
16782
16793
|
parent: DetailParentSchema,
|
|
16783
16794
|
steps: array(string()).optional()
|
|
@@ -31369,5 +31380,33 @@ function encodeVectorBase64(vector) {
|
|
|
31369
31380
|
for (let i = 0; i < floats.length; i += 1) view.setFloat32(i * 4, floats[i] ?? 0, true);
|
|
31370
31381
|
return Buffer.from(bytes).toString("base64");
|
|
31371
31382
|
}
|
|
31383
|
+
/** Vector length implied by a base64 payload, without decoding it. */
|
|
31384
|
+
function vectorDimFromBase64(encoded) {
|
|
31385
|
+
return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
|
|
31386
|
+
}
|
|
31387
|
+
object({
|
|
31388
|
+
/**
|
|
31389
|
+
* Fraction of the box's own size added on EACH side before cutting.
|
|
31390
|
+
*
|
|
31391
|
+
* CLIP is trained on natural images WITH surroundings; a pixel-tight crop
|
|
31392
|
+
* removes exactly the context it is strongest on (a dog cut to its outline
|
|
31393
|
+
* is a dark blob). The right value is an empirical question, which is why it
|
|
31394
|
+
* is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
|
|
31395
|
+
*/
|
|
31396
|
+
paddingRatio: number().min(0).max(4),
|
|
31397
|
+
/**
|
|
31398
|
+
* Square the window (in PIXELS) before cutting.
|
|
31399
|
+
*
|
|
31400
|
+
* CLIP's input is square, so a tall bbox resized straight to NxN is squashed
|
|
31401
|
+
* — a standing person becomes a shape the model never saw. Squaring costs
|
|
31402
|
+
* extra background, which is context the model wants anyway. Off by default
|
|
31403
|
+
* because the live path has never squared and the stored index reflects that.
|
|
31404
|
+
*/
|
|
31405
|
+
square: boolean()
|
|
31406
|
+
});
|
|
31407
|
+
({
|
|
31408
|
+
paddingRatio: .15,
|
|
31409
|
+
square: false
|
|
31410
|
+
}).paddingRatio;
|
|
31372
31411
|
//#endregion
|
|
31373
|
-
export {
|
|
31412
|
+
export { string as $, kebabToCamel as A, BaseAddon as B, cosineSimilarity as C, encodeVectorBase64 as D, embeddingEncoderCapability as E, subKindsOf as F, nodePin as G, createEvent as H, vectorDimFromBase64 as I, boolean as J, _enum as K, videoclipsCapability as L, pipelineAnalyticsCapability as M, plateGalleryCapability as N, faceGalleryCapability as O, readDeviceStateFrom as P, record as Q, zoneAnalyticsCapability as R, buildEventKindDescriptor as S, defineCustomActions as T, hydrateSchema as U, DeviceType as V, isDeviceScopedCap as W, number as X, literal as Y, object as Z, TimelapseRuleInputSchema as _, MACRO_LABELS as a, alarmPanelCapability as b, NcConditionDescriptorSchema as c, NcRuleSchema as d, unknown as et, NcSnoozeInputSchema as f, OpsLogEntrySchema as g, NcTaxonomySchema as h, EVENT_PAD_MS as i, notificationRulesCapability as j, hfModelUrl as k, NcRuleInputSchema as l, NcSnoozeSuppressedSchema as m, DEFAULT_EVENT_COLOR as n, NC_CONDITION_CATALOG as o, NcSnoozeSchema as p, array as q, EVENT_KIND_BY_CAP as r, NC_TAXONOMY as s, BaseDevice as t, EventCategory as tt, NcRulePatchSchema as u, TimelapseRuleSchema as v, customAction as w, audioMetricsCapability as x, addonWidgetsSourceCapability as y, errMsg as z };
|
|
@@ -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_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as BaseAddon, E as embeddingEncoderCapability, k as hfModelUrl } from "../dist-BHtH4Obt.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import "node:fs";
|
|
@@ -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,33 +12061,40 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12150
12061
|
missingBbox += 1;
|
|
12151
12062
|
continue;
|
|
12152
12063
|
}
|
|
12153
|
-
const
|
|
12154
|
-
if (
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
if (cut === null) {
|
|
12164
|
-
failed += 1;
|
|
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
|
+
});
|
|
12165
12074
|
continue;
|
|
12166
12075
|
}
|
|
12167
|
-
|
|
12168
|
-
if (embedding.length === 0) {
|
|
12076
|
+
if (outcome.kind === "failed") {
|
|
12169
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
|
+
});
|
|
12170
12085
|
continue;
|
|
12171
12086
|
}
|
|
12172
|
-
const
|
|
12173
|
-
if (
|
|
12087
|
+
const written = await deps.store(source, outcome.embeddingBase64);
|
|
12088
|
+
if (written === "persisted") rebuilt += 1;
|
|
12174
12089
|
else {
|
|
12175
12090
|
failed += 1;
|
|
12176
|
-
deps.logger.warn("embedding rebuild wrote nothing for a track", {
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
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
|
+
});
|
|
12180
12098
|
}
|
|
12181
12099
|
} catch (err) {
|
|
12182
12100
|
failed += 1;
|
|
@@ -12190,6 +12108,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12190
12108
|
rebuilt,
|
|
12191
12109
|
missingKeyFrame,
|
|
12192
12110
|
missingBbox,
|
|
12111
|
+
notRunnable,
|
|
12193
12112
|
failed
|
|
12194
12113
|
});
|
|
12195
12114
|
}
|
|
@@ -12205,16 +12124,16 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12205
12124
|
rebuilt,
|
|
12206
12125
|
missingKeyFrame,
|
|
12207
12126
|
missingBbox,
|
|
12127
|
+
notRunnable,
|
|
12208
12128
|
failed,
|
|
12209
|
-
complete
|
|
12210
|
-
cropMargin: margin,
|
|
12211
|
-
square
|
|
12129
|
+
complete
|
|
12212
12130
|
} });
|
|
12213
12131
|
return {
|
|
12214
12132
|
scanned,
|
|
12215
12133
|
rebuilt,
|
|
12216
12134
|
missingKeyFrame,
|
|
12217
12135
|
missingBbox,
|
|
12136
|
+
notRunnable,
|
|
12218
12137
|
failed,
|
|
12219
12138
|
complete
|
|
12220
12139
|
};
|
|
@@ -12382,6 +12301,69 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
12382
12301
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
12383
12302
|
}
|
|
12384
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
|
|
12385
12367
|
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
12386
12368
|
/**
|
|
12387
12369
|
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
@@ -14573,6 +14555,42 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14573
14555
|
};
|
|
14574
14556
|
}
|
|
14575
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
|
|
14576
14594
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14577
14595
|
function createKeyFrameCrop(deps) {
|
|
14578
14596
|
return async (input) => {
|
|
@@ -18562,30 +18580,18 @@ function buildGlobalSettingsSchema() {
|
|
|
18562
18580
|
{
|
|
18563
18581
|
id: "embedding-index",
|
|
18564
18582
|
title: "Semantic search index",
|
|
18565
|
-
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.",
|
|
18566
18584
|
columns: 1,
|
|
18567
18585
|
fields: [
|
|
18568
|
-
{
|
|
18569
|
-
type: "number",
|
|
18570
|
-
key: "rebuildCropMargin",
|
|
18571
|
-
label: "Rebuild crop margin",
|
|
18572
|
-
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.",
|
|
18573
|
-
default: .2,
|
|
18574
|
-
min: 0,
|
|
18575
|
-
max: 2,
|
|
18576
|
-
step: .1
|
|
18577
|
-
},
|
|
18578
18586
|
{
|
|
18579
18587
|
type: "addon-action-button",
|
|
18580
18588
|
key: "rebuildEmbeddingsButton",
|
|
18581
18589
|
label: "Rebuild index",
|
|
18582
|
-
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.",
|
|
18583
18591
|
addonId: "pipeline-analytics",
|
|
18584
18592
|
action: "embedding.rebuild",
|
|
18585
18593
|
buttonLabel: "Rebuild",
|
|
18586
18594
|
variant: "primary",
|
|
18587
|
-
paramsFromForm: { cropMargin: "rebuildCropMargin" },
|
|
18588
|
-
paramsStatic: { square: true },
|
|
18589
18595
|
successMessage: "Rebuild started — press Progress to follow it."
|
|
18590
18596
|
},
|
|
18591
18597
|
{
|
|
@@ -18596,7 +18602,7 @@ function buildGlobalSettingsSchema() {
|
|
|
18596
18602
|
addonId: "pipeline-analytics",
|
|
18597
18603
|
action: "embedding.rebuildStatus",
|
|
18598
18604
|
buttonLabel: "Check progress",
|
|
18599
|
-
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}"
|
|
18600
18606
|
},
|
|
18601
18607
|
{
|
|
18602
18608
|
type: "addon-action-button",
|
|
@@ -20588,6 +20594,29 @@ var IdentityStore = class {
|
|
|
20588
20594
|
*/
|
|
20589
20595
|
/** The vector-store index this store owns. */
|
|
20590
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
|
+
}
|
|
20591
20620
|
var ObjectEmbeddingStore = class {
|
|
20592
20621
|
vectorStore;
|
|
20593
20622
|
logger;
|
|
@@ -20654,8 +20683,9 @@ var ObjectEmbeddingStore = class {
|
|
|
20654
20683
|
* and dropped", because a counter that cannot reads as verification.
|
|
20655
20684
|
*/
|
|
20656
20685
|
async upsertIfBetter(input) {
|
|
20657
|
-
|
|
20658
|
-
if (
|
|
20686
|
+
const vector = resolveVectorPayload(input);
|
|
20687
|
+
if (vector === null) return "failed";
|
|
20688
|
+
if (!await this.ensureIndex(vector.dim)) return "failed";
|
|
20659
20689
|
try {
|
|
20660
20690
|
if (input.force !== true) {
|
|
20661
20691
|
const stored = (await this.vectorStore.getByIds.query({
|
|
@@ -20668,7 +20698,7 @@ var ObjectEmbeddingStore = class {
|
|
|
20668
20698
|
index: OBJECT_CLIP_INDEX,
|
|
20669
20699
|
items: [{
|
|
20670
20700
|
id: input.trackId,
|
|
20671
|
-
vector:
|
|
20701
|
+
vector: vector.base64,
|
|
20672
20702
|
metadata: metadataFor(input)
|
|
20673
20703
|
}]
|
|
20674
20704
|
});
|
|
@@ -20677,7 +20707,7 @@ var ObjectEmbeddingStore = class {
|
|
|
20677
20707
|
tags: { deviceId: input.deviceId },
|
|
20678
20708
|
meta: {
|
|
20679
20709
|
trackId: input.trackId,
|
|
20680
|
-
dim:
|
|
20710
|
+
dim: vector.dim,
|
|
20681
20711
|
rejected: result.rejected
|
|
20682
20712
|
}
|
|
20683
20713
|
});
|
|
@@ -23979,7 +24009,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
23979
24009
|
let storage = this.ctx.kernel.storage;
|
|
23980
24010
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
23981
24011
|
if (mediaRoot) {
|
|
23982
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
24012
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-m6HS7TOO.js"));
|
|
23983
24013
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
23984
24014
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
23985
24015
|
}
|
|
@@ -27043,6 +27073,43 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27043
27073
|
return info.modelId;
|
|
27044
27074
|
}
|
|
27045
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
|
+
/**
|
|
27046
27113
|
* Every track id the track store still holds, across the given devices.
|
|
27047
27114
|
*
|
|
27048
27115
|
* Returned as a Set for the orphan sweep, which REFUSES an empty one — a
|
|
@@ -27081,6 +27148,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27081
27148
|
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
27082
27149
|
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
27083
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.
|
|
27084
27157
|
*/
|
|
27085
27158
|
async rebuildObjectEmbeddings(input) {
|
|
27086
27159
|
if (this.rebuildState.running) {
|
|
@@ -27138,19 +27211,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27138
27211
|
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
27139
27212
|
};
|
|
27140
27213
|
},
|
|
27141
|
-
|
|
27142
|
-
|
|
27143
|
-
crop: new Uint8Array(crop),
|
|
27144
|
-
width,
|
|
27145
|
-
height
|
|
27146
|
-
})).embedding;
|
|
27147
|
-
},
|
|
27148
|
-
store: async (source, embedding) => store.upsertIfBetter({
|
|
27214
|
+
embed: (source) => this.embedRebuildSourceViaPipeline(source),
|
|
27215
|
+
store: async (source, embeddingBase64) => store.upsertIfBetter({
|
|
27149
27216
|
trackId: source.trackId,
|
|
27150
27217
|
deviceId: source.deviceId,
|
|
27151
27218
|
timestamp: source.timestamp,
|
|
27152
27219
|
className: source.className,
|
|
27153
|
-
|
|
27220
|
+
embeddingBase64,
|
|
27154
27221
|
modelId,
|
|
27155
27222
|
confidence: source.confidence,
|
|
27156
27223
|
force: true,
|
|
@@ -27170,12 +27237,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27170
27237
|
rebuilt: 0,
|
|
27171
27238
|
missingKeyFrame: 0,
|
|
27172
27239
|
missingBbox: 0,
|
|
27240
|
+
notRunnable: 0,
|
|
27173
27241
|
failed: 0
|
|
27174
27242
|
}
|
|
27175
27243
|
};
|
|
27176
27244
|
rebuildObjectEmbeddings(deps, {
|
|
27177
|
-
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
27178
|
-
...input.square !== void 0 ? { square: input.square } : {},
|
|
27179
27245
|
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
|
|
27180
27246
|
onProgress: (progress) => {
|
|
27181
27247
|
if (this.rebuildState.running) this.rebuildState.progress = progress;
|
|
@@ -27200,7 +27266,6 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27200
27266
|
});
|
|
27201
27267
|
this.ctx.logger.info("object embedding rebuild started", { meta: {
|
|
27202
27268
|
deviceId: input.deviceId ?? "all",
|
|
27203
|
-
cropMargin: input.cropMargin ?? "default",
|
|
27204
27269
|
maxTracks: input.maxTracks ?? "unbounded"
|
|
27205
27270
|
} });
|
|
27206
27271
|
return {
|
|
@@ -27242,6 +27307,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27242
27307
|
rebuilt: p?.rebuilt ?? 0,
|
|
27243
27308
|
missingKeyFrame: p?.missingKeyFrame ?? 0,
|
|
27244
27309
|
missingBbox: p?.missingBbox ?? 0,
|
|
27310
|
+
notRunnable: p?.notRunnable ?? 0,
|
|
27245
27311
|
failed: p?.failed ?? 0,
|
|
27246
27312
|
complete: state.complete ?? null,
|
|
27247
27313
|
startedAtMs: state.startedAtMs ?? null,
|
|
@@ -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,33 +12055,40 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12144
12055
|
missingBbox += 1;
|
|
12145
12056
|
continue;
|
|
12146
12057
|
}
|
|
12147
|
-
const
|
|
12148
|
-
if (
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
if (cut === null) {
|
|
12158
|
-
failed += 1;
|
|
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
|
+
});
|
|
12159
12068
|
continue;
|
|
12160
12069
|
}
|
|
12161
|
-
|
|
12162
|
-
if (embedding.length === 0) {
|
|
12070
|
+
if (outcome.kind === "failed") {
|
|
12163
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
|
+
});
|
|
12164
12079
|
continue;
|
|
12165
12080
|
}
|
|
12166
|
-
const
|
|
12167
|
-
if (
|
|
12081
|
+
const written = await deps.store(source, outcome.embeddingBase64);
|
|
12082
|
+
if (written === "persisted") rebuilt += 1;
|
|
12168
12083
|
else {
|
|
12169
12084
|
failed += 1;
|
|
12170
|
-
deps.logger.warn("embedding rebuild wrote nothing for a track", {
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
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
|
+
});
|
|
12174
12092
|
}
|
|
12175
12093
|
} catch (err) {
|
|
12176
12094
|
failed += 1;
|
|
@@ -12184,6 +12102,7 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12184
12102
|
rebuilt,
|
|
12185
12103
|
missingKeyFrame,
|
|
12186
12104
|
missingBbox,
|
|
12105
|
+
notRunnable,
|
|
12187
12106
|
failed
|
|
12188
12107
|
});
|
|
12189
12108
|
}
|
|
@@ -12199,16 +12118,16 @@ async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
|
12199
12118
|
rebuilt,
|
|
12200
12119
|
missingKeyFrame,
|
|
12201
12120
|
missingBbox,
|
|
12121
|
+
notRunnable,
|
|
12202
12122
|
failed,
|
|
12203
|
-
complete
|
|
12204
|
-
cropMargin: margin,
|
|
12205
|
-
square
|
|
12123
|
+
complete
|
|
12206
12124
|
} });
|
|
12207
12125
|
return {
|
|
12208
12126
|
scanned,
|
|
12209
12127
|
rebuilt,
|
|
12210
12128
|
missingKeyFrame,
|
|
12211
12129
|
missingBbox,
|
|
12130
|
+
notRunnable,
|
|
12212
12131
|
failed,
|
|
12213
12132
|
complete
|
|
12214
12133
|
};
|
|
@@ -12376,6 +12295,69 @@ async function resolveDefaultEventMedia(deps) {
|
|
|
12376
12295
|
return pickTrackFallbackMedia(await deps.listTrackMedia(trackId)) ?? null;
|
|
12377
12296
|
}
|
|
12378
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
|
|
12379
12361
|
//#region src/pipeline-analytics/pipeline/face-matcher.ts
|
|
12380
12362
|
/**
|
|
12381
12363
|
* Count enrolled samples per identity for probes of a matching model+dimension.
|
|
@@ -14567,6 +14549,42 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14567
14549
|
};
|
|
14568
14550
|
}
|
|
14569
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
|
|
14570
14588
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14571
14589
|
function createKeyFrameCrop(deps) {
|
|
14572
14590
|
return async (input) => {
|
|
@@ -18556,30 +18574,18 @@ function buildGlobalSettingsSchema() {
|
|
|
18556
18574
|
{
|
|
18557
18575
|
id: "embedding-index",
|
|
18558
18576
|
title: "Semantic search index",
|
|
18559
|
-
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.",
|
|
18560
18578
|
columns: 1,
|
|
18561
18579
|
fields: [
|
|
18562
|
-
{
|
|
18563
|
-
type: "number",
|
|
18564
|
-
key: "rebuildCropMargin",
|
|
18565
|
-
label: "Rebuild crop margin",
|
|
18566
|
-
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.",
|
|
18567
|
-
default: .2,
|
|
18568
|
-
min: 0,
|
|
18569
|
-
max: 2,
|
|
18570
|
-
step: .1
|
|
18571
|
-
},
|
|
18572
18580
|
{
|
|
18573
18581
|
type: "addon-action-button",
|
|
18574
18582
|
key: "rebuildEmbeddingsButton",
|
|
18575
18583
|
label: "Rebuild index",
|
|
18576
|
-
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.",
|
|
18577
18585
|
addonId: "pipeline-analytics",
|
|
18578
18586
|
action: "embedding.rebuild",
|
|
18579
18587
|
buttonLabel: "Rebuild",
|
|
18580
18588
|
variant: "primary",
|
|
18581
|
-
paramsFromForm: { cropMargin: "rebuildCropMargin" },
|
|
18582
|
-
paramsStatic: { square: true },
|
|
18583
18589
|
successMessage: "Rebuild started — press Progress to follow it."
|
|
18584
18590
|
},
|
|
18585
18591
|
{
|
|
@@ -18590,7 +18596,7 @@ function buildGlobalSettingsSchema() {
|
|
|
18590
18596
|
addonId: "pipeline-analytics",
|
|
18591
18597
|
action: "embedding.rebuildStatus",
|
|
18592
18598
|
buttonLabel: "Check progress",
|
|
18593
|
-
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}"
|
|
18594
18600
|
},
|
|
18595
18601
|
{
|
|
18596
18602
|
type: "addon-action-button",
|
|
@@ -20582,6 +20588,29 @@ var IdentityStore = class {
|
|
|
20582
20588
|
*/
|
|
20583
20589
|
/** The vector-store index this store owns. */
|
|
20584
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
|
+
}
|
|
20585
20614
|
var ObjectEmbeddingStore = class {
|
|
20586
20615
|
vectorStore;
|
|
20587
20616
|
logger;
|
|
@@ -20648,8 +20677,9 @@ var ObjectEmbeddingStore = class {
|
|
|
20648
20677
|
* and dropped", because a counter that cannot reads as verification.
|
|
20649
20678
|
*/
|
|
20650
20679
|
async upsertIfBetter(input) {
|
|
20651
|
-
|
|
20652
|
-
if (
|
|
20680
|
+
const vector = resolveVectorPayload(input);
|
|
20681
|
+
if (vector === null) return "failed";
|
|
20682
|
+
if (!await this.ensureIndex(vector.dim)) return "failed";
|
|
20653
20683
|
try {
|
|
20654
20684
|
if (input.force !== true) {
|
|
20655
20685
|
const stored = (await this.vectorStore.getByIds.query({
|
|
@@ -20662,7 +20692,7 @@ var ObjectEmbeddingStore = class {
|
|
|
20662
20692
|
index: OBJECT_CLIP_INDEX,
|
|
20663
20693
|
items: [{
|
|
20664
20694
|
id: input.trackId,
|
|
20665
|
-
vector:
|
|
20695
|
+
vector: vector.base64,
|
|
20666
20696
|
metadata: metadataFor(input)
|
|
20667
20697
|
}]
|
|
20668
20698
|
});
|
|
@@ -20671,7 +20701,7 @@ var ObjectEmbeddingStore = class {
|
|
|
20671
20701
|
tags: { deviceId: input.deviceId },
|
|
20672
20702
|
meta: {
|
|
20673
20703
|
trackId: input.trackId,
|
|
20674
|
-
dim:
|
|
20704
|
+
dim: vector.dim,
|
|
20675
20705
|
rejected: result.rejected
|
|
20676
20706
|
}
|
|
20677
20707
|
});
|
|
@@ -27037,6 +27067,43 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27037
27067
|
return info.modelId;
|
|
27038
27068
|
}
|
|
27039
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
|
+
/**
|
|
27040
27107
|
* Every track id the track store still holds, across the given devices.
|
|
27041
27108
|
*
|
|
27042
27109
|
* Returned as a Set for the orphan sweep, which REFUSES an empty one — a
|
|
@@ -27075,6 +27142,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27075
27142
|
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
27076
27143
|
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
27077
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.
|
|
27078
27151
|
*/
|
|
27079
27152
|
async rebuildObjectEmbeddings(input) {
|
|
27080
27153
|
if (this.rebuildState.running) {
|
|
@@ -27132,19 +27205,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27132
27205
|
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
27133
27206
|
};
|
|
27134
27207
|
},
|
|
27135
|
-
|
|
27136
|
-
|
|
27137
|
-
crop: new Uint8Array(crop),
|
|
27138
|
-
width,
|
|
27139
|
-
height
|
|
27140
|
-
})).embedding;
|
|
27141
|
-
},
|
|
27142
|
-
store: async (source, embedding) => store.upsertIfBetter({
|
|
27208
|
+
embed: (source) => this.embedRebuildSourceViaPipeline(source),
|
|
27209
|
+
store: async (source, embeddingBase64) => store.upsertIfBetter({
|
|
27143
27210
|
trackId: source.trackId,
|
|
27144
27211
|
deviceId: source.deviceId,
|
|
27145
27212
|
timestamp: source.timestamp,
|
|
27146
27213
|
className: source.className,
|
|
27147
|
-
|
|
27214
|
+
embeddingBase64,
|
|
27148
27215
|
modelId,
|
|
27149
27216
|
confidence: source.confidence,
|
|
27150
27217
|
force: true,
|
|
@@ -27164,12 +27231,11 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27164
27231
|
rebuilt: 0,
|
|
27165
27232
|
missingKeyFrame: 0,
|
|
27166
27233
|
missingBbox: 0,
|
|
27234
|
+
notRunnable: 0,
|
|
27167
27235
|
failed: 0
|
|
27168
27236
|
}
|
|
27169
27237
|
};
|
|
27170
27238
|
rebuildObjectEmbeddings(deps, {
|
|
27171
|
-
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
27172
|
-
...input.square !== void 0 ? { square: input.square } : {},
|
|
27173
27239
|
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {},
|
|
27174
27240
|
onProgress: (progress) => {
|
|
27175
27241
|
if (this.rebuildState.running) this.rebuildState.progress = progress;
|
|
@@ -27194,7 +27260,6 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27194
27260
|
});
|
|
27195
27261
|
this.ctx.logger.info("object embedding rebuild started", { meta: {
|
|
27196
27262
|
deviceId: input.deviceId ?? "all",
|
|
27197
|
-
cropMargin: input.cropMargin ?? "default",
|
|
27198
27263
|
maxTracks: input.maxTracks ?? "unbounded"
|
|
27199
27264
|
} });
|
|
27200
27265
|
return {
|
|
@@ -27236,6 +27301,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27236
27301
|
rebuilt: p?.rebuilt ?? 0,
|
|
27237
27302
|
missingKeyFrame: p?.missingKeyFrame ?? 0,
|
|
27238
27303
|
missingBbox: p?.missingBbox ?? 0,
|
|
27304
|
+
notRunnable: p?.notRunnable ?? 0,
|
|
27239
27305
|
failed: p?.failed ?? 0,
|
|
27240
27306
|
complete: state.complete ?? null,
|
|
27241
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",
|