@camstack/addon-post-analysis 1.2.38 → 1.2.39
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-BdBTDQxw.mjs → dist-BlnI3F_z.mjs} +48 -1
- package/dist/{dist-DQvvZ5_E.js → dist-z5sH8cjz.js} +48 -1
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-CJcK55RY.js → node-BKP-AAun.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrKQwfNe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CVOC4n3K.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DyfCBQE8.mjs → hostInit-D5ShhMc1.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +311 -45
- package/dist/pipeline-analytics/index.mjs +308 -42
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -15472,6 +15472,42 @@ var EventPruneCountsSchema = object({
|
|
|
15472
15472
|
object: number().int(),
|
|
15473
15473
|
audio: number().int()
|
|
15474
15474
|
});
|
|
15475
|
+
/**
|
|
15476
|
+
* Re-embed stored tracks from their key frames.
|
|
15477
|
+
*
|
|
15478
|
+
* The reason this is an operator-callable method and not a migration script:
|
|
15479
|
+
* every knob that decides what a vector MEANS — encoder model, crop margin,
|
|
15480
|
+
* squaring — is only changeable if the existing vectors can be regenerated.
|
|
15481
|
+
* Mixing feature spaces in one index makes cosine scores incomparable, and the
|
|
15482
|
+
* symptom is a quality regression with no visible cause.
|
|
15483
|
+
*/
|
|
15484
|
+
var RebuildObjectEmbeddingsInput = object({
|
|
15485
|
+
/** Restrict to one camera. Omit for the whole fleet. */
|
|
15486
|
+
deviceId: number().optional(),
|
|
15487
|
+
since: number().optional(),
|
|
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
|
+
/** Stop after this many tracks; the result reports whether more remain. */
|
|
15498
|
+
maxTracks: number().int().positive().optional()
|
|
15499
|
+
});
|
|
15500
|
+
var RebuildObjectEmbeddingsResultSchema = object({
|
|
15501
|
+
scanned: number(),
|
|
15502
|
+
rebuilt: number(),
|
|
15503
|
+
/** Tracks whose key frame is gone — nothing to re-embed from. */
|
|
15504
|
+
missingKeyFrame: number(),
|
|
15505
|
+
/** Tracks with no usable detection box. */
|
|
15506
|
+
missingBbox: number(),
|
|
15507
|
+
failed: number(),
|
|
15508
|
+
/** False when the pass stopped at `maxTracks` with tracks left. */
|
|
15509
|
+
complete: boolean()
|
|
15510
|
+
});
|
|
15475
15511
|
var pipelineAnalyticsCapability = {
|
|
15476
15512
|
name: "pipeline-analytics",
|
|
15477
15513
|
scope: "device",
|
|
@@ -15744,7 +15780,12 @@ var pipelineAnalyticsCapability = {
|
|
|
15744
15780
|
* ObjectEvents by trackId. Returns up to `limit` events scored ≥
|
|
15745
15781
|
* `minScore`, sorted descending by score.
|
|
15746
15782
|
*/
|
|
15747
|
-
searchObjectEvents: method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly())
|
|
15783
|
+
searchObjectEvents: method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()),
|
|
15784
|
+
rebuildObjectEmbeddings: method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
15785
|
+
kind: "mutation",
|
|
15786
|
+
auth: "admin",
|
|
15787
|
+
timeoutMs: 30 * 6e4
|
|
15788
|
+
})
|
|
15748
15789
|
},
|
|
15749
15790
|
events: {
|
|
15750
15791
|
/**
|
|
@@ -29018,6 +29059,12 @@ Object.freeze({
|
|
|
29018
29059
|
addonId: null,
|
|
29019
29060
|
access: "create"
|
|
29020
29061
|
},
|
|
29062
|
+
"pipelineAnalytics.rebuildObjectEmbeddings": {
|
|
29063
|
+
capName: "pipeline-analytics",
|
|
29064
|
+
capScope: "device",
|
|
29065
|
+
addonId: null,
|
|
29066
|
+
access: "create"
|
|
29067
|
+
},
|
|
29021
29068
|
"pipelineAnalytics.relocateMedia": {
|
|
29022
29069
|
capName: "pipeline-analytics",
|
|
29023
29070
|
capScope: "device",
|
|
@@ -15494,6 +15494,42 @@ var EventPruneCountsSchema = object({
|
|
|
15494
15494
|
object: number().int(),
|
|
15495
15495
|
audio: number().int()
|
|
15496
15496
|
});
|
|
15497
|
+
/**
|
|
15498
|
+
* Re-embed stored tracks from their key frames.
|
|
15499
|
+
*
|
|
15500
|
+
* The reason this is an operator-callable method and not a migration script:
|
|
15501
|
+
* every knob that decides what a vector MEANS — encoder model, crop margin,
|
|
15502
|
+
* squaring — is only changeable if the existing vectors can be regenerated.
|
|
15503
|
+
* Mixing feature spaces in one index makes cosine scores incomparable, and the
|
|
15504
|
+
* symptom is a quality regression with no visible cause.
|
|
15505
|
+
*/
|
|
15506
|
+
var RebuildObjectEmbeddingsInput = object({
|
|
15507
|
+
/** Restrict to one camera. Omit for the whole fleet. */
|
|
15508
|
+
deviceId: number().optional(),
|
|
15509
|
+
since: number().optional(),
|
|
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
|
+
/** Stop after this many tracks; the result reports whether more remain. */
|
|
15520
|
+
maxTracks: number().int().positive().optional()
|
|
15521
|
+
});
|
|
15522
|
+
var RebuildObjectEmbeddingsResultSchema = object({
|
|
15523
|
+
scanned: number(),
|
|
15524
|
+
rebuilt: number(),
|
|
15525
|
+
/** Tracks whose key frame is gone — nothing to re-embed from. */
|
|
15526
|
+
missingKeyFrame: number(),
|
|
15527
|
+
/** Tracks with no usable detection box. */
|
|
15528
|
+
missingBbox: number(),
|
|
15529
|
+
failed: number(),
|
|
15530
|
+
/** False when the pass stopped at `maxTracks` with tracks left. */
|
|
15531
|
+
complete: boolean()
|
|
15532
|
+
});
|
|
15497
15533
|
var pipelineAnalyticsCapability = {
|
|
15498
15534
|
name: "pipeline-analytics",
|
|
15499
15535
|
scope: "device",
|
|
@@ -15766,7 +15802,12 @@ var pipelineAnalyticsCapability = {
|
|
|
15766
15802
|
* ObjectEvents by trackId. Returns up to `limit` events scored ≥
|
|
15767
15803
|
* `minScore`, sorted descending by score.
|
|
15768
15804
|
*/
|
|
15769
|
-
searchObjectEvents: method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly())
|
|
15805
|
+
searchObjectEvents: method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()),
|
|
15806
|
+
rebuildObjectEmbeddings: method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
15807
|
+
kind: "mutation",
|
|
15808
|
+
auth: "admin",
|
|
15809
|
+
timeoutMs: 30 * 6e4
|
|
15810
|
+
})
|
|
15770
15811
|
},
|
|
15771
15812
|
events: {
|
|
15772
15813
|
/**
|
|
@@ -29040,6 +29081,12 @@ Object.freeze({
|
|
|
29040
29081
|
addonId: null,
|
|
29041
29082
|
access: "create"
|
|
29042
29083
|
},
|
|
29084
|
+
"pipelineAnalytics.rebuildObjectEmbeddings": {
|
|
29085
|
+
capName: "pipeline-analytics",
|
|
29086
|
+
capScope: "device",
|
|
29087
|
+
addonId: null,
|
|
29088
|
+
access: "create"
|
|
29089
|
+
},
|
|
29043
29090
|
"pipelineAnalytics.relocateMedia": {
|
|
29044
29091
|
capName: "pipeline-analytics",
|
|
29045
29092
|
capScope: "device",
|
|
@@ -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-z5sH8cjz.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 { E as embeddingEncoderCapability, k as hfModelUrl, z as BaseAddon } from "../dist-
|
|
1
|
+
import { E as embeddingEncoderCapability, k as hfModelUrl, z as BaseAddon } from "../dist-BlnI3F_z.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-z5sH8cjz.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);
|
|
@@ -2255,8 +2255,8 @@ var NcDispatcher = class {
|
|
|
2255
2255
|
try {
|
|
2256
2256
|
const points = (await this.deps.getZonePolygons?.(deviceId, zoneIds) ?? []).flat();
|
|
2257
2257
|
if (points.length === 0) return null;
|
|
2258
|
-
const { default: sharp$
|
|
2259
|
-
const img = sharp$
|
|
2258
|
+
const { default: sharp$8 } = await import("sharp");
|
|
2259
|
+
const img = sharp$8(Buffer.from(jpeg));
|
|
2260
2260
|
const meta = await img.metadata();
|
|
2261
2261
|
const W = meta.width ?? 0;
|
|
2262
2262
|
const H = meta.height ?? 0;
|
|
@@ -11955,6 +11955,189 @@ var EmbeddingDispatcher = class {
|
|
|
11955
11955
|
}
|
|
11956
11956
|
};
|
|
11957
11957
|
//#endregion
|
|
11958
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
11959
|
+
/**
|
|
11960
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
11961
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
11962
|
+
*
|
|
11963
|
+
* Formula (before clamping):
|
|
11964
|
+
* newX = x - padding * w
|
|
11965
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
11966
|
+
* newY = y - padding * h
|
|
11967
|
+
* newH = h + 2 * padding * h
|
|
11968
|
+
*
|
|
11969
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
11970
|
+
* new object.
|
|
11971
|
+
*
|
|
11972
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
11973
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
11974
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
11975
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
11976
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
11977
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
11978
|
+
*/
|
|
11979
|
+
function padBbox(bbox, padding) {
|
|
11980
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
11981
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
11982
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
11983
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
11984
|
+
const w = Math.min(rawW, 1);
|
|
11985
|
+
const h = Math.min(rawH, 1);
|
|
11986
|
+
return {
|
|
11987
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
11988
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
11989
|
+
w,
|
|
11990
|
+
h
|
|
11991
|
+
};
|
|
11992
|
+
}
|
|
11993
|
+
/**
|
|
11994
|
+
* Widen a bbox and optionally square it, staying inside the unit frame.
|
|
11995
|
+
*
|
|
11996
|
+
* Squaring uses the frame's aspect so the window is square in PIXELS, not in
|
|
11997
|
+
* normalised units — those are only the same on a square frame, and no camera
|
|
11998
|
+
* produces one.
|
|
11999
|
+
*/
|
|
12000
|
+
function cropWindow(bbox, frameWidth, frameHeight, options) {
|
|
12001
|
+
const padded = padBbox(bbox, options.margin);
|
|
12002
|
+
if (!options.square) return padded;
|
|
12003
|
+
const pxW = padded.w * frameWidth;
|
|
12004
|
+
const pxH = padded.h * frameHeight;
|
|
12005
|
+
const side = Math.max(pxW, pxH);
|
|
12006
|
+
const w = Math.min(side / frameWidth, 1);
|
|
12007
|
+
const h = Math.min(side / frameHeight, 1);
|
|
12008
|
+
const cx = padded.x + padded.w / 2;
|
|
12009
|
+
const cy = padded.y + padded.h / 2;
|
|
12010
|
+
return {
|
|
12011
|
+
w,
|
|
12012
|
+
h,
|
|
12013
|
+
x: Math.min(Math.max(0, cx - w / 2), 1 - w),
|
|
12014
|
+
y: Math.min(Math.max(0, cy - h / 2), 1 - h)
|
|
12015
|
+
};
|
|
12016
|
+
}
|
|
12017
|
+
/** Cut a JPEG crop out of a JPEG frame using a normalised window. */
|
|
12018
|
+
async function cropFromJpeg(jpeg, window) {
|
|
12019
|
+
const image = (0, sharp.default)(jpeg);
|
|
12020
|
+
const meta = await image.metadata();
|
|
12021
|
+
const frameWidth = meta.width;
|
|
12022
|
+
const frameHeight = meta.height;
|
|
12023
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12024
|
+
const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
|
|
12025
|
+
const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
|
|
12026
|
+
const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
|
|
12027
|
+
const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
|
|
12028
|
+
return {
|
|
12029
|
+
crop: await image.extract({
|
|
12030
|
+
left,
|
|
12031
|
+
top,
|
|
12032
|
+
width,
|
|
12033
|
+
height
|
|
12034
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
12035
|
+
width,
|
|
12036
|
+
height
|
|
12037
|
+
};
|
|
12038
|
+
}
|
|
12039
|
+
/**
|
|
12040
|
+
* Re-embed every listed track from its key frame.
|
|
12041
|
+
*
|
|
12042
|
+
* Every skip is COUNTED and the counts are returned, because the caller's next
|
|
12043
|
+
* decision — whether the index is now consistent enough to trust — depends on
|
|
12044
|
+
* knowing how much was not rebuilt, not just how much was.
|
|
12045
|
+
*/
|
|
12046
|
+
async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
12047
|
+
const margin = options.cropMargin ?? .2;
|
|
12048
|
+
const square = options.square ?? true;
|
|
12049
|
+
const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
|
|
12050
|
+
const trackIds = await deps.listTrackIds();
|
|
12051
|
+
const considered = trackIds.slice(0, Number.isFinite(maxTracks) ? maxTracks : void 0);
|
|
12052
|
+
let rebuilt = 0;
|
|
12053
|
+
let missingKeyFrame = 0;
|
|
12054
|
+
let missingBbox = 0;
|
|
12055
|
+
let failed = 0;
|
|
12056
|
+
for (const trackId of considered) try {
|
|
12057
|
+
const source = await deps.loadSource(trackId);
|
|
12058
|
+
if (source === null) {
|
|
12059
|
+
missingKeyFrame += 1;
|
|
12060
|
+
continue;
|
|
12061
|
+
}
|
|
12062
|
+
if (source.keyFrame === null) {
|
|
12063
|
+
missingKeyFrame += 1;
|
|
12064
|
+
continue;
|
|
12065
|
+
}
|
|
12066
|
+
if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
|
|
12067
|
+
missingBbox += 1;
|
|
12068
|
+
continue;
|
|
12069
|
+
}
|
|
12070
|
+
const meta = await (0, sharp.default)(source.keyFrame).metadata();
|
|
12071
|
+
if (meta.width === void 0 || meta.height === void 0) {
|
|
12072
|
+
failed += 1;
|
|
12073
|
+
continue;
|
|
12074
|
+
}
|
|
12075
|
+
const window = cropWindow(source.bbox, meta.width, meta.height, {
|
|
12076
|
+
margin,
|
|
12077
|
+
square
|
|
12078
|
+
});
|
|
12079
|
+
const cut = await cropFromJpeg(source.keyFrame, window);
|
|
12080
|
+
if (cut === null) {
|
|
12081
|
+
failed += 1;
|
|
12082
|
+
continue;
|
|
12083
|
+
}
|
|
12084
|
+
const embedding = await deps.encode(cut.crop, cut.width, cut.height);
|
|
12085
|
+
if (embedding.length === 0) {
|
|
12086
|
+
failed += 1;
|
|
12087
|
+
continue;
|
|
12088
|
+
}
|
|
12089
|
+
await deps.store(source, embedding);
|
|
12090
|
+
rebuilt += 1;
|
|
12091
|
+
} catch (err) {
|
|
12092
|
+
failed += 1;
|
|
12093
|
+
deps.logger.warn("embedding rebuild failed for a track", { meta: {
|
|
12094
|
+
trackId,
|
|
12095
|
+
error: String(err)
|
|
12096
|
+
} });
|
|
12097
|
+
}
|
|
12098
|
+
const complete = considered.length === trackIds.length;
|
|
12099
|
+
deps.logger.info("object embedding rebuild finished", { meta: {
|
|
12100
|
+
scanned: considered.length,
|
|
12101
|
+
rebuilt,
|
|
12102
|
+
missingKeyFrame,
|
|
12103
|
+
missingBbox,
|
|
12104
|
+
failed,
|
|
12105
|
+
complete,
|
|
12106
|
+
cropMargin: margin,
|
|
12107
|
+
square
|
|
12108
|
+
} });
|
|
12109
|
+
return {
|
|
12110
|
+
scanned: considered.length,
|
|
12111
|
+
rebuilt,
|
|
12112
|
+
missingKeyFrame,
|
|
12113
|
+
missingBbox,
|
|
12114
|
+
failed,
|
|
12115
|
+
complete
|
|
12116
|
+
};
|
|
12117
|
+
}
|
|
12118
|
+
/**
|
|
12119
|
+
* Read a normalised bbox off an object event.
|
|
12120
|
+
*
|
|
12121
|
+
* Events carry the box in FRAME PIXELS alongside `frameWidth`/`frameHeight`,
|
|
12122
|
+
* not normalised — normalising here rather than at the call site keeps the
|
|
12123
|
+
* conversion in one place, next to the code that consumes it.
|
|
12124
|
+
* Returns null when the frame dimensions are missing, because a box without
|
|
12125
|
+
* them cannot be placed on a differently-sized key frame.
|
|
12126
|
+
*/
|
|
12127
|
+
function normaliseEventBbox(event) {
|
|
12128
|
+
const { bbox, frameWidth, frameHeight } = event;
|
|
12129
|
+
if (bbox === void 0) return null;
|
|
12130
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12131
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
12132
|
+
if (bbox.w <= 0 || bbox.h <= 0) return null;
|
|
12133
|
+
return {
|
|
12134
|
+
x: bbox.x / frameWidth,
|
|
12135
|
+
y: bbox.y / frameHeight,
|
|
12136
|
+
w: bbox.w / frameWidth,
|
|
12137
|
+
h: bbox.h / frameHeight
|
|
12138
|
+
};
|
|
12139
|
+
}
|
|
12140
|
+
//#endregion
|
|
11958
12141
|
//#region src/pipeline-analytics/event-media-handler.ts
|
|
11959
12142
|
var CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
11960
12143
|
/** Default / clamp bounds for the `thumb` variant edge (px). Mirrors
|
|
@@ -14286,42 +14469,6 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14286
14469
|
};
|
|
14287
14470
|
}
|
|
14288
14471
|
//#endregion
|
|
14289
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
14290
|
-
/**
|
|
14291
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
14292
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
14293
|
-
*
|
|
14294
|
-
* Formula (before clamping):
|
|
14295
|
-
* newX = x - padding * w
|
|
14296
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
14297
|
-
* newY = y - padding * h
|
|
14298
|
-
* newH = h + 2 * padding * h
|
|
14299
|
-
*
|
|
14300
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
14301
|
-
* new object.
|
|
14302
|
-
*
|
|
14303
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
14304
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
14305
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
14306
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
14307
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
14308
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
14309
|
-
*/
|
|
14310
|
-
function padBbox(bbox, padding) {
|
|
14311
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
14312
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
14313
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
14314
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
14315
|
-
const w = Math.min(rawW, 1);
|
|
14316
|
-
const h = Math.min(rawH, 1);
|
|
14317
|
-
return {
|
|
14318
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
14319
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
14320
|
-
w,
|
|
14321
|
-
h
|
|
14322
|
-
};
|
|
14323
|
-
}
|
|
14324
|
-
//#endregion
|
|
14325
14472
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14326
14473
|
function createKeyFrameCrop(deps) {
|
|
14327
14474
|
return async (input) => {
|
|
@@ -22693,6 +22840,15 @@ function buildStationarySnapshot(input) {
|
|
|
22693
22840
|
//#region src/pipeline-analytics/index.ts
|
|
22694
22841
|
/** This addon's manifest id — the owner of the fixed Notification Center
|
|
22695
22842
|
* integration and of the alarm device under it. */
|
|
22843
|
+
/**
|
|
22844
|
+
* How long the active encoder's model id is trusted without re-asking.
|
|
22845
|
+
*
|
|
22846
|
+
* `embeddingEncoder.getInfo` measured 1.42s on the live hub — a cross-process
|
|
22847
|
+
* call into an addon busy with live inference — for a value that changes only
|
|
22848
|
+
* when an operator swaps encoders. A minute is short enough that a swap takes
|
|
22849
|
+
* effect without a restart and long enough that no search pays for it twice.
|
|
22850
|
+
*/
|
|
22851
|
+
var ENCODER_INFO_TTL_MS = 6e4;
|
|
22696
22852
|
var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
22697
22853
|
/** Operator-visible name of the fixed integration. */
|
|
22698
22854
|
var NC_INTEGRATION_NAME = "Notification Center";
|
|
@@ -22880,6 +23036,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
22880
23036
|
plateStore = null;
|
|
22881
23037
|
plateRecognizer = null;
|
|
22882
23038
|
objectEmbeddingStore = null;
|
|
23039
|
+
/** Cached active-encoder model id — see `resolveEncoderModelId`. */
|
|
23040
|
+
encoderInfoCache = null;
|
|
22883
23041
|
/** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
|
|
22884
23042
|
* classifier/plate enrichment via `pipelineRunner.runDetailSubtree`. Replaces
|
|
22885
23043
|
* the per-frame child consumption the executor no longer emits. */
|
|
@@ -23502,7 +23660,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
23502
23660
|
let storage = this.ctx.kernel.storage;
|
|
23503
23661
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
23504
23662
|
if (mediaRoot) {
|
|
23505
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
23663
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-BKP-AAun.js"));
|
|
23506
23664
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
23507
23665
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
23508
23666
|
}
|
|
@@ -26511,6 +26669,117 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26511
26669
|
* with each winner's deviceId + trackId filter) and join score.
|
|
26512
26670
|
* 5. Return at most `limit` ScoredObjectEvents sorted descending by score.
|
|
26513
26671
|
*/
|
|
26672
|
+
/**
|
|
26673
|
+
* The active encoder's model id, cached for {@link ENCODER_INFO_TTL_MS}.
|
|
26674
|
+
*
|
|
26675
|
+
* Refreshed rather than pinned forever so an operator swapping the encoder
|
|
26676
|
+
* does not need a restart to make search gate on the new model — the window
|
|
26677
|
+
* where the old id is used is bounded by the TTL, and during it a search
|
|
26678
|
+
* simply ranks against the previous model's vectors, which is what it would
|
|
26679
|
+
* have done anyway until the new ones exist.
|
|
26680
|
+
*/
|
|
26681
|
+
async resolveEncoderModelId(api) {
|
|
26682
|
+
const now = Date.now();
|
|
26683
|
+
if (this.encoderInfoCache !== null && now - this.encoderInfoCache.at < ENCODER_INFO_TTL_MS) return this.encoderInfoCache.modelId;
|
|
26684
|
+
const info = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
26685
|
+
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
26686
|
+
return null;
|
|
26687
|
+
});
|
|
26688
|
+
if (info === null) return this.encoderInfoCache?.modelId ?? null;
|
|
26689
|
+
this.encoderInfoCache = {
|
|
26690
|
+
modelId: info.modelId,
|
|
26691
|
+
at: now
|
|
26692
|
+
};
|
|
26693
|
+
return info.modelId;
|
|
26694
|
+
}
|
|
26695
|
+
/**
|
|
26696
|
+
* Re-embed stored tracks from their key frames — the tool that makes the crop
|
|
26697
|
+
* policy tunable.
|
|
26698
|
+
*
|
|
26699
|
+
* Pixels come from each track's `keyFrame` (the FULL frame at native
|
|
26700
|
+
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
26701
|
+
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
26702
|
+
* widened after the fact.
|
|
26703
|
+
*/
|
|
26704
|
+
async rebuildObjectEmbeddings(input) {
|
|
26705
|
+
const store = this.objectEmbeddingStore;
|
|
26706
|
+
const trackStore = this.trackStore;
|
|
26707
|
+
const mediaStore = this.mediaStore;
|
|
26708
|
+
const eventStore = this.eventStore;
|
|
26709
|
+
const api = this.ctx.api;
|
|
26710
|
+
const empty = {
|
|
26711
|
+
scanned: 0,
|
|
26712
|
+
rebuilt: 0,
|
|
26713
|
+
missingKeyFrame: 0,
|
|
26714
|
+
missingBbox: 0,
|
|
26715
|
+
failed: 0,
|
|
26716
|
+
complete: true
|
|
26717
|
+
};
|
|
26718
|
+
if (!store || !trackStore || !mediaStore || !eventStore || !api) {
|
|
26719
|
+
this.ctx.logger.warn("rebuildObjectEmbeddings: analytics stores unavailable");
|
|
26720
|
+
return empty;
|
|
26721
|
+
}
|
|
26722
|
+
const modelId = await this.resolveEncoderModelId(api) ?? "unknown";
|
|
26723
|
+
const deviceIds = input.deviceId !== void 0 ? [input.deviceId] : await trackStore.listDeviceIds();
|
|
26724
|
+
return rebuildObjectEmbeddings({
|
|
26725
|
+
listTrackIds: async () => {
|
|
26726
|
+
return (await trackStore.queryRecent({
|
|
26727
|
+
deviceIds,
|
|
26728
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
26729
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
26730
|
+
limit: input.maxTracks ?? 500
|
|
26731
|
+
})).tracks.map((t) => t.trackId);
|
|
26732
|
+
},
|
|
26733
|
+
loadSource: async (trackId) => {
|
|
26734
|
+
const media = await mediaStore.listByOwner("track", trackId);
|
|
26735
|
+
const keyFrameFile = media.find((m) => m.kind === "keyFrame");
|
|
26736
|
+
const cropFile = media.find((m) => m.kind === "thumbnail");
|
|
26737
|
+
const bestEventId = (await eventStore.peakForTrack(trackId)).bestEventId;
|
|
26738
|
+
const events = await eventStore.queryObjectByTrackIds([trackId]);
|
|
26739
|
+
const best = events.find((e) => e.id === bestEventId) ?? events[0];
|
|
26740
|
+
if (best === void 0) return null;
|
|
26741
|
+
return {
|
|
26742
|
+
trackId,
|
|
26743
|
+
deviceId: best.deviceId,
|
|
26744
|
+
timestamp: best.timestamp,
|
|
26745
|
+
className: best.className,
|
|
26746
|
+
confidence: best.confidence ?? 0,
|
|
26747
|
+
keyFrame: keyFrameFile ? Buffer.from(keyFrameFile.base64, "base64") : null,
|
|
26748
|
+
bbox: normaliseEventBbox(best),
|
|
26749
|
+
...cropFile !== void 0 ? { mediaKey: cropFile.key } : {},
|
|
26750
|
+
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
26751
|
+
};
|
|
26752
|
+
},
|
|
26753
|
+
encode: async (crop, width, height) => {
|
|
26754
|
+
return (await api.embeddingEncoder.encode.query({
|
|
26755
|
+
crop: new Uint8Array(crop),
|
|
26756
|
+
width,
|
|
26757
|
+
height
|
|
26758
|
+
})).embedding;
|
|
26759
|
+
},
|
|
26760
|
+
store: async (source, embedding) => {
|
|
26761
|
+
await store.upsertIfBetter({
|
|
26762
|
+
trackId: source.trackId,
|
|
26763
|
+
deviceId: source.deviceId,
|
|
26764
|
+
timestamp: source.timestamp,
|
|
26765
|
+
className: source.className,
|
|
26766
|
+
embedding,
|
|
26767
|
+
modelId,
|
|
26768
|
+
confidence: Number.POSITIVE_INFINITY,
|
|
26769
|
+
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
26770
|
+
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
26771
|
+
});
|
|
26772
|
+
},
|
|
26773
|
+
logger: {
|
|
26774
|
+
info: (message, extras) => this.ctx.logger.info(message, extras),
|
|
26775
|
+
warn: (message, extras) => this.ctx.logger.warn(message, extras)
|
|
26776
|
+
}
|
|
26777
|
+
}, {
|
|
26778
|
+
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
26779
|
+
...input.square !== void 0 ? { square: input.square } : {},
|
|
26780
|
+
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {}
|
|
26781
|
+
});
|
|
26782
|
+
}
|
|
26514
26783
|
async searchObjectEvents(input) {
|
|
26515
26784
|
const store = this.objectEmbeddingStore;
|
|
26516
26785
|
if (!store) return [];
|
|
@@ -26522,10 +26791,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26522
26791
|
});
|
|
26523
26792
|
if (!encodeResult) return [];
|
|
26524
26793
|
const queryVec = new Float32Array(encodeResult.embedding);
|
|
26525
|
-
const
|
|
26526
|
-
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
26527
|
-
return null;
|
|
26528
|
-
});
|
|
26794
|
+
const encoderModelId = await this.resolveEncoderModelId(api);
|
|
26529
26795
|
const search = await store.searchByVector({
|
|
26530
26796
|
vector: queryVec,
|
|
26531
26797
|
limit: input.limit,
|
|
@@ -26534,7 +26800,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26534
26800
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
26535
26801
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
26536
26802
|
...input.classFilter !== void 0 ? { className: input.classFilter } : {},
|
|
26537
|
-
...
|
|
26803
|
+
...encoderModelId !== null ? { modelId: encoderModelId } : {}
|
|
26538
26804
|
});
|
|
26539
26805
|
if (search.truncated) this.ctx.logger.warn("searchObjectEvents: candidate pool truncated", {
|
|
26540
26806
|
meta: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as unknown, A as kebabToCamel, B as DeviceType, C as cosineSimilarity, D as encodeVectorBase64, F as subKindsOf, G as _enum, H as hydrateSchema, I as videoclipsCapability, J as literal, K as array, L as zoneAnalyticsCapability, M as pipelineAnalyticsCapability, N as plateGalleryCapability, O as faceGalleryCapability, P as readDeviceStateFrom, Q as string, R as errMsg, S as buildEventKindDescriptor, T as defineCustomActions, U as isDeviceScopedCap, V as createEvent, W as nodePin, X as object, Y as number, Z as record, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as EventCategory, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as notificationRulesCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as boolean, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as BaseAddon } from "../dist-
|
|
1
|
+
import { $ as unknown, A as kebabToCamel, B as DeviceType, C as cosineSimilarity, D as encodeVectorBase64, F as subKindsOf, G as _enum, H as hydrateSchema, I as videoclipsCapability, J as literal, K as array, L as zoneAnalyticsCapability, M as pipelineAnalyticsCapability, N as plateGalleryCapability, O as faceGalleryCapability, P as readDeviceStateFrom, Q as string, R as errMsg, S as buildEventKindDescriptor, T as defineCustomActions, U as isDeviceScopedCap, V as createEvent, W as nodePin, X as object, Y as number, Z as record, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as alarmPanelCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as EventCategory, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as notificationRulesCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as boolean, r as EVENT_KIND_BY_CAP, s as NC_TAXONOMY, t as BaseDevice, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as customAction, x as audioMetricsCapability, y as addonWidgetsSourceCapability, z as BaseAddon } from "../dist-BlnI3F_z.mjs";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -11949,6 +11949,189 @@ var EmbeddingDispatcher = class {
|
|
|
11949
11949
|
}
|
|
11950
11950
|
};
|
|
11951
11951
|
//#endregion
|
|
11952
|
+
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
11953
|
+
/**
|
|
11954
|
+
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
11955
|
+
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
11956
|
+
*
|
|
11957
|
+
* Formula (before clamping):
|
|
11958
|
+
* newX = x - padding * w
|
|
11959
|
+
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
11960
|
+
* newY = y - padding * h
|
|
11961
|
+
* newH = h + 2 * padding * h
|
|
11962
|
+
*
|
|
11963
|
+
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
11964
|
+
* new object.
|
|
11965
|
+
*
|
|
11966
|
+
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
11967
|
+
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
11968
|
+
* It shrinks only when the padded window is larger than the frame itself
|
|
11969
|
+
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
11970
|
+
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
11971
|
+
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
11972
|
+
*/
|
|
11973
|
+
function padBbox(bbox, padding) {
|
|
11974
|
+
const rawX = bbox.x - padding * bbox.w;
|
|
11975
|
+
const rawY = bbox.y - padding * bbox.h;
|
|
11976
|
+
const rawW = bbox.w * (1 + 2 * padding);
|
|
11977
|
+
const rawH = bbox.h * (1 + 2 * padding);
|
|
11978
|
+
const w = Math.min(rawW, 1);
|
|
11979
|
+
const h = Math.min(rawH, 1);
|
|
11980
|
+
return {
|
|
11981
|
+
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
11982
|
+
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
11983
|
+
w,
|
|
11984
|
+
h
|
|
11985
|
+
};
|
|
11986
|
+
}
|
|
11987
|
+
/**
|
|
11988
|
+
* Widen a bbox and optionally square it, staying inside the unit frame.
|
|
11989
|
+
*
|
|
11990
|
+
* Squaring uses the frame's aspect so the window is square in PIXELS, not in
|
|
11991
|
+
* normalised units — those are only the same on a square frame, and no camera
|
|
11992
|
+
* produces one.
|
|
11993
|
+
*/
|
|
11994
|
+
function cropWindow(bbox, frameWidth, frameHeight, options) {
|
|
11995
|
+
const padded = padBbox(bbox, options.margin);
|
|
11996
|
+
if (!options.square) return padded;
|
|
11997
|
+
const pxW = padded.w * frameWidth;
|
|
11998
|
+
const pxH = padded.h * frameHeight;
|
|
11999
|
+
const side = Math.max(pxW, pxH);
|
|
12000
|
+
const w = Math.min(side / frameWidth, 1);
|
|
12001
|
+
const h = Math.min(side / frameHeight, 1);
|
|
12002
|
+
const cx = padded.x + padded.w / 2;
|
|
12003
|
+
const cy = padded.y + padded.h / 2;
|
|
12004
|
+
return {
|
|
12005
|
+
w,
|
|
12006
|
+
h,
|
|
12007
|
+
x: Math.min(Math.max(0, cx - w / 2), 1 - w),
|
|
12008
|
+
y: Math.min(Math.max(0, cy - h / 2), 1 - h)
|
|
12009
|
+
};
|
|
12010
|
+
}
|
|
12011
|
+
/** Cut a JPEG crop out of a JPEG frame using a normalised window. */
|
|
12012
|
+
async function cropFromJpeg(jpeg, window) {
|
|
12013
|
+
const image = sharp(jpeg);
|
|
12014
|
+
const meta = await image.metadata();
|
|
12015
|
+
const frameWidth = meta.width;
|
|
12016
|
+
const frameHeight = meta.height;
|
|
12017
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12018
|
+
const left = Math.max(0, Math.min(Math.round(window.x * frameWidth), frameWidth - 1));
|
|
12019
|
+
const top = Math.max(0, Math.min(Math.round(window.y * frameHeight), frameHeight - 1));
|
|
12020
|
+
const width = Math.max(1, Math.min(Math.round(window.w * frameWidth), frameWidth - left));
|
|
12021
|
+
const height = Math.max(1, Math.min(Math.round(window.h * frameHeight), frameHeight - top));
|
|
12022
|
+
return {
|
|
12023
|
+
crop: await image.extract({
|
|
12024
|
+
left,
|
|
12025
|
+
top,
|
|
12026
|
+
width,
|
|
12027
|
+
height
|
|
12028
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
12029
|
+
width,
|
|
12030
|
+
height
|
|
12031
|
+
};
|
|
12032
|
+
}
|
|
12033
|
+
/**
|
|
12034
|
+
* Re-embed every listed track from its key frame.
|
|
12035
|
+
*
|
|
12036
|
+
* Every skip is COUNTED and the counts are returned, because the caller's next
|
|
12037
|
+
* decision — whether the index is now consistent enough to trust — depends on
|
|
12038
|
+
* knowing how much was not rebuilt, not just how much was.
|
|
12039
|
+
*/
|
|
12040
|
+
async function rebuildObjectEmbeddings(deps, options = {}) {
|
|
12041
|
+
const margin = options.cropMargin ?? .2;
|
|
12042
|
+
const square = options.square ?? true;
|
|
12043
|
+
const maxTracks = options.maxTracks ?? Number.POSITIVE_INFINITY;
|
|
12044
|
+
const trackIds = await deps.listTrackIds();
|
|
12045
|
+
const considered = trackIds.slice(0, Number.isFinite(maxTracks) ? maxTracks : void 0);
|
|
12046
|
+
let rebuilt = 0;
|
|
12047
|
+
let missingKeyFrame = 0;
|
|
12048
|
+
let missingBbox = 0;
|
|
12049
|
+
let failed = 0;
|
|
12050
|
+
for (const trackId of considered) try {
|
|
12051
|
+
const source = await deps.loadSource(trackId);
|
|
12052
|
+
if (source === null) {
|
|
12053
|
+
missingKeyFrame += 1;
|
|
12054
|
+
continue;
|
|
12055
|
+
}
|
|
12056
|
+
if (source.keyFrame === null) {
|
|
12057
|
+
missingKeyFrame += 1;
|
|
12058
|
+
continue;
|
|
12059
|
+
}
|
|
12060
|
+
if (source.bbox === null || source.bbox.w <= 0 || source.bbox.h <= 0) {
|
|
12061
|
+
missingBbox += 1;
|
|
12062
|
+
continue;
|
|
12063
|
+
}
|
|
12064
|
+
const meta = await sharp(source.keyFrame).metadata();
|
|
12065
|
+
if (meta.width === void 0 || meta.height === void 0) {
|
|
12066
|
+
failed += 1;
|
|
12067
|
+
continue;
|
|
12068
|
+
}
|
|
12069
|
+
const window = cropWindow(source.bbox, meta.width, meta.height, {
|
|
12070
|
+
margin,
|
|
12071
|
+
square
|
|
12072
|
+
});
|
|
12073
|
+
const cut = await cropFromJpeg(source.keyFrame, window);
|
|
12074
|
+
if (cut === null) {
|
|
12075
|
+
failed += 1;
|
|
12076
|
+
continue;
|
|
12077
|
+
}
|
|
12078
|
+
const embedding = await deps.encode(cut.crop, cut.width, cut.height);
|
|
12079
|
+
if (embedding.length === 0) {
|
|
12080
|
+
failed += 1;
|
|
12081
|
+
continue;
|
|
12082
|
+
}
|
|
12083
|
+
await deps.store(source, embedding);
|
|
12084
|
+
rebuilt += 1;
|
|
12085
|
+
} catch (err) {
|
|
12086
|
+
failed += 1;
|
|
12087
|
+
deps.logger.warn("embedding rebuild failed for a track", { meta: {
|
|
12088
|
+
trackId,
|
|
12089
|
+
error: String(err)
|
|
12090
|
+
} });
|
|
12091
|
+
}
|
|
12092
|
+
const complete = considered.length === trackIds.length;
|
|
12093
|
+
deps.logger.info("object embedding rebuild finished", { meta: {
|
|
12094
|
+
scanned: considered.length,
|
|
12095
|
+
rebuilt,
|
|
12096
|
+
missingKeyFrame,
|
|
12097
|
+
missingBbox,
|
|
12098
|
+
failed,
|
|
12099
|
+
complete,
|
|
12100
|
+
cropMargin: margin,
|
|
12101
|
+
square
|
|
12102
|
+
} });
|
|
12103
|
+
return {
|
|
12104
|
+
scanned: considered.length,
|
|
12105
|
+
rebuilt,
|
|
12106
|
+
missingKeyFrame,
|
|
12107
|
+
missingBbox,
|
|
12108
|
+
failed,
|
|
12109
|
+
complete
|
|
12110
|
+
};
|
|
12111
|
+
}
|
|
12112
|
+
/**
|
|
12113
|
+
* Read a normalised bbox off an object event.
|
|
12114
|
+
*
|
|
12115
|
+
* Events carry the box in FRAME PIXELS alongside `frameWidth`/`frameHeight`,
|
|
12116
|
+
* not normalised — normalising here rather than at the call site keeps the
|
|
12117
|
+
* conversion in one place, next to the code that consumes it.
|
|
12118
|
+
* Returns null when the frame dimensions are missing, because a box without
|
|
12119
|
+
* them cannot be placed on a differently-sized key frame.
|
|
12120
|
+
*/
|
|
12121
|
+
function normaliseEventBbox(event) {
|
|
12122
|
+
const { bbox, frameWidth, frameHeight } = event;
|
|
12123
|
+
if (bbox === void 0) return null;
|
|
12124
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
12125
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
12126
|
+
if (bbox.w <= 0 || bbox.h <= 0) return null;
|
|
12127
|
+
return {
|
|
12128
|
+
x: bbox.x / frameWidth,
|
|
12129
|
+
y: bbox.y / frameHeight,
|
|
12130
|
+
w: bbox.w / frameWidth,
|
|
12131
|
+
h: bbox.h / frameHeight
|
|
12132
|
+
};
|
|
12133
|
+
}
|
|
12134
|
+
//#endregion
|
|
11952
12135
|
//#region src/pipeline-analytics/event-media-handler.ts
|
|
11953
12136
|
var CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
11954
12137
|
/** Default / clamp bounds for the `thumb` variant edge (px). Mirrors
|
|
@@ -14280,42 +14463,6 @@ async function encodeKeyFrameVariants(native) {
|
|
|
14280
14463
|
};
|
|
14281
14464
|
}
|
|
14282
14465
|
//#endregion
|
|
14283
|
-
//#region src/pipeline-analytics/pipeline/bbox-pad.ts
|
|
14284
|
-
/**
|
|
14285
|
-
* Pad a normalized bounding box by a fractional amount relative to its own
|
|
14286
|
-
* dimensions. All coordinates remain in the [0, 1] unit-square.
|
|
14287
|
-
*
|
|
14288
|
-
* Formula (before clamping):
|
|
14289
|
-
* newX = x - padding * w
|
|
14290
|
-
* newW = w + 2 * padding * w (i.e. w * (1 + 2 * padding))
|
|
14291
|
-
* newY = y - padding * h
|
|
14292
|
-
* newH = h + 2 * padding * h
|
|
14293
|
-
*
|
|
14294
|
-
* padding === 0 is the identity operation. Pure, immutable — always returns a
|
|
14295
|
-
* new object.
|
|
14296
|
-
*
|
|
14297
|
-
* NOTE: clamping SHIFTS, it does not truncate — a window that overflows a
|
|
14298
|
-
* bound slides inward and keeps the extent that was asked for, on either side.
|
|
14299
|
-
* It shrinks only when the padded window is larger than the frame itself
|
|
14300
|
-
* would (e.g. a left-hugging box gains a bit more right-side context). This is
|
|
14301
|
-
* acceptable for detection crops (more context, never out of [0,1]); the
|
|
14302
|
-
* downstream `extractCrop` clamps to pixel bounds again regardless.
|
|
14303
|
-
*/
|
|
14304
|
-
function padBbox(bbox, padding) {
|
|
14305
|
-
const rawX = bbox.x - padding * bbox.w;
|
|
14306
|
-
const rawY = bbox.y - padding * bbox.h;
|
|
14307
|
-
const rawW = bbox.w * (1 + 2 * padding);
|
|
14308
|
-
const rawH = bbox.h * (1 + 2 * padding);
|
|
14309
|
-
const w = Math.min(rawW, 1);
|
|
14310
|
-
const h = Math.min(rawH, 1);
|
|
14311
|
-
return {
|
|
14312
|
-
x: Math.min(Math.max(0, rawX), 1 - w),
|
|
14313
|
-
y: Math.min(Math.max(0, rawY), 1 - h),
|
|
14314
|
-
w,
|
|
14315
|
-
h
|
|
14316
|
-
};
|
|
14317
|
-
}
|
|
14318
|
-
//#endregion
|
|
14319
14466
|
//#region src/pipeline-analytics/pipeline/keyframe-crop.ts
|
|
14320
14467
|
function createKeyFrameCrop(deps) {
|
|
14321
14468
|
return async (input) => {
|
|
@@ -22687,6 +22834,15 @@ function buildStationarySnapshot(input) {
|
|
|
22687
22834
|
//#region src/pipeline-analytics/index.ts
|
|
22688
22835
|
/** This addon's manifest id — the owner of the fixed Notification Center
|
|
22689
22836
|
* integration and of the alarm device under it. */
|
|
22837
|
+
/**
|
|
22838
|
+
* How long the active encoder's model id is trusted without re-asking.
|
|
22839
|
+
*
|
|
22840
|
+
* `embeddingEncoder.getInfo` measured 1.42s on the live hub — a cross-process
|
|
22841
|
+
* call into an addon busy with live inference — for a value that changes only
|
|
22842
|
+
* when an operator swaps encoders. A minute is short enough that a swap takes
|
|
22843
|
+
* effect without a restart and long enough that no search pays for it twice.
|
|
22844
|
+
*/
|
|
22845
|
+
var ENCODER_INFO_TTL_MS = 6e4;
|
|
22690
22846
|
var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
22691
22847
|
/** Operator-visible name of the fixed integration. */
|
|
22692
22848
|
var NC_INTEGRATION_NAME = "Notification Center";
|
|
@@ -22874,6 +23030,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
22874
23030
|
plateStore = null;
|
|
22875
23031
|
plateRecognizer = null;
|
|
22876
23032
|
objectEmbeddingStore = null;
|
|
23033
|
+
/** Cached active-encoder model id — see `resolveEncoderModelId`. */
|
|
23034
|
+
encoderInfoCache = null;
|
|
22877
23035
|
/** Two-plane detail scheduler/dispatcher: per-track on-demand face/clip/
|
|
22878
23036
|
* classifier/plate enrichment via `pipelineRunner.runDetailSubtree`. Replaces
|
|
22879
23037
|
* the per-frame child consumption the executor no longer emits. */
|
|
@@ -26505,6 +26663,117 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26505
26663
|
* with each winner's deviceId + trackId filter) and join score.
|
|
26506
26664
|
* 5. Return at most `limit` ScoredObjectEvents sorted descending by score.
|
|
26507
26665
|
*/
|
|
26666
|
+
/**
|
|
26667
|
+
* The active encoder's model id, cached for {@link ENCODER_INFO_TTL_MS}.
|
|
26668
|
+
*
|
|
26669
|
+
* Refreshed rather than pinned forever so an operator swapping the encoder
|
|
26670
|
+
* does not need a restart to make search gate on the new model — the window
|
|
26671
|
+
* where the old id is used is bounded by the TTL, and during it a search
|
|
26672
|
+
* simply ranks against the previous model's vectors, which is what it would
|
|
26673
|
+
* have done anyway until the new ones exist.
|
|
26674
|
+
*/
|
|
26675
|
+
async resolveEncoderModelId(api) {
|
|
26676
|
+
const now = Date.now();
|
|
26677
|
+
if (this.encoderInfoCache !== null && now - this.encoderInfoCache.at < ENCODER_INFO_TTL_MS) return this.encoderInfoCache.modelId;
|
|
26678
|
+
const info = await api.embeddingEncoder.getInfo.query().catch((err) => {
|
|
26679
|
+
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
26680
|
+
return null;
|
|
26681
|
+
});
|
|
26682
|
+
if (info === null) return this.encoderInfoCache?.modelId ?? null;
|
|
26683
|
+
this.encoderInfoCache = {
|
|
26684
|
+
modelId: info.modelId,
|
|
26685
|
+
at: now
|
|
26686
|
+
};
|
|
26687
|
+
return info.modelId;
|
|
26688
|
+
}
|
|
26689
|
+
/**
|
|
26690
|
+
* Re-embed stored tracks from their key frames — the tool that makes the crop
|
|
26691
|
+
* policy tunable.
|
|
26692
|
+
*
|
|
26693
|
+
* Pixels come from each track's `keyFrame` (the FULL frame at native
|
|
26694
|
+
* resolution, captured at the best detection) plus that detection's bbox, so
|
|
26695
|
+
* the window can be WIDENED. The tight crop originally embedded cannot be
|
|
26696
|
+
* widened after the fact.
|
|
26697
|
+
*/
|
|
26698
|
+
async rebuildObjectEmbeddings(input) {
|
|
26699
|
+
const store = this.objectEmbeddingStore;
|
|
26700
|
+
const trackStore = this.trackStore;
|
|
26701
|
+
const mediaStore = this.mediaStore;
|
|
26702
|
+
const eventStore = this.eventStore;
|
|
26703
|
+
const api = this.ctx.api;
|
|
26704
|
+
const empty = {
|
|
26705
|
+
scanned: 0,
|
|
26706
|
+
rebuilt: 0,
|
|
26707
|
+
missingKeyFrame: 0,
|
|
26708
|
+
missingBbox: 0,
|
|
26709
|
+
failed: 0,
|
|
26710
|
+
complete: true
|
|
26711
|
+
};
|
|
26712
|
+
if (!store || !trackStore || !mediaStore || !eventStore || !api) {
|
|
26713
|
+
this.ctx.logger.warn("rebuildObjectEmbeddings: analytics stores unavailable");
|
|
26714
|
+
return empty;
|
|
26715
|
+
}
|
|
26716
|
+
const modelId = await this.resolveEncoderModelId(api) ?? "unknown";
|
|
26717
|
+
const deviceIds = input.deviceId !== void 0 ? [input.deviceId] : await trackStore.listDeviceIds();
|
|
26718
|
+
return rebuildObjectEmbeddings({
|
|
26719
|
+
listTrackIds: async () => {
|
|
26720
|
+
return (await trackStore.queryRecent({
|
|
26721
|
+
deviceIds,
|
|
26722
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
26723
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
26724
|
+
limit: input.maxTracks ?? 500
|
|
26725
|
+
})).tracks.map((t) => t.trackId);
|
|
26726
|
+
},
|
|
26727
|
+
loadSource: async (trackId) => {
|
|
26728
|
+
const media = await mediaStore.listByOwner("track", trackId);
|
|
26729
|
+
const keyFrameFile = media.find((m) => m.kind === "keyFrame");
|
|
26730
|
+
const cropFile = media.find((m) => m.kind === "thumbnail");
|
|
26731
|
+
const bestEventId = (await eventStore.peakForTrack(trackId)).bestEventId;
|
|
26732
|
+
const events = await eventStore.queryObjectByTrackIds([trackId]);
|
|
26733
|
+
const best = events.find((e) => e.id === bestEventId) ?? events[0];
|
|
26734
|
+
if (best === void 0) return null;
|
|
26735
|
+
return {
|
|
26736
|
+
trackId,
|
|
26737
|
+
deviceId: best.deviceId,
|
|
26738
|
+
timestamp: best.timestamp,
|
|
26739
|
+
className: best.className,
|
|
26740
|
+
confidence: best.confidence ?? 0,
|
|
26741
|
+
keyFrame: keyFrameFile ? Buffer.from(keyFrameFile.base64, "base64") : null,
|
|
26742
|
+
bbox: normaliseEventBbox(best),
|
|
26743
|
+
...cropFile !== void 0 ? { mediaKey: cropFile.key } : {},
|
|
26744
|
+
...keyFrameFile !== void 0 ? { keyFrameMediaKey: keyFrameFile.key } : {}
|
|
26745
|
+
};
|
|
26746
|
+
},
|
|
26747
|
+
encode: async (crop, width, height) => {
|
|
26748
|
+
return (await api.embeddingEncoder.encode.query({
|
|
26749
|
+
crop: new Uint8Array(crop),
|
|
26750
|
+
width,
|
|
26751
|
+
height
|
|
26752
|
+
})).embedding;
|
|
26753
|
+
},
|
|
26754
|
+
store: async (source, embedding) => {
|
|
26755
|
+
await store.upsertIfBetter({
|
|
26756
|
+
trackId: source.trackId,
|
|
26757
|
+
deviceId: source.deviceId,
|
|
26758
|
+
timestamp: source.timestamp,
|
|
26759
|
+
className: source.className,
|
|
26760
|
+
embedding,
|
|
26761
|
+
modelId,
|
|
26762
|
+
confidence: Number.POSITIVE_INFINITY,
|
|
26763
|
+
...source.mediaKey !== void 0 ? { mediaKey: source.mediaKey } : {},
|
|
26764
|
+
...source.keyFrameMediaKey !== void 0 ? { keyFrameMediaKey: source.keyFrameMediaKey } : {}
|
|
26765
|
+
});
|
|
26766
|
+
},
|
|
26767
|
+
logger: {
|
|
26768
|
+
info: (message, extras) => this.ctx.logger.info(message, extras),
|
|
26769
|
+
warn: (message, extras) => this.ctx.logger.warn(message, extras)
|
|
26770
|
+
}
|
|
26771
|
+
}, {
|
|
26772
|
+
...input.cropMargin !== void 0 ? { cropMargin: input.cropMargin } : {},
|
|
26773
|
+
...input.square !== void 0 ? { square: input.square } : {},
|
|
26774
|
+
...input.maxTracks !== void 0 ? { maxTracks: input.maxTracks } : {}
|
|
26775
|
+
});
|
|
26776
|
+
}
|
|
26508
26777
|
async searchObjectEvents(input) {
|
|
26509
26778
|
const store = this.objectEmbeddingStore;
|
|
26510
26779
|
if (!store) return [];
|
|
@@ -26516,10 +26785,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26516
26785
|
});
|
|
26517
26786
|
if (!encodeResult) return [];
|
|
26518
26787
|
const queryVec = new Float32Array(encodeResult.embedding);
|
|
26519
|
-
const
|
|
26520
|
-
this.ctx.logger.warn("searchObjectEvents: getInfo failed, proceeding without modelId gate", { meta: { error: String(err) } });
|
|
26521
|
-
return null;
|
|
26522
|
-
});
|
|
26788
|
+
const encoderModelId = await this.resolveEncoderModelId(api);
|
|
26523
26789
|
const search = await store.searchByVector({
|
|
26524
26790
|
vector: queryVec,
|
|
26525
26791
|
limit: input.limit,
|
|
@@ -26528,7 +26794,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26528
26794
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
26529
26795
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
26530
26796
|
...input.classFilter !== void 0 ? { className: input.classFilter } : {},
|
|
26531
|
-
...
|
|
26797
|
+
...encoderModelId !== null ? { modelId: encoderModelId } : {}
|
|
26532
26798
|
});
|
|
26533
26799
|
if (search.truncated) this.ctx.logger.warn("searchObjectEvents: candidate pool truncated", {
|
|
26534
26800
|
meta: {
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CVOC4n3K.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.39",
|
|
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",
|