@camstack/addon-post-analysis 1.1.24 → 1.1.25
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-B0VgBdBN.js → dist-CCC79h7t.js} +1 -0
- package/dist/{dist-DRK78NMq.mjs → dist-Csk_yJr_.mjs} +1 -0
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/enrichment-engine/index.js +2 -2
- package/dist/enrichment-engine/index.mjs +1 -1
- package/dist/{node-Cx0LM0Sq.js → node-CIEkEv1F.js} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BsRL9Bh-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cb6Z2H5K.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-sO1Drmtf.mjs → hostInit-BrqCuPYQ.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +331 -73
- package/dist/pipeline-analytics/index.mjs +329 -71
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/dist/{resolve-frame-DBXwF5fk.js → resolve-frame-sKYbstL-.js} +1 -1
- package/package.json +1 -1
|
@@ -10585,6 +10585,7 @@ var PipelineAddonSchemaSchema = object({
|
|
|
10585
10585
|
defaultModelId: string(),
|
|
10586
10586
|
defaultModelIdByFormat: record(string(), string()).optional(),
|
|
10587
10587
|
enabledByDefault: boolean().optional(),
|
|
10588
|
+
backfillIntoExistingOverrides: boolean().optional(),
|
|
10588
10589
|
defaultConfidence: number(),
|
|
10589
10590
|
group: string().optional(),
|
|
10590
10591
|
configSchema: array(ConfigFieldBridge).readonly().optional()
|
|
@@ -10563,6 +10563,7 @@ var PipelineAddonSchemaSchema = object({
|
|
|
10563
10563
|
defaultModelId: string(),
|
|
10564
10564
|
defaultModelIdByFormat: record(string(), string()).optional(),
|
|
10565
10565
|
enabledByDefault: boolean().optional(),
|
|
10566
|
+
backfillIntoExistingOverrides: boolean().optional(),
|
|
10566
10567
|
defaultConfidence: number(),
|
|
10567
10568
|
group: string().optional(),
|
|
10568
10569
|
configSchema: array(ConfigFieldBridge).readonly().optional()
|
|
@@ -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-CCC79h7t.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 { a as embeddingEncoderCapability, p as BaseAddon, s as hfModelUrl } from "../dist-
|
|
1
|
+
import { a as embeddingEncoderCapability, p as BaseAddon, s as hfModelUrl } from "../dist-Csk_yJr_.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_resolve_frame = require("../resolve-frame-
|
|
5
|
+
const require_dist = require("../dist-CCC79h7t.js");
|
|
6
|
+
const require_resolve_frame = require("../resolve-frame-sKYbstL-.js");
|
|
7
7
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
8
8
|
//#region src/enrichment-engine/types.ts
|
|
9
9
|
var DEFAULT_ENRICHMENT_CONFIG = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as object, S as number, T as tuple, _ as createEvent, b as array, g as asJsonObject, h as EventCategory, p as BaseAddon, w as string, x as boolean, y as _enum } from "../dist-
|
|
1
|
+
import { C as object, S as number, T as tuple, _ as createEvent, b as array, g as asJsonObject, h as EventCategory, p as BaseAddon, w as string, x as boolean, y as _enum } from "../dist-Csk_yJr_.mjs";
|
|
2
2
|
import { n as extractCrop, t as resolveFrame } from "../resolve-frame-CT1T1tWy.mjs";
|
|
3
3
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
4
4
|
//#region src/enrichment-engine/types.ts
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.1.
|
|
21
|
+
version: "1.1.41",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.1.
|
|
36
|
+
version: "1.1.33",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.1.
|
|
39
|
+
version: "1.1.41",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.1.
|
|
84
|
+
version: "1.1.33",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_resolve_frame = require("../resolve-frame-
|
|
5
|
+
const require_dist = require("../dist-CCC79h7t.js");
|
|
6
|
+
const require_resolve_frame = require("../resolve-frame-sKYbstL-.js");
|
|
7
7
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
8
8
|
let sharp = require("sharp");
|
|
9
9
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -1532,6 +1532,43 @@ var FrameProcessor = class {
|
|
|
1532
1532
|
}
|
|
1533
1533
|
};
|
|
1534
1534
|
//#endregion
|
|
1535
|
+
//#region src/pipeline-analytics/pipeline/best-detection-tracker.ts
|
|
1536
|
+
var BestDetectionTracker = class {
|
|
1537
|
+
hysteresis;
|
|
1538
|
+
minGapMs;
|
|
1539
|
+
best = /* @__PURE__ */ new Map();
|
|
1540
|
+
constructor(options = {}) {
|
|
1541
|
+
this.hysteresis = options.hysteresis ?? 0;
|
|
1542
|
+
this.minGapMs = options.minGapMs ?? 0;
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Record a detection's `confidence` (at wall-clock `timestamp`) for `trackId`.
|
|
1546
|
+
* Returns true when it becomes the track's new best — the first sighting, or a
|
|
1547
|
+
* confidence that beats the held peak by more than `hysteresis` AND respects
|
|
1548
|
+
* `minGapMs`. On acceptance the held peak is advanced to this observation.
|
|
1549
|
+
*/
|
|
1550
|
+
observe(trackId, confidence, timestamp) {
|
|
1551
|
+
const cur = this.best.get(trackId);
|
|
1552
|
+
const isNewBest = cur === void 0 || confidence > cur.confidence + this.hysteresis && timestamp - cur.atMs >= this.minGapMs;
|
|
1553
|
+
if (isNewBest) this.best.set(trackId, {
|
|
1554
|
+
confidence,
|
|
1555
|
+
atMs: timestamp
|
|
1556
|
+
});
|
|
1557
|
+
return isNewBest;
|
|
1558
|
+
}
|
|
1559
|
+
/** The held peak for a track (undefined if never observed). */
|
|
1560
|
+
peak(trackId) {
|
|
1561
|
+
return this.best.get(trackId);
|
|
1562
|
+
}
|
|
1563
|
+
/** Drop a track's peak (call at track end). */
|
|
1564
|
+
delete(trackId) {
|
|
1565
|
+
this.best.delete(trackId);
|
|
1566
|
+
}
|
|
1567
|
+
clear() {
|
|
1568
|
+
this.best.clear();
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
//#endregion
|
|
1535
1572
|
//#region src/pipeline-analytics/pipeline/native-detection.ts
|
|
1536
1573
|
/**
|
|
1537
1574
|
* Nominal frame size used to denormalize native `[0,1]` boxes when a
|
|
@@ -1801,6 +1838,16 @@ var TrackStore = class {
|
|
|
1801
1838
|
lastSnapshotAt(trackId) {
|
|
1802
1839
|
return this.active.get(trackId)?.lastSnapshotAt ?? 0;
|
|
1803
1840
|
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Seed the snapshot cadence clock (once, at track start) WITHOUT appending a
|
|
1843
|
+
* snapshot, so the first periodic snapshot fires ~snapshotIntervalMs after the
|
|
1844
|
+
* track begins rather than immediately — the `firstFrame` already covers the
|
|
1845
|
+
* track's start. No-op if a snapshot was already taken (clock already set).
|
|
1846
|
+
*/
|
|
1847
|
+
seedSnapshotClock(trackId, timestamp) {
|
|
1848
|
+
const t = this.active.get(trackId);
|
|
1849
|
+
if (t && t.lastSnapshotAt === 0) t.lastSnapshotAt = timestamp;
|
|
1850
|
+
}
|
|
1804
1851
|
getActive(deviceId) {
|
|
1805
1852
|
const out = [];
|
|
1806
1853
|
for (const t of this.active.values()) if (t.deviceId === deviceId && t.active) out.push(cloneTrack(t));
|
|
@@ -2065,6 +2112,46 @@ var MediaStore = class {
|
|
|
2065
2112
|
}
|
|
2066
2113
|
}
|
|
2067
2114
|
/**
|
|
2115
|
+
* Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
|
|
2116
|
+
* kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
|
|
2117
|
+
* each new capture replaces the previous one (blob + index row) rather than
|
|
2118
|
+
* accumulating a filmstrip the way `put` does. Deletes any existing rows of
|
|
2119
|
+
* that (owner, kind) first, then writes the fresh one. Returns the new key.
|
|
2120
|
+
*/
|
|
2121
|
+
async putReplacing(params) {
|
|
2122
|
+
const existing = await this.store.query.query({
|
|
2123
|
+
collection: MEDIA_COLLECTION,
|
|
2124
|
+
filter: { where: {
|
|
2125
|
+
ownerKind: params.ownerKind,
|
|
2126
|
+
ownerId: params.ownerId,
|
|
2127
|
+
kind: params.kind
|
|
2128
|
+
} }
|
|
2129
|
+
});
|
|
2130
|
+
const newKey = await this.put(params);
|
|
2131
|
+
for (const row of existing) {
|
|
2132
|
+
if (row.id === newKey) continue;
|
|
2133
|
+
const path = String(row.data["path"] ?? "");
|
|
2134
|
+
if (path) try {
|
|
2135
|
+
await this.storage.delete({
|
|
2136
|
+
location: "eventMedia",
|
|
2137
|
+
relativePath: path
|
|
2138
|
+
});
|
|
2139
|
+
} catch {}
|
|
2140
|
+
try {
|
|
2141
|
+
await this.store.delete.mutate({
|
|
2142
|
+
collection: MEDIA_COLLECTION,
|
|
2143
|
+
key: row.id
|
|
2144
|
+
});
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
this.logger.debug("media putReplacing: stale row delete failed", { meta: {
|
|
2147
|
+
key: row.id,
|
|
2148
|
+
error: String(err)
|
|
2149
|
+
} });
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return newKey;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2068
2155
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
2069
2156
|
* found in the index or if the blob is missing from storage.
|
|
2070
2157
|
*/
|
|
@@ -2977,7 +3064,9 @@ var EventMediaDispatcher = class {
|
|
|
2977
3064
|
}
|
|
2978
3065
|
async captureForFrame(input) {
|
|
2979
3066
|
const { deviceId, frameHandle, events, trackFrames } = input;
|
|
2980
|
-
|
|
3067
|
+
const snapshots = input.snapshots ?? [];
|
|
3068
|
+
const empty = { storedSnapshots: [] };
|
|
3069
|
+
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
2981
3070
|
let decoded;
|
|
2982
3071
|
try {
|
|
2983
3072
|
decoded = await require_resolve_frame.resolveFrame(frameHandle, {
|
|
@@ -2994,7 +3083,7 @@ var EventMediaDispatcher = class {
|
|
|
2994
3083
|
error: String(err)
|
|
2995
3084
|
}
|
|
2996
3085
|
});
|
|
2997
|
-
return;
|
|
3086
|
+
return empty;
|
|
2998
3087
|
}
|
|
2999
3088
|
if (!decoded) {
|
|
3000
3089
|
this.deps.logger.debug("event media: frame recycled before resolve", {
|
|
@@ -3004,7 +3093,7 @@ var EventMediaDispatcher = class {
|
|
|
3004
3093
|
shmId: frameHandle.shmId
|
|
3005
3094
|
}
|
|
3006
3095
|
});
|
|
3007
|
-
return;
|
|
3096
|
+
return empty;
|
|
3008
3097
|
}
|
|
3009
3098
|
if (decoded.format !== "rgb") {
|
|
3010
3099
|
this.deps.logger.debug("event media: resolved frame is not RGB", {
|
|
@@ -3014,13 +3103,87 @@ var EventMediaDispatcher = class {
|
|
|
3014
3103
|
format: decoded.format
|
|
3015
3104
|
}
|
|
3016
3105
|
});
|
|
3017
|
-
return;
|
|
3106
|
+
return empty;
|
|
3018
3107
|
}
|
|
3019
3108
|
const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
|
|
3020
3109
|
const fw = decoded.width;
|
|
3021
3110
|
const fh = decoded.height;
|
|
3022
3111
|
for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
|
|
3023
3112
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
3113
|
+
const storedSnapshots = [];
|
|
3114
|
+
for (const sn of snapshots) {
|
|
3115
|
+
const stored = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn);
|
|
3116
|
+
if (stored) storedSnapshots.push(stored);
|
|
3117
|
+
}
|
|
3118
|
+
return { storedSnapshots };
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Periodic per-track media (§5). Encodes the boxed frame ONCE and fans it out
|
|
3122
|
+
* to whichever of the three destinations is requested: an appended `snapshot`
|
|
3123
|
+
* (timeline filmstrip), the rolling `lastFrame` (overwrite), and the best
|
|
3124
|
+
* `thumbnail` (overwrite). Returns the appended snapshot for TrackStore wiring
|
|
3125
|
+
* (null when `appendSnapshot` is false or the encode failed).
|
|
3126
|
+
*/
|
|
3127
|
+
async writeTrackSnapshot(deviceId, frameData, fw, fh, sn) {
|
|
3128
|
+
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return null;
|
|
3129
|
+
let boxed;
|
|
3130
|
+
try {
|
|
3131
|
+
boxed = await drawBoxedFrame(frameData, fw, fh, [{
|
|
3132
|
+
...sn.bbox,
|
|
3133
|
+
...sn.label ? { label: sn.label } : {}
|
|
3134
|
+
}], { quality: MEDIA_QUALITY });
|
|
3135
|
+
} catch (err) {
|
|
3136
|
+
this.deps.logger.warn("event media: track snapshot encode failed", {
|
|
3137
|
+
tags: { deviceId },
|
|
3138
|
+
meta: {
|
|
3139
|
+
deviceId,
|
|
3140
|
+
trackId: sn.trackId,
|
|
3141
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3142
|
+
}
|
|
3143
|
+
});
|
|
3144
|
+
return null;
|
|
3145
|
+
}
|
|
3146
|
+
let stored = null;
|
|
3147
|
+
if (sn.appendSnapshot) try {
|
|
3148
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
3149
|
+
deviceId,
|
|
3150
|
+
ownerKind: "track",
|
|
3151
|
+
ownerId: sn.trackId,
|
|
3152
|
+
kind: "snapshot",
|
|
3153
|
+
timestamp: sn.timestamp,
|
|
3154
|
+
data: boxed
|
|
3155
|
+
});
|
|
3156
|
+
stored = {
|
|
3157
|
+
trackId: sn.trackId,
|
|
3158
|
+
mediaKey,
|
|
3159
|
+
timestamp: sn.timestamp,
|
|
3160
|
+
bbox: sn.bbox
|
|
3161
|
+
};
|
|
3162
|
+
} catch {}
|
|
3163
|
+
if (sn.rollingLastFrame) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
3164
|
+
if (sn.bestThumbnail) await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
3165
|
+
return stored;
|
|
3166
|
+
}
|
|
3167
|
+
async replaceKind(deviceId, trackId, kind, timestamp, data) {
|
|
3168
|
+
try {
|
|
3169
|
+
await this.deps.mediaStore.putReplacing({
|
|
3170
|
+
deviceId,
|
|
3171
|
+
ownerKind: "track",
|
|
3172
|
+
ownerId: trackId,
|
|
3173
|
+
kind,
|
|
3174
|
+
timestamp,
|
|
3175
|
+
data
|
|
3176
|
+
});
|
|
3177
|
+
} catch (err) {
|
|
3178
|
+
this.deps.logger.debug(`event media: ${kind} replace failed`, {
|
|
3179
|
+
tags: { deviceId },
|
|
3180
|
+
meta: {
|
|
3181
|
+
deviceId,
|
|
3182
|
+
trackId,
|
|
3183
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3024
3187
|
}
|
|
3025
3188
|
async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
|
|
3026
3189
|
const box = {
|
|
@@ -4015,11 +4178,20 @@ function resolveFaceSettings(raw) {
|
|
|
4015
4178
|
* its default — parse never throws). Mirrors `face-settings` /
|
|
4016
4179
|
* `audio-detection-settings`.
|
|
4017
4180
|
*/
|
|
4018
|
-
var MediaSettingsSchema = require_dist.object({
|
|
4019
|
-
/** Fractional padding added around a detection bbox before cropping.
|
|
4020
|
-
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4021
|
-
* max 2 (200%). */
|
|
4022
|
-
cropPadding: require_dist.number().min(0).max(2).default(.15)
|
|
4181
|
+
var MediaSettingsSchema = require_dist.object({
|
|
4182
|
+
/** Fractional padding added around a detection bbox before cropping.
|
|
4183
|
+
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4184
|
+
* max 2 (200%). */
|
|
4185
|
+
cropPadding: require_dist.number().min(0).max(2).default(.15),
|
|
4186
|
+
/** Master switch for periodic per-track snapshots (the timeline filmstrip +
|
|
4187
|
+
* the rolling `lastFrame` + the best `thumbnail`). When false, only the
|
|
4188
|
+
* per-track `firstFrame` and per-event media are produced. */
|
|
4189
|
+
saveThumbnails: require_dist.boolean().default(true),
|
|
4190
|
+
/** Cadence (ms) for the periodic per-track `snapshot` + rolling `lastFrame`.
|
|
4191
|
+
* A snapshot is captured for an active track only after this much wall-clock
|
|
4192
|
+
* has elapsed since its previous one. */
|
|
4193
|
+
snapshotIntervalMs: require_dist.number().int().min(500).max(6e4).default(5e3)
|
|
4194
|
+
});
|
|
4023
4195
|
var MEDIA_DEFAULTS = MediaSettingsSchema.parse({});
|
|
4024
4196
|
/**
|
|
4025
4197
|
* Resolve a per-device store blob into typed media settings. Unknown/invalid
|
|
@@ -4030,7 +4202,11 @@ function resolveMediaSettings(raw) {
|
|
|
4030
4202
|
const parsed = MediaSettingsSchema.shape[key].safeParse(raw[key]);
|
|
4031
4203
|
return parsed.success ? parsed.data : MEDIA_DEFAULTS[key];
|
|
4032
4204
|
};
|
|
4033
|
-
return {
|
|
4205
|
+
return {
|
|
4206
|
+
cropPadding: pick("cropPadding"),
|
|
4207
|
+
saveThumbnails: pick("saveThumbnails"),
|
|
4208
|
+
snapshotIntervalMs: pick("snapshotIntervalMs")
|
|
4209
|
+
};
|
|
4034
4210
|
}
|
|
4035
4211
|
//#endregion
|
|
4036
4212
|
//#region src/pipeline-analytics/store/identity-store.ts
|
|
@@ -4926,6 +5102,11 @@ var FaceRecognizer = class {
|
|
|
4926
5102
|
names = /* @__PURE__ */ new Map();
|
|
4927
5103
|
aggregates = /* @__PURE__ */ new Map();
|
|
4928
5104
|
bestFace = /* @__PURE__ */ new Map();
|
|
5105
|
+
/** The ONE best-detection-per-track policy, shared with the best-frame path
|
|
5106
|
+
* (`index.ts`). Face params: no hysteresis / no rate-limit — always keep the
|
|
5107
|
+
* true highest-confidence face (holding a buffer in memory is cheap, and a
|
|
5108
|
+
* track may last well under the best-frame rate-limit window). */
|
|
5109
|
+
bestTracker = new BestDetectionTracker();
|
|
4929
5110
|
constructor(deps) {
|
|
4930
5111
|
this.deps = deps;
|
|
4931
5112
|
}
|
|
@@ -4959,55 +5140,25 @@ var FaceRecognizer = class {
|
|
|
4959
5140
|
threshold: settings.similarityThreshold,
|
|
4960
5141
|
margin: settings.margin
|
|
4961
5142
|
}) : /* @__PURE__ */ new Map();
|
|
5143
|
+
const labelWork = [];
|
|
4962
5144
|
for (const c of candidates) {
|
|
4963
5145
|
const match = matches.get(c.trackId) ?? null;
|
|
4964
5146
|
const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
|
|
4965
5147
|
this.aggregates.set(c.trackId, state);
|
|
4966
|
-
if (changed && state.assignedIdentityId !== null) {
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
trackId: c.trackId
|
|
4972
|
-
},
|
|
4973
|
-
meta: {
|
|
4974
|
-
identityId: state.assignedIdentityId,
|
|
4975
|
-
name: name ?? null,
|
|
4976
|
-
score: match?.score ?? null
|
|
4977
|
-
}
|
|
4978
|
-
});
|
|
4979
|
-
if (name !== void 0) {
|
|
4980
|
-
try {
|
|
4981
|
-
await this.deps.trackStore.setLabel(c.trackId, name);
|
|
4982
|
-
} catch (err) {
|
|
4983
|
-
this.deps.logger.warn("setLabel failed", {
|
|
4984
|
-
tags: { deviceId: input.deviceId },
|
|
4985
|
-
meta: {
|
|
4986
|
-
trackId: c.trackId,
|
|
4987
|
-
error: String(err)
|
|
4988
|
-
}
|
|
4989
|
-
});
|
|
4990
|
-
}
|
|
4991
|
-
try {
|
|
4992
|
-
await this.deps.eventStore.setLabelForTrack(c.trackId, name);
|
|
4993
|
-
} catch (err) {
|
|
4994
|
-
this.deps.logger.warn("setLabelForTrack failed", {
|
|
4995
|
-
tags: { deviceId: input.deviceId },
|
|
4996
|
-
meta: {
|
|
4997
|
-
trackId: c.trackId,
|
|
4998
|
-
error: String(err)
|
|
4999
|
-
}
|
|
5000
|
-
});
|
|
5001
|
-
}
|
|
5002
|
-
}
|
|
5003
|
-
}
|
|
5148
|
+
if (changed && state.assignedIdentityId !== null) labelWork.push({
|
|
5149
|
+
trackId: c.trackId,
|
|
5150
|
+
assignedIdentityId: state.assignedIdentityId,
|
|
5151
|
+
matchScore: match?.score ?? null
|
|
5152
|
+
});
|
|
5004
5153
|
const recognizedIdentityId = state.assignedIdentityId ?? match?.identityId ?? void 0;
|
|
5005
5154
|
const held = this.bestFace.get(c.trackId);
|
|
5006
|
-
|
|
5155
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp);
|
|
5156
|
+
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
5157
|
+
if (isNewBest || needsCrop) {
|
|
5007
5158
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
5008
5159
|
let crop;
|
|
5009
|
-
if (input.frameHandle !== void 0) try {
|
|
5010
|
-
crop = await this.deps.captureCrop(input.frameHandle,
|
|
5160
|
+
if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
5161
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
5011
5162
|
} catch (err) {
|
|
5012
5163
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
5013
5164
|
tags: { deviceId: input.deviceId },
|
|
@@ -5017,14 +5168,20 @@ var FaceRecognizer = class {
|
|
|
5017
5168
|
}
|
|
5018
5169
|
});
|
|
5019
5170
|
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5171
|
+
if (isNewBest) {
|
|
5172
|
+
const bestCrop = crop ?? held?.crop;
|
|
5173
|
+
this.bestFace.set(c.trackId, {
|
|
5174
|
+
score: c.confidence,
|
|
5175
|
+
embedding: c.embedding,
|
|
5176
|
+
embeddingModelId: c.embeddingModelId,
|
|
5177
|
+
bbox: cropBbox,
|
|
5178
|
+
timestamp: input.timestamp,
|
|
5179
|
+
...bestCrop !== void 0 ? { crop: bestCrop } : {},
|
|
5180
|
+
...recognizedIdentityId !== void 0 ? { recognizedIdentityId } : {}
|
|
5181
|
+
});
|
|
5182
|
+
} else if (crop !== void 0 && held !== void 0) this.bestFace.set(c.trackId, {
|
|
5183
|
+
...held,
|
|
5184
|
+
crop
|
|
5028
5185
|
});
|
|
5029
5186
|
this.deps.logger.debug("face: best-face held", {
|
|
5030
5187
|
tags: {
|
|
@@ -5033,15 +5190,53 @@ var FaceRecognizer = class {
|
|
|
5033
5190
|
},
|
|
5034
5191
|
meta: {
|
|
5035
5192
|
score: c.confidence,
|
|
5036
|
-
|
|
5193
|
+
isNewBest,
|
|
5194
|
+
hasCrop: this.bestFace.get(c.trackId)?.crop !== void 0,
|
|
5037
5195
|
recognizedIdentityId: recognizedIdentityId ?? null
|
|
5038
5196
|
}
|
|
5039
5197
|
});
|
|
5040
|
-
} else if (recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5198
|
+
} else if (held !== void 0 && recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5041
5199
|
...held,
|
|
5042
5200
|
recognizedIdentityId
|
|
5043
5201
|
});
|
|
5044
5202
|
}
|
|
5203
|
+
for (const work of labelWork) {
|
|
5204
|
+
const name = this.names.get(work.assignedIdentityId);
|
|
5205
|
+
this.deps.logger.info("face: identity assigned", {
|
|
5206
|
+
tags: {
|
|
5207
|
+
deviceId: input.deviceId,
|
|
5208
|
+
trackId: work.trackId
|
|
5209
|
+
},
|
|
5210
|
+
meta: {
|
|
5211
|
+
identityId: work.assignedIdentityId,
|
|
5212
|
+
name: name ?? null,
|
|
5213
|
+
score: work.matchScore
|
|
5214
|
+
}
|
|
5215
|
+
});
|
|
5216
|
+
if (name === void 0) continue;
|
|
5217
|
+
try {
|
|
5218
|
+
await this.deps.trackStore.setLabel(work.trackId, name);
|
|
5219
|
+
} catch (err) {
|
|
5220
|
+
this.deps.logger.warn("setLabel failed", {
|
|
5221
|
+
tags: { deviceId: input.deviceId },
|
|
5222
|
+
meta: {
|
|
5223
|
+
trackId: work.trackId,
|
|
5224
|
+
error: String(err)
|
|
5225
|
+
}
|
|
5226
|
+
});
|
|
5227
|
+
}
|
|
5228
|
+
try {
|
|
5229
|
+
await this.deps.eventStore.setLabelForTrack(work.trackId, name);
|
|
5230
|
+
} catch (err) {
|
|
5231
|
+
this.deps.logger.warn("setLabelForTrack failed", {
|
|
5232
|
+
tags: { deviceId: input.deviceId },
|
|
5233
|
+
meta: {
|
|
5234
|
+
trackId: work.trackId,
|
|
5235
|
+
error: String(err)
|
|
5236
|
+
}
|
|
5237
|
+
});
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5045
5240
|
}
|
|
5046
5241
|
/**
|
|
5047
5242
|
* Persist the held best face for a finished track as ONE FaceStore buffer
|
|
@@ -5052,6 +5247,7 @@ var FaceRecognizer = class {
|
|
|
5052
5247
|
const held = this.bestFace.get(trackId);
|
|
5053
5248
|
this.aggregates.delete(trackId);
|
|
5054
5249
|
this.bestFace.delete(trackId);
|
|
5250
|
+
this.bestTracker.delete(trackId);
|
|
5055
5251
|
if (held === void 0) {
|
|
5056
5252
|
this.deps.logger.debug("face: track ended without a held face", { tags: {
|
|
5057
5253
|
deviceId,
|
|
@@ -5727,6 +5923,12 @@ function createEventMediaHandler(deps) {
|
|
|
5727
5923
|
*/
|
|
5728
5924
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
5729
5925
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
5926
|
+
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
5927
|
+
* detection confidence beats the held best by at least this margin (hysteresis
|
|
5928
|
+
* so jitter around a plateau doesn't churn the write). */
|
|
5929
|
+
var BEST_FRAME_HYSTERESIS = .05;
|
|
5930
|
+
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
5931
|
+
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
5730
5932
|
/** Cluster setting key (in the centralized addon store) selecting the SINGLE
|
|
5731
5933
|
* node that runs post-analysis (event/media/audio/motion generation). All
|
|
5732
5934
|
* other nodes are fully inert. No multi-node balancing. Default: the hub. */
|
|
@@ -5838,6 +6040,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5838
6040
|
mediaCacheByDevice = /* @__PURE__ */ new Map();
|
|
5839
6041
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
5840
6042
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
6043
|
+
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
6044
|
+
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
6045
|
+
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
6046
|
+
* capture re-encodes a full boxed frame. */
|
|
6047
|
+
bestFrameTracker = new BestDetectionTracker({
|
|
6048
|
+
hysteresis: BEST_FRAME_HYSTERESIS,
|
|
6049
|
+
minGapMs: BEST_FRAME_MIN_GAP_MS
|
|
6050
|
+
});
|
|
5841
6051
|
shuttingDown = false;
|
|
5842
6052
|
/** True only on the cluster's designated post-processing node. When false the
|
|
5843
6053
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -5871,7 +6081,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
5871
6081
|
let storage = this.ctx.kernel.storage;
|
|
5872
6082
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
5873
6083
|
if (mediaRoot) {
|
|
5874
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
6084
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-CIEkEv1F.js"));
|
|
5875
6085
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
5876
6086
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
5877
6087
|
}
|
|
@@ -6381,6 +6591,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6381
6591
|
this.processors.clear();
|
|
6382
6592
|
this.lastActiveTrackIds.clear();
|
|
6383
6593
|
this.dropoutSkipsByKey.clear();
|
|
6594
|
+
this.bestFrameTracker.clear();
|
|
6384
6595
|
this.levelStateByDevice.clear();
|
|
6385
6596
|
this.settingsCacheByDevice.clear();
|
|
6386
6597
|
this.sensitivityCacheByDevice.clear();
|
|
@@ -6487,12 +6698,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6487
6698
|
className: t.className,
|
|
6488
6699
|
source
|
|
6489
6700
|
} });
|
|
6490
|
-
if (this.eventMediaDispatcher && frameHandle)
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6701
|
+
if (this.eventMediaDispatcher && frameHandle) {
|
|
6702
|
+
firstFrameTargets.push({
|
|
6703
|
+
trackId: id,
|
|
6704
|
+
timestamp: result.timestamp,
|
|
6705
|
+
bbox: { ...t.bbox },
|
|
6706
|
+
...t.label ? { label: t.label } : {}
|
|
6707
|
+
});
|
|
6708
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp);
|
|
6709
|
+
}
|
|
6496
6710
|
this.ctx.eventBus.emit({
|
|
6497
6711
|
id: `pa-${(0, node_crypto.randomUUID)()}`,
|
|
6498
6712
|
timestamp: new Date(result.timestamp),
|
|
@@ -6565,11 +6779,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6565
6779
|
let plateCrops = 0;
|
|
6566
6780
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
6567
6781
|
else plateCrops += 1;
|
|
6568
|
-
|
|
6782
|
+
const snapshotTargets = this.buildSnapshotTargets(result.tracked, result.timestamp, mediaSettings);
|
|
6783
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
|
|
6569
6784
|
log.info("media capture", { meta: {
|
|
6570
6785
|
source,
|
|
6571
6786
|
events: eventTargets.length,
|
|
6572
6787
|
trackFrames: firstFrameTargets.length,
|
|
6788
|
+
snapshots: snapshotTargets.length,
|
|
6573
6789
|
faceCrops,
|
|
6574
6790
|
plateCrops
|
|
6575
6791
|
} });
|
|
@@ -6578,8 +6794,20 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6578
6794
|
frameHandle,
|
|
6579
6795
|
events: eventTargets,
|
|
6580
6796
|
trackFrames: firstFrameTargets,
|
|
6797
|
+
snapshots: snapshotTargets,
|
|
6581
6798
|
cropPadding: mediaSettings.cropPadding
|
|
6582
|
-
})
|
|
6799
|
+
}).then((res) => {
|
|
6800
|
+
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
6801
|
+
timestamp: s.timestamp,
|
|
6802
|
+
position: {
|
|
6803
|
+
x: s.bbox.x + s.bbox.w / 2,
|
|
6804
|
+
y: s.bbox.y + s.bbox.h / 2,
|
|
6805
|
+
timestamp: s.timestamp,
|
|
6806
|
+
bbox: s.bbox
|
|
6807
|
+
},
|
|
6808
|
+
mediaKey: s.mediaKey
|
|
6809
|
+
});
|
|
6810
|
+
}).catch(() => {});
|
|
6583
6811
|
}
|
|
6584
6812
|
}
|
|
6585
6813
|
if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
|
|
@@ -6712,6 +6940,35 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6712
6940
|
});
|
|
6713
6941
|
return settings;
|
|
6714
6942
|
}
|
|
6943
|
+
/**
|
|
6944
|
+
* §5 — decide which active tracks need periodic media THIS frame. Pure over
|
|
6945
|
+
* TrackStore.lastSnapshotAt + the per-track best-confidence map:
|
|
6946
|
+
* • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
|
|
6947
|
+
* the snapshotIntervalMs cadence, gated by `saveThumbnails`;
|
|
6948
|
+
* • `thumbnail` (best) fires when confidence beats the held best by the
|
|
6949
|
+
* hysteresis margin, rate-limited to one per BEST_FRAME_MIN_GAP_MS, and is
|
|
6950
|
+
* NOT gated by saveThumbnails (a single best still-frame is always useful).
|
|
6951
|
+
* The per-track best-confidence map is updated here as a side effect.
|
|
6952
|
+
*/
|
|
6953
|
+
buildSnapshotTargets(tracked, timestamp, media) {
|
|
6954
|
+
const targets = [];
|
|
6955
|
+
for (const t of tracked) {
|
|
6956
|
+
const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
|
|
6957
|
+
const dueSnapshot = media.saveThumbnails && lastSnap > 0 && timestamp - lastSnap >= media.snapshotIntervalMs;
|
|
6958
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp);
|
|
6959
|
+
if (!dueSnapshot && !isNewBest) continue;
|
|
6960
|
+
targets.push({
|
|
6961
|
+
trackId: t.trackId,
|
|
6962
|
+
timestamp,
|
|
6963
|
+
bbox: { ...t.bbox },
|
|
6964
|
+
...t.label ? { label: t.label } : {},
|
|
6965
|
+
appendSnapshot: dueSnapshot,
|
|
6966
|
+
rollingLastFrame: dueSnapshot,
|
|
6967
|
+
bestThumbnail: isNewBest
|
|
6968
|
+
});
|
|
6969
|
+
}
|
|
6970
|
+
return targets;
|
|
6971
|
+
}
|
|
6715
6972
|
async handleAudioResult(data) {
|
|
6716
6973
|
if (this.shuttingDown) return;
|
|
6717
6974
|
const { deviceId, frame } = data;
|
|
@@ -6966,6 +7223,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
6966
7223
|
});
|
|
6967
7224
|
this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
6968
7225
|
this.plateRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7226
|
+
this.bestFrameTracker.delete(t.trackId);
|
|
6969
7227
|
this.ctx.eventBus.emit({
|
|
6970
7228
|
id: `pa-end-${t.trackId}`,
|
|
6971
7229
|
timestamp: new Date(t.lastSeen),
|
|
@@ -7399,7 +7657,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
7399
7657
|
min: 500,
|
|
7400
7658
|
max: 6e4,
|
|
7401
7659
|
step: 500,
|
|
7402
|
-
default:
|
|
7660
|
+
default: 5e3,
|
|
7403
7661
|
showValue: true,
|
|
7404
7662
|
unit: "s",
|
|
7405
7663
|
displayScale: 1e3
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as object, S as number, c as pipelineAnalyticsCapability, d as zoneAnalyticsCapability, f as errMsg, h as EventCategory, i as cosineSimilarity, l as plateGalleryCapability, m as DeviceType, n as addonWidgetsSourceCapability, o as faceGalleryCapability, p as BaseAddon, r as audioMetricsCapability, t as EVENT_PAD_MS, u as videoclipsCapability, v as hydrateSchema, w as string, x as boolean } from "../dist-
|
|
1
|
+
import { C as object, S as number, c as pipelineAnalyticsCapability, d as zoneAnalyticsCapability, f as errMsg, h as EventCategory, i as cosineSimilarity, l as plateGalleryCapability, m as DeviceType, n as addonWidgetsSourceCapability, o as faceGalleryCapability, p as BaseAddon, r as audioMetricsCapability, t as EVENT_PAD_MS, u as videoclipsCapability, v as hydrateSchema, w as string, x as boolean } from "../dist-Csk_yJr_.mjs";
|
|
2
2
|
import { n as extractCrop, t as resolveFrame } from "../resolve-frame-CT1T1tWy.mjs";
|
|
3
3
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
4
4
|
import sharp from "sharp";
|
|
@@ -1527,6 +1527,43 @@ var FrameProcessor = class {
|
|
|
1527
1527
|
}
|
|
1528
1528
|
};
|
|
1529
1529
|
//#endregion
|
|
1530
|
+
//#region src/pipeline-analytics/pipeline/best-detection-tracker.ts
|
|
1531
|
+
var BestDetectionTracker = class {
|
|
1532
|
+
hysteresis;
|
|
1533
|
+
minGapMs;
|
|
1534
|
+
best = /* @__PURE__ */ new Map();
|
|
1535
|
+
constructor(options = {}) {
|
|
1536
|
+
this.hysteresis = options.hysteresis ?? 0;
|
|
1537
|
+
this.minGapMs = options.minGapMs ?? 0;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Record a detection's `confidence` (at wall-clock `timestamp`) for `trackId`.
|
|
1541
|
+
* Returns true when it becomes the track's new best — the first sighting, or a
|
|
1542
|
+
* confidence that beats the held peak by more than `hysteresis` AND respects
|
|
1543
|
+
* `minGapMs`. On acceptance the held peak is advanced to this observation.
|
|
1544
|
+
*/
|
|
1545
|
+
observe(trackId, confidence, timestamp) {
|
|
1546
|
+
const cur = this.best.get(trackId);
|
|
1547
|
+
const isNewBest = cur === void 0 || confidence > cur.confidence + this.hysteresis && timestamp - cur.atMs >= this.minGapMs;
|
|
1548
|
+
if (isNewBest) this.best.set(trackId, {
|
|
1549
|
+
confidence,
|
|
1550
|
+
atMs: timestamp
|
|
1551
|
+
});
|
|
1552
|
+
return isNewBest;
|
|
1553
|
+
}
|
|
1554
|
+
/** The held peak for a track (undefined if never observed). */
|
|
1555
|
+
peak(trackId) {
|
|
1556
|
+
return this.best.get(trackId);
|
|
1557
|
+
}
|
|
1558
|
+
/** Drop a track's peak (call at track end). */
|
|
1559
|
+
delete(trackId) {
|
|
1560
|
+
this.best.delete(trackId);
|
|
1561
|
+
}
|
|
1562
|
+
clear() {
|
|
1563
|
+
this.best.clear();
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
//#endregion
|
|
1530
1567
|
//#region src/pipeline-analytics/pipeline/native-detection.ts
|
|
1531
1568
|
/**
|
|
1532
1569
|
* Nominal frame size used to denormalize native `[0,1]` boxes when a
|
|
@@ -1796,6 +1833,16 @@ var TrackStore = class {
|
|
|
1796
1833
|
lastSnapshotAt(trackId) {
|
|
1797
1834
|
return this.active.get(trackId)?.lastSnapshotAt ?? 0;
|
|
1798
1835
|
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Seed the snapshot cadence clock (once, at track start) WITHOUT appending a
|
|
1838
|
+
* snapshot, so the first periodic snapshot fires ~snapshotIntervalMs after the
|
|
1839
|
+
* track begins rather than immediately — the `firstFrame` already covers the
|
|
1840
|
+
* track's start. No-op if a snapshot was already taken (clock already set).
|
|
1841
|
+
*/
|
|
1842
|
+
seedSnapshotClock(trackId, timestamp) {
|
|
1843
|
+
const t = this.active.get(trackId);
|
|
1844
|
+
if (t && t.lastSnapshotAt === 0) t.lastSnapshotAt = timestamp;
|
|
1845
|
+
}
|
|
1799
1846
|
getActive(deviceId) {
|
|
1800
1847
|
const out = [];
|
|
1801
1848
|
for (const t of this.active.values()) if (t.deviceId === deviceId && t.active) out.push(cloneTrack(t));
|
|
@@ -2060,6 +2107,46 @@ var MediaStore = class {
|
|
|
2060
2107
|
}
|
|
2061
2108
|
}
|
|
2062
2109
|
/**
|
|
2110
|
+
* Overwrite semantics: keep exactly ONE media entry per (ownerKind, ownerId,
|
|
2111
|
+
* kind). Used for the rolling `lastFrame` and the best `thumbnail` per track —
|
|
2112
|
+
* each new capture replaces the previous one (blob + index row) rather than
|
|
2113
|
+
* accumulating a filmstrip the way `put` does. Deletes any existing rows of
|
|
2114
|
+
* that (owner, kind) first, then writes the fresh one. Returns the new key.
|
|
2115
|
+
*/
|
|
2116
|
+
async putReplacing(params) {
|
|
2117
|
+
const existing = await this.store.query.query({
|
|
2118
|
+
collection: MEDIA_COLLECTION,
|
|
2119
|
+
filter: { where: {
|
|
2120
|
+
ownerKind: params.ownerKind,
|
|
2121
|
+
ownerId: params.ownerId,
|
|
2122
|
+
kind: params.kind
|
|
2123
|
+
} }
|
|
2124
|
+
});
|
|
2125
|
+
const newKey = await this.put(params);
|
|
2126
|
+
for (const row of existing) {
|
|
2127
|
+
if (row.id === newKey) continue;
|
|
2128
|
+
const path = String(row.data["path"] ?? "");
|
|
2129
|
+
if (path) try {
|
|
2130
|
+
await this.storage.delete({
|
|
2131
|
+
location: "eventMedia",
|
|
2132
|
+
relativePath: path
|
|
2133
|
+
});
|
|
2134
|
+
} catch {}
|
|
2135
|
+
try {
|
|
2136
|
+
await this.store.delete.mutate({
|
|
2137
|
+
collection: MEDIA_COLLECTION,
|
|
2138
|
+
key: row.id
|
|
2139
|
+
});
|
|
2140
|
+
} catch (err) {
|
|
2141
|
+
this.logger.debug("media putReplacing: stale row delete failed", { meta: {
|
|
2142
|
+
key: row.id,
|
|
2143
|
+
error: String(err)
|
|
2144
|
+
} });
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return newKey;
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2063
2150
|
* Fetch one media entry by its key (id). Returns null if the key is not
|
|
2064
2151
|
* found in the index or if the blob is missing from storage.
|
|
2065
2152
|
*/
|
|
@@ -2972,7 +3059,9 @@ var EventMediaDispatcher = class {
|
|
|
2972
3059
|
}
|
|
2973
3060
|
async captureForFrame(input) {
|
|
2974
3061
|
const { deviceId, frameHandle, events, trackFrames } = input;
|
|
2975
|
-
|
|
3062
|
+
const snapshots = input.snapshots ?? [];
|
|
3063
|
+
const empty = { storedSnapshots: [] };
|
|
3064
|
+
if (events.length === 0 && trackFrames.length === 0 && snapshots.length === 0) return empty;
|
|
2976
3065
|
let decoded;
|
|
2977
3066
|
try {
|
|
2978
3067
|
decoded = await resolveFrame(frameHandle, {
|
|
@@ -2989,7 +3078,7 @@ var EventMediaDispatcher = class {
|
|
|
2989
3078
|
error: String(err)
|
|
2990
3079
|
}
|
|
2991
3080
|
});
|
|
2992
|
-
return;
|
|
3081
|
+
return empty;
|
|
2993
3082
|
}
|
|
2994
3083
|
if (!decoded) {
|
|
2995
3084
|
this.deps.logger.debug("event media: frame recycled before resolve", {
|
|
@@ -2999,7 +3088,7 @@ var EventMediaDispatcher = class {
|
|
|
2999
3088
|
shmId: frameHandle.shmId
|
|
3000
3089
|
}
|
|
3001
3090
|
});
|
|
3002
|
-
return;
|
|
3091
|
+
return empty;
|
|
3003
3092
|
}
|
|
3004
3093
|
if (decoded.format !== "rgb") {
|
|
3005
3094
|
this.deps.logger.debug("event media: resolved frame is not RGB", {
|
|
@@ -3009,13 +3098,87 @@ var EventMediaDispatcher = class {
|
|
|
3009
3098
|
format: decoded.format
|
|
3010
3099
|
}
|
|
3011
3100
|
});
|
|
3012
|
-
return;
|
|
3101
|
+
return empty;
|
|
3013
3102
|
}
|
|
3014
3103
|
const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
|
|
3015
3104
|
const fw = decoded.width;
|
|
3016
3105
|
const fh = decoded.height;
|
|
3017
3106
|
for (const ev of events) await this.writeEventMedia(deviceId, frameData, fw, fh, ev, input.cropPadding);
|
|
3018
3107
|
for (const tf of trackFrames) await this.writeTrackFrame(deviceId, frameData, fw, fh, tf);
|
|
3108
|
+
const storedSnapshots = [];
|
|
3109
|
+
for (const sn of snapshots) {
|
|
3110
|
+
const stored = await this.writeTrackSnapshot(deviceId, frameData, fw, fh, sn);
|
|
3111
|
+
if (stored) storedSnapshots.push(stored);
|
|
3112
|
+
}
|
|
3113
|
+
return { storedSnapshots };
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Periodic per-track media (§5). Encodes the boxed frame ONCE and fans it out
|
|
3117
|
+
* to whichever of the three destinations is requested: an appended `snapshot`
|
|
3118
|
+
* (timeline filmstrip), the rolling `lastFrame` (overwrite), and the best
|
|
3119
|
+
* `thumbnail` (overwrite). Returns the appended snapshot for TrackStore wiring
|
|
3120
|
+
* (null when `appendSnapshot` is false or the encode failed).
|
|
3121
|
+
*/
|
|
3122
|
+
async writeTrackSnapshot(deviceId, frameData, fw, fh, sn) {
|
|
3123
|
+
if (!sn.appendSnapshot && !sn.rollingLastFrame && !sn.bestThumbnail) return null;
|
|
3124
|
+
let boxed;
|
|
3125
|
+
try {
|
|
3126
|
+
boxed = await drawBoxedFrame(frameData, fw, fh, [{
|
|
3127
|
+
...sn.bbox,
|
|
3128
|
+
...sn.label ? { label: sn.label } : {}
|
|
3129
|
+
}], { quality: MEDIA_QUALITY });
|
|
3130
|
+
} catch (err) {
|
|
3131
|
+
this.deps.logger.warn("event media: track snapshot encode failed", {
|
|
3132
|
+
tags: { deviceId },
|
|
3133
|
+
meta: {
|
|
3134
|
+
deviceId,
|
|
3135
|
+
trackId: sn.trackId,
|
|
3136
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
let stored = null;
|
|
3142
|
+
if (sn.appendSnapshot) try {
|
|
3143
|
+
const mediaKey = await this.deps.mediaStore.put({
|
|
3144
|
+
deviceId,
|
|
3145
|
+
ownerKind: "track",
|
|
3146
|
+
ownerId: sn.trackId,
|
|
3147
|
+
kind: "snapshot",
|
|
3148
|
+
timestamp: sn.timestamp,
|
|
3149
|
+
data: boxed
|
|
3150
|
+
});
|
|
3151
|
+
stored = {
|
|
3152
|
+
trackId: sn.trackId,
|
|
3153
|
+
mediaKey,
|
|
3154
|
+
timestamp: sn.timestamp,
|
|
3155
|
+
bbox: sn.bbox
|
|
3156
|
+
};
|
|
3157
|
+
} catch {}
|
|
3158
|
+
if (sn.rollingLastFrame) await this.replaceKind(deviceId, sn.trackId, "lastFrame", sn.timestamp, boxed);
|
|
3159
|
+
if (sn.bestThumbnail) await this.replaceKind(deviceId, sn.trackId, "thumbnail", sn.timestamp, boxed);
|
|
3160
|
+
return stored;
|
|
3161
|
+
}
|
|
3162
|
+
async replaceKind(deviceId, trackId, kind, timestamp, data) {
|
|
3163
|
+
try {
|
|
3164
|
+
await this.deps.mediaStore.putReplacing({
|
|
3165
|
+
deviceId,
|
|
3166
|
+
ownerKind: "track",
|
|
3167
|
+
ownerId: trackId,
|
|
3168
|
+
kind,
|
|
3169
|
+
timestamp,
|
|
3170
|
+
data
|
|
3171
|
+
});
|
|
3172
|
+
} catch (err) {
|
|
3173
|
+
this.deps.logger.debug(`event media: ${kind} replace failed`, {
|
|
3174
|
+
tags: { deviceId },
|
|
3175
|
+
meta: {
|
|
3176
|
+
deviceId,
|
|
3177
|
+
trackId,
|
|
3178
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3019
3182
|
}
|
|
3020
3183
|
async writeEventMedia(deviceId, frameData, fw, fh, ev, cropPadding) {
|
|
3021
3184
|
const box = {
|
|
@@ -4010,11 +4173,20 @@ function resolveFaceSettings(raw) {
|
|
|
4010
4173
|
* its default — parse never throws). Mirrors `face-settings` /
|
|
4011
4174
|
* `audio-detection-settings`.
|
|
4012
4175
|
*/
|
|
4013
|
-
var MediaSettingsSchema = object({
|
|
4014
|
-
/** Fractional padding added around a detection bbox before cropping.
|
|
4015
|
-
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4016
|
-
* max 2 (200%). */
|
|
4017
|
-
cropPadding: number().min(0).max(2).default(.15)
|
|
4176
|
+
var MediaSettingsSchema = object({
|
|
4177
|
+
/** Fractional padding added around a detection bbox before cropping.
|
|
4178
|
+
* 0 = tight crop, 0.15 = expand each side by 15% of the box dimension,
|
|
4179
|
+
* max 2 (200%). */
|
|
4180
|
+
cropPadding: number().min(0).max(2).default(.15),
|
|
4181
|
+
/** Master switch for periodic per-track snapshots (the timeline filmstrip +
|
|
4182
|
+
* the rolling `lastFrame` + the best `thumbnail`). When false, only the
|
|
4183
|
+
* per-track `firstFrame` and per-event media are produced. */
|
|
4184
|
+
saveThumbnails: boolean().default(true),
|
|
4185
|
+
/** Cadence (ms) for the periodic per-track `snapshot` + rolling `lastFrame`.
|
|
4186
|
+
* A snapshot is captured for an active track only after this much wall-clock
|
|
4187
|
+
* has elapsed since its previous one. */
|
|
4188
|
+
snapshotIntervalMs: number().int().min(500).max(6e4).default(5e3)
|
|
4189
|
+
});
|
|
4018
4190
|
var MEDIA_DEFAULTS = MediaSettingsSchema.parse({});
|
|
4019
4191
|
/**
|
|
4020
4192
|
* Resolve a per-device store blob into typed media settings. Unknown/invalid
|
|
@@ -4025,7 +4197,11 @@ function resolveMediaSettings(raw) {
|
|
|
4025
4197
|
const parsed = MediaSettingsSchema.shape[key].safeParse(raw[key]);
|
|
4026
4198
|
return parsed.success ? parsed.data : MEDIA_DEFAULTS[key];
|
|
4027
4199
|
};
|
|
4028
|
-
return {
|
|
4200
|
+
return {
|
|
4201
|
+
cropPadding: pick("cropPadding"),
|
|
4202
|
+
saveThumbnails: pick("saveThumbnails"),
|
|
4203
|
+
snapshotIntervalMs: pick("snapshotIntervalMs")
|
|
4204
|
+
};
|
|
4029
4205
|
}
|
|
4030
4206
|
//#endregion
|
|
4031
4207
|
//#region src/pipeline-analytics/store/identity-store.ts
|
|
@@ -4921,6 +5097,11 @@ var FaceRecognizer = class {
|
|
|
4921
5097
|
names = /* @__PURE__ */ new Map();
|
|
4922
5098
|
aggregates = /* @__PURE__ */ new Map();
|
|
4923
5099
|
bestFace = /* @__PURE__ */ new Map();
|
|
5100
|
+
/** The ONE best-detection-per-track policy, shared with the best-frame path
|
|
5101
|
+
* (`index.ts`). Face params: no hysteresis / no rate-limit — always keep the
|
|
5102
|
+
* true highest-confidence face (holding a buffer in memory is cheap, and a
|
|
5103
|
+
* track may last well under the best-frame rate-limit window). */
|
|
5104
|
+
bestTracker = new BestDetectionTracker();
|
|
4924
5105
|
constructor(deps) {
|
|
4925
5106
|
this.deps = deps;
|
|
4926
5107
|
}
|
|
@@ -4954,55 +5135,25 @@ var FaceRecognizer = class {
|
|
|
4954
5135
|
threshold: settings.similarityThreshold,
|
|
4955
5136
|
margin: settings.margin
|
|
4956
5137
|
}) : /* @__PURE__ */ new Map();
|
|
5138
|
+
const labelWork = [];
|
|
4957
5139
|
for (const c of candidates) {
|
|
4958
5140
|
const match = matches.get(c.trackId) ?? null;
|
|
4959
5141
|
const { state, changed } = updateTrackAggregate(this.aggregates.get(c.trackId) ?? EMPTY_AGGREGATE, match, { confirmFrames: settings.confirmFrames });
|
|
4960
5142
|
this.aggregates.set(c.trackId, state);
|
|
4961
|
-
if (changed && state.assignedIdentityId !== null) {
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
trackId: c.trackId
|
|
4967
|
-
},
|
|
4968
|
-
meta: {
|
|
4969
|
-
identityId: state.assignedIdentityId,
|
|
4970
|
-
name: name ?? null,
|
|
4971
|
-
score: match?.score ?? null
|
|
4972
|
-
}
|
|
4973
|
-
});
|
|
4974
|
-
if (name !== void 0) {
|
|
4975
|
-
try {
|
|
4976
|
-
await this.deps.trackStore.setLabel(c.trackId, name);
|
|
4977
|
-
} catch (err) {
|
|
4978
|
-
this.deps.logger.warn("setLabel failed", {
|
|
4979
|
-
tags: { deviceId: input.deviceId },
|
|
4980
|
-
meta: {
|
|
4981
|
-
trackId: c.trackId,
|
|
4982
|
-
error: String(err)
|
|
4983
|
-
}
|
|
4984
|
-
});
|
|
4985
|
-
}
|
|
4986
|
-
try {
|
|
4987
|
-
await this.deps.eventStore.setLabelForTrack(c.trackId, name);
|
|
4988
|
-
} catch (err) {
|
|
4989
|
-
this.deps.logger.warn("setLabelForTrack failed", {
|
|
4990
|
-
tags: { deviceId: input.deviceId },
|
|
4991
|
-
meta: {
|
|
4992
|
-
trackId: c.trackId,
|
|
4993
|
-
error: String(err)
|
|
4994
|
-
}
|
|
4995
|
-
});
|
|
4996
|
-
}
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
5143
|
+
if (changed && state.assignedIdentityId !== null) labelWork.push({
|
|
5144
|
+
trackId: c.trackId,
|
|
5145
|
+
assignedIdentityId: state.assignedIdentityId,
|
|
5146
|
+
matchScore: match?.score ?? null
|
|
5147
|
+
});
|
|
4999
5148
|
const recognizedIdentityId = state.assignedIdentityId ?? match?.identityId ?? void 0;
|
|
5000
5149
|
const held = this.bestFace.get(c.trackId);
|
|
5001
|
-
|
|
5150
|
+
const isNewBest = this.bestTracker.observe(c.trackId, c.confidence, input.timestamp);
|
|
5151
|
+
const needsCrop = held !== void 0 && held.crop === void 0;
|
|
5152
|
+
if (isNewBest || needsCrop) {
|
|
5002
5153
|
const cropBbox = c.faceBbox ?? c.bbox;
|
|
5003
5154
|
let crop;
|
|
5004
|
-
if (input.frameHandle !== void 0) try {
|
|
5005
|
-
crop = await this.deps.captureCrop(input.frameHandle,
|
|
5155
|
+
if (c.faceBbox !== void 0 && input.frameHandle !== void 0) try {
|
|
5156
|
+
crop = await this.deps.captureCrop(input.frameHandle, c.faceBbox, input.frameWidth, input.frameHeight, input.cropPadding) ?? void 0;
|
|
5006
5157
|
} catch (err) {
|
|
5007
5158
|
this.deps.logger.debug("FaceRecognizer crop capture failed", {
|
|
5008
5159
|
tags: { deviceId: input.deviceId },
|
|
@@ -5012,14 +5163,20 @@ var FaceRecognizer = class {
|
|
|
5012
5163
|
}
|
|
5013
5164
|
});
|
|
5014
5165
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5166
|
+
if (isNewBest) {
|
|
5167
|
+
const bestCrop = crop ?? held?.crop;
|
|
5168
|
+
this.bestFace.set(c.trackId, {
|
|
5169
|
+
score: c.confidence,
|
|
5170
|
+
embedding: c.embedding,
|
|
5171
|
+
embeddingModelId: c.embeddingModelId,
|
|
5172
|
+
bbox: cropBbox,
|
|
5173
|
+
timestamp: input.timestamp,
|
|
5174
|
+
...bestCrop !== void 0 ? { crop: bestCrop } : {},
|
|
5175
|
+
...recognizedIdentityId !== void 0 ? { recognizedIdentityId } : {}
|
|
5176
|
+
});
|
|
5177
|
+
} else if (crop !== void 0 && held !== void 0) this.bestFace.set(c.trackId, {
|
|
5178
|
+
...held,
|
|
5179
|
+
crop
|
|
5023
5180
|
});
|
|
5024
5181
|
this.deps.logger.debug("face: best-face held", {
|
|
5025
5182
|
tags: {
|
|
@@ -5028,15 +5185,53 @@ var FaceRecognizer = class {
|
|
|
5028
5185
|
},
|
|
5029
5186
|
meta: {
|
|
5030
5187
|
score: c.confidence,
|
|
5031
|
-
|
|
5188
|
+
isNewBest,
|
|
5189
|
+
hasCrop: this.bestFace.get(c.trackId)?.crop !== void 0,
|
|
5032
5190
|
recognizedIdentityId: recognizedIdentityId ?? null
|
|
5033
5191
|
}
|
|
5034
5192
|
});
|
|
5035
|
-
} else if (recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5193
|
+
} else if (held !== void 0 && recognizedIdentityId !== void 0 && held.recognizedIdentityId === void 0) this.bestFace.set(c.trackId, {
|
|
5036
5194
|
...held,
|
|
5037
5195
|
recognizedIdentityId
|
|
5038
5196
|
});
|
|
5039
5197
|
}
|
|
5198
|
+
for (const work of labelWork) {
|
|
5199
|
+
const name = this.names.get(work.assignedIdentityId);
|
|
5200
|
+
this.deps.logger.info("face: identity assigned", {
|
|
5201
|
+
tags: {
|
|
5202
|
+
deviceId: input.deviceId,
|
|
5203
|
+
trackId: work.trackId
|
|
5204
|
+
},
|
|
5205
|
+
meta: {
|
|
5206
|
+
identityId: work.assignedIdentityId,
|
|
5207
|
+
name: name ?? null,
|
|
5208
|
+
score: work.matchScore
|
|
5209
|
+
}
|
|
5210
|
+
});
|
|
5211
|
+
if (name === void 0) continue;
|
|
5212
|
+
try {
|
|
5213
|
+
await this.deps.trackStore.setLabel(work.trackId, name);
|
|
5214
|
+
} catch (err) {
|
|
5215
|
+
this.deps.logger.warn("setLabel failed", {
|
|
5216
|
+
tags: { deviceId: input.deviceId },
|
|
5217
|
+
meta: {
|
|
5218
|
+
trackId: work.trackId,
|
|
5219
|
+
error: String(err)
|
|
5220
|
+
}
|
|
5221
|
+
});
|
|
5222
|
+
}
|
|
5223
|
+
try {
|
|
5224
|
+
await this.deps.eventStore.setLabelForTrack(work.trackId, name);
|
|
5225
|
+
} catch (err) {
|
|
5226
|
+
this.deps.logger.warn("setLabelForTrack failed", {
|
|
5227
|
+
tags: { deviceId: input.deviceId },
|
|
5228
|
+
meta: {
|
|
5229
|
+
trackId: work.trackId,
|
|
5230
|
+
error: String(err)
|
|
5231
|
+
}
|
|
5232
|
+
});
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5040
5235
|
}
|
|
5041
5236
|
/**
|
|
5042
5237
|
* Persist the held best face for a finished track as ONE FaceStore buffer
|
|
@@ -5047,6 +5242,7 @@ var FaceRecognizer = class {
|
|
|
5047
5242
|
const held = this.bestFace.get(trackId);
|
|
5048
5243
|
this.aggregates.delete(trackId);
|
|
5049
5244
|
this.bestFace.delete(trackId);
|
|
5245
|
+
this.bestTracker.delete(trackId);
|
|
5050
5246
|
if (held === void 0) {
|
|
5051
5247
|
this.deps.logger.debug("face: track ended without a held face", { tags: {
|
|
5052
5248
|
deviceId,
|
|
@@ -5722,6 +5918,12 @@ function createEventMediaHandler(deps) {
|
|
|
5722
5918
|
*/
|
|
5723
5919
|
var TTL_SWEEP_INTERVAL_MS = 5e3;
|
|
5724
5920
|
var SETTINGS_CACHE_TTL_MS = 5e3;
|
|
5921
|
+
/** §5 best-frame: a track's `thumbnail` is overwritten only when the current
|
|
5922
|
+
* detection confidence beats the held best by at least this margin (hysteresis
|
|
5923
|
+
* so jitter around a plateau doesn't churn the write). */
|
|
5924
|
+
var BEST_FRAME_HYSTERESIS = .05;
|
|
5925
|
+
/** §5 best-frame: at most one best-thumbnail capture per this interval per track. */
|
|
5926
|
+
var BEST_FRAME_MIN_GAP_MS = 2e3;
|
|
5725
5927
|
/** Cluster setting key (in the centralized addon store) selecting the SINGLE
|
|
5726
5928
|
* node that runs post-analysis (event/media/audio/motion generation). All
|
|
5727
5929
|
* other nodes are fully inert. No multi-node balancing. Default: the hub. */
|
|
@@ -5833,6 +6035,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
5833
6035
|
mediaCacheByDevice = /* @__PURE__ */ new Map();
|
|
5834
6036
|
/** Consecutive detector-dropout frames skipped per (deviceId, source) key. */
|
|
5835
6037
|
dropoutSkipsByKey = /* @__PURE__ */ new Map();
|
|
6038
|
+
/** Best (highest-confidence) frame per track — drives the single overwrite
|
|
6039
|
+
* `thumbnail` (§5 best-frame). Shares the ONE best-detection policy with the
|
|
6040
|
+
* face path (`face-recognizer.ts`); rate-limited here since each best-frame
|
|
6041
|
+
* capture re-encodes a full boxed frame. */
|
|
6042
|
+
bestFrameTracker = new BestDetectionTracker({
|
|
6043
|
+
hysteresis: BEST_FRAME_HYSTERESIS,
|
|
6044
|
+
minGapMs: BEST_FRAME_MIN_GAP_MS
|
|
6045
|
+
});
|
|
5836
6046
|
shuttingDown = false;
|
|
5837
6047
|
/** True only on the cluster's designated post-processing node. When false the
|
|
5838
6048
|
* addon subscribes to NOTHING — fully inert (no event/media generation). */
|
|
@@ -6376,6 +6586,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6376
6586
|
this.processors.clear();
|
|
6377
6587
|
this.lastActiveTrackIds.clear();
|
|
6378
6588
|
this.dropoutSkipsByKey.clear();
|
|
6589
|
+
this.bestFrameTracker.clear();
|
|
6379
6590
|
this.levelStateByDevice.clear();
|
|
6380
6591
|
this.settingsCacheByDevice.clear();
|
|
6381
6592
|
this.sensitivityCacheByDevice.clear();
|
|
@@ -6482,12 +6693,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6482
6693
|
className: t.className,
|
|
6483
6694
|
source
|
|
6484
6695
|
} });
|
|
6485
|
-
if (this.eventMediaDispatcher && frameHandle)
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6696
|
+
if (this.eventMediaDispatcher && frameHandle) {
|
|
6697
|
+
firstFrameTargets.push({
|
|
6698
|
+
trackId: id,
|
|
6699
|
+
timestamp: result.timestamp,
|
|
6700
|
+
bbox: { ...t.bbox },
|
|
6701
|
+
...t.label ? { label: t.label } : {}
|
|
6702
|
+
});
|
|
6703
|
+
this.trackStore.seedSnapshotClock(id, result.timestamp);
|
|
6704
|
+
}
|
|
6491
6705
|
this.ctx.eventBus.emit({
|
|
6492
6706
|
id: `pa-${randomUUID()}`,
|
|
6493
6707
|
timestamp: new Date(result.timestamp),
|
|
@@ -6560,11 +6774,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6560
6774
|
let plateCrops = 0;
|
|
6561
6775
|
for (const crops of childCropsByEvent.values()) for (const c of crops) if (c.kind === "faceCrop") faceCrops += 1;
|
|
6562
6776
|
else plateCrops += 1;
|
|
6563
|
-
|
|
6777
|
+
const snapshotTargets = this.buildSnapshotTargets(result.tracked, result.timestamp, mediaSettings);
|
|
6778
|
+
if (eventTargets.length > 0 || firstFrameTargets.length > 0 || snapshotTargets.length > 0) {
|
|
6564
6779
|
log.info("media capture", { meta: {
|
|
6565
6780
|
source,
|
|
6566
6781
|
events: eventTargets.length,
|
|
6567
6782
|
trackFrames: firstFrameTargets.length,
|
|
6783
|
+
snapshots: snapshotTargets.length,
|
|
6568
6784
|
faceCrops,
|
|
6569
6785
|
plateCrops
|
|
6570
6786
|
} });
|
|
@@ -6573,8 +6789,20 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6573
6789
|
frameHandle,
|
|
6574
6790
|
events: eventTargets,
|
|
6575
6791
|
trackFrames: firstFrameTargets,
|
|
6792
|
+
snapshots: snapshotTargets,
|
|
6576
6793
|
cropPadding: mediaSettings.cropPadding
|
|
6577
|
-
})
|
|
6794
|
+
}).then((res) => {
|
|
6795
|
+
for (const s of res.storedSnapshots) this.trackStore?.addSnapshot(s.trackId, {
|
|
6796
|
+
timestamp: s.timestamp,
|
|
6797
|
+
position: {
|
|
6798
|
+
x: s.bbox.x + s.bbox.w / 2,
|
|
6799
|
+
y: s.bbox.y + s.bbox.h / 2,
|
|
6800
|
+
timestamp: s.timestamp,
|
|
6801
|
+
bbox: s.bbox
|
|
6802
|
+
},
|
|
6803
|
+
mediaKey: s.mediaKey
|
|
6804
|
+
});
|
|
6805
|
+
}).catch(() => {});
|
|
6578
6806
|
}
|
|
6579
6807
|
}
|
|
6580
6808
|
if (this.faceRecognizer && faceGloballyEnabled && faceSettings) this.faceRecognizer.processFrame({
|
|
@@ -6707,6 +6935,35 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6707
6935
|
});
|
|
6708
6936
|
return settings;
|
|
6709
6937
|
}
|
|
6938
|
+
/**
|
|
6939
|
+
* §5 — decide which active tracks need periodic media THIS frame. Pure over
|
|
6940
|
+
* TrackStore.lastSnapshotAt + the per-track best-confidence map:
|
|
6941
|
+
* • `snapshot` (append) + `lastFrame` (rolling overwrite) fire together on
|
|
6942
|
+
* the snapshotIntervalMs cadence, gated by `saveThumbnails`;
|
|
6943
|
+
* • `thumbnail` (best) fires when confidence beats the held best by the
|
|
6944
|
+
* hysteresis margin, rate-limited to one per BEST_FRAME_MIN_GAP_MS, and is
|
|
6945
|
+
* NOT gated by saveThumbnails (a single best still-frame is always useful).
|
|
6946
|
+
* The per-track best-confidence map is updated here as a side effect.
|
|
6947
|
+
*/
|
|
6948
|
+
buildSnapshotTargets(tracked, timestamp, media) {
|
|
6949
|
+
const targets = [];
|
|
6950
|
+
for (const t of tracked) {
|
|
6951
|
+
const lastSnap = this.trackStore.lastSnapshotAt(t.trackId);
|
|
6952
|
+
const dueSnapshot = media.saveThumbnails && lastSnap > 0 && timestamp - lastSnap >= media.snapshotIntervalMs;
|
|
6953
|
+
const isNewBest = this.bestFrameTracker.observe(t.trackId, t.confidence, timestamp);
|
|
6954
|
+
if (!dueSnapshot && !isNewBest) continue;
|
|
6955
|
+
targets.push({
|
|
6956
|
+
trackId: t.trackId,
|
|
6957
|
+
timestamp,
|
|
6958
|
+
bbox: { ...t.bbox },
|
|
6959
|
+
...t.label ? { label: t.label } : {},
|
|
6960
|
+
appendSnapshot: dueSnapshot,
|
|
6961
|
+
rollingLastFrame: dueSnapshot,
|
|
6962
|
+
bestThumbnail: isNewBest
|
|
6963
|
+
});
|
|
6964
|
+
}
|
|
6965
|
+
return targets;
|
|
6966
|
+
}
|
|
6710
6967
|
async handleAudioResult(data) {
|
|
6711
6968
|
if (this.shuttingDown) return;
|
|
6712
6969
|
const { deviceId, frame } = data;
|
|
@@ -6961,6 +7218,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
6961
7218
|
});
|
|
6962
7219
|
this.faceRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
6963
7220
|
this.plateRecognizer?.onTrackEnd(t.deviceId, t.trackId);
|
|
7221
|
+
this.bestFrameTracker.delete(t.trackId);
|
|
6964
7222
|
this.ctx.eventBus.emit({
|
|
6965
7223
|
id: `pa-end-${t.trackId}`,
|
|
6966
7224
|
timestamp: new Date(t.lastSeen),
|
|
@@ -7394,7 +7652,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
7394
7652
|
min: 500,
|
|
7395
7653
|
max: 6e4,
|
|
7396
7654
|
step: 500,
|
|
7397
|
-
default:
|
|
7655
|
+
default: 5e3,
|
|
7398
7656
|
showValue: true,
|
|
7399
7657
|
unit: "s",
|
|
7400
7658
|
displayScale: 1e3
|
|
@@ -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-Cb6Z2H5K.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.1.
|
|
3
|
+
"version": "1.1.25",
|
|
4
4
|
"description": "CamStack Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|