@camstack/addon-post-analysis 1.1.26 → 1.1.28
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-DytVmDZg.mjs → dist-Blpsv-M0.mjs} +1042 -56
- package/dist/{dist-BEx5ST1W.js → dist-Bnb58pyL.js} +1051 -89
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-Cg_cGqs0.js → node-Cvhwrf43.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +107 -107
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-p-r6oXq8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CZZpqsjV.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C6LbOCa9.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DBgBsiUM.mjs} +2 -2
- package/dist/pipeline-analytics/{hostInit-2UZIpU0W.mjs → hostInit-D-KSUwyU.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +2845 -129
- package/dist/pipeline-analytics/index.mjs +2841 -125
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -19
- package/dist/enrichment-engine/index.js +0 -857
- package/dist/enrichment-engine/index.mjs +0 -852
- package/dist/resolve-frame-CT1T1tWy.mjs +0 -44
- package/dist/resolve-frame-Cbm_NFuq.js +0 -57
|
@@ -1,852 +0,0 @@
|
|
|
1
|
-
import { C as number, E as tuple, S as boolean, T as string, _ as asJsonObject, b as _enum, g as EventCategory, m as BaseAddon, v as createEvent, w as object, x as array } from "../dist-DytVmDZg.mjs";
|
|
2
|
-
import { n as extractCrop, t as resolveFrame } from "../resolve-frame-CT1T1tWy.mjs";
|
|
3
|
-
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
4
|
-
//#region src/enrichment-engine/types.ts
|
|
5
|
-
var DEFAULT_ENRICHMENT_CONFIG = {
|
|
6
|
-
enabled: false,
|
|
7
|
-
embedding: {
|
|
8
|
-
enabled: false,
|
|
9
|
-
modelId: "clip-vit-b32",
|
|
10
|
-
agentId: "local",
|
|
11
|
-
runtime: "node",
|
|
12
|
-
backend: "cpu",
|
|
13
|
-
classes: [],
|
|
14
|
-
minConfidence: .5,
|
|
15
|
-
maxPerSecPerCamera: 1,
|
|
16
|
-
cropStrategy: "first",
|
|
17
|
-
retentionDays: 30
|
|
18
|
-
},
|
|
19
|
-
sceneMonitor: {
|
|
20
|
-
enabled: false,
|
|
21
|
-
modelId: "clip-vit-b32",
|
|
22
|
-
pollIntervalSec: 10,
|
|
23
|
-
hysteresisCount: 3
|
|
24
|
-
},
|
|
25
|
-
activitySummary: {
|
|
26
|
-
enabled: false,
|
|
27
|
-
intervalSec: 60,
|
|
28
|
-
activityThresholds: {
|
|
29
|
-
low: 2,
|
|
30
|
-
medium: 10,
|
|
31
|
-
high: 30
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region src/enrichment-engine/workers/embedding-dispatcher.ts
|
|
37
|
-
function selectCropBbox(detection) {
|
|
38
|
-
return detection.refinedBbox ?? detection.bbox;
|
|
39
|
-
}
|
|
40
|
-
var EmbeddingDispatcher = class {
|
|
41
|
-
config;
|
|
42
|
-
encoders;
|
|
43
|
-
eventBus;
|
|
44
|
-
logger;
|
|
45
|
-
ownNodeId;
|
|
46
|
-
readers;
|
|
47
|
-
getRemoteFrame;
|
|
48
|
-
lastEmbedTime = /* @__PURE__ */ new Map();
|
|
49
|
-
pendingCrops = /* @__PURE__ */ new Map();
|
|
50
|
-
flushTimer = null;
|
|
51
|
-
unsubscribe = null;
|
|
52
|
-
_processedCount = 0;
|
|
53
|
-
_totalInferenceMs = 0;
|
|
54
|
-
_encoderIndex = 0;
|
|
55
|
-
constructor(deps) {
|
|
56
|
-
this.config = deps.config;
|
|
57
|
-
this.encoders = deps.encoders;
|
|
58
|
-
this.eventBus = deps.eventBus;
|
|
59
|
-
this.logger = deps.logger;
|
|
60
|
-
this.ownNodeId = deps.ownNodeId;
|
|
61
|
-
this.readers = deps.readers;
|
|
62
|
-
this.getRemoteFrame = deps.getRemoteFrame;
|
|
63
|
-
}
|
|
64
|
-
async start() {
|
|
65
|
-
if (!this.config.enabled) {
|
|
66
|
-
this.logger.info("EmbeddingDispatcher disabled");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
this.unsubscribe = this.eventBus.subscribe({ category: EventCategory.DetectionResult }, (event) => {
|
|
70
|
-
this.handleDetectionResult(event);
|
|
71
|
-
});
|
|
72
|
-
if (this.config.cropStrategy === "best-confidence") this.flushTimer = setInterval(() => {
|
|
73
|
-
this.flushPending();
|
|
74
|
-
}, 1e3);
|
|
75
|
-
this.logger.info("EmbeddingDispatcher started", { meta: {
|
|
76
|
-
strategy: this.config.cropStrategy,
|
|
77
|
-
maxPerSec: this.config.maxPerSecPerCamera
|
|
78
|
-
} });
|
|
79
|
-
}
|
|
80
|
-
async stop() {
|
|
81
|
-
this.unsubscribe?.();
|
|
82
|
-
this.unsubscribe = null;
|
|
83
|
-
if (this.flushTimer) {
|
|
84
|
-
clearInterval(this.flushTimer);
|
|
85
|
-
this.flushTimer = null;
|
|
86
|
-
}
|
|
87
|
-
await this.flushPending();
|
|
88
|
-
}
|
|
89
|
-
get processedCount() {
|
|
90
|
-
return this._processedCount;
|
|
91
|
-
}
|
|
92
|
-
get avgInferenceMs() {
|
|
93
|
-
return this._processedCount > 0 ? this._totalInferenceMs / this._processedCount : 0;
|
|
94
|
-
}
|
|
95
|
-
get queueDepth() {
|
|
96
|
-
return this.pendingCrops.size;
|
|
97
|
-
}
|
|
98
|
-
async handleDetectionResult(event) {
|
|
99
|
-
const data = event.data;
|
|
100
|
-
const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
|
|
101
|
-
if (!deviceId) return;
|
|
102
|
-
const detections = data.analysisResults ?? [];
|
|
103
|
-
const handle = data.frameHandle;
|
|
104
|
-
if (!handle) {
|
|
105
|
-
this.logger.debug("skip: no frameHandle on DetectionResult", {
|
|
106
|
-
tags: { deviceId: Number(deviceId) },
|
|
107
|
-
meta: { deviceId }
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
let decoded;
|
|
112
|
-
try {
|
|
113
|
-
decoded = await resolveFrame(handle, {
|
|
114
|
-
ownNodeId: this.ownNodeId,
|
|
115
|
-
readers: this.readers,
|
|
116
|
-
getRemoteFrame: this.getRemoteFrame
|
|
117
|
-
});
|
|
118
|
-
} catch (err) {
|
|
119
|
-
this.logger.debug("skip: resolveFrame threw", {
|
|
120
|
-
tags: { deviceId: Number(deviceId) },
|
|
121
|
-
meta: {
|
|
122
|
-
deviceId,
|
|
123
|
-
shmId: handle.shmId,
|
|
124
|
-
error: String(err)
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (!decoded) {
|
|
130
|
-
this.logger.debug("skip: frame recycled before resolve", {
|
|
131
|
-
tags: { deviceId: Number(deviceId) },
|
|
132
|
-
meta: {
|
|
133
|
-
deviceId,
|
|
134
|
-
shmId: handle.shmId
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (decoded.format !== "rgb") {
|
|
140
|
-
this.logger.debug("skip: resolved frame is not RGB", {
|
|
141
|
-
tags: { deviceId: Number(deviceId) },
|
|
142
|
-
meta: {
|
|
143
|
-
deviceId,
|
|
144
|
-
format: decoded.format
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data);
|
|
150
|
-
const frameWidth = decoded.width;
|
|
151
|
-
const frameHeight = decoded.height;
|
|
152
|
-
for (const det of detections) {
|
|
153
|
-
const detection = det.detection;
|
|
154
|
-
if (!detection) continue;
|
|
155
|
-
if (this.config.classes.length > 0 && !this.config.classes.includes(detection.class)) continue;
|
|
156
|
-
if (detection.score < this.config.minConfidence) continue;
|
|
157
|
-
const now = Date.now();
|
|
158
|
-
const minInterval = 1e3 / this.config.maxPerSecPerCamera;
|
|
159
|
-
if (now - (this.lastEmbedTime.get(deviceId) ?? 0) < minInterval) continue;
|
|
160
|
-
const trackId = detection.trackId ?? `${deviceId}-${now}`;
|
|
161
|
-
const pending = {
|
|
162
|
-
trackId,
|
|
163
|
-
deviceId,
|
|
164
|
-
class: detection.class,
|
|
165
|
-
confidence: detection.score,
|
|
166
|
-
frameData,
|
|
167
|
-
frameWidth,
|
|
168
|
-
frameHeight,
|
|
169
|
-
bbox: selectCropBbox(detection),
|
|
170
|
-
receivedAt: now
|
|
171
|
-
};
|
|
172
|
-
switch (this.config.cropStrategy) {
|
|
173
|
-
case "first":
|
|
174
|
-
if (!this.pendingCrops.has(trackId)) {
|
|
175
|
-
this.pendingCrops.set(trackId, pending);
|
|
176
|
-
this.processOne(pending);
|
|
177
|
-
}
|
|
178
|
-
break;
|
|
179
|
-
case "best-confidence": {
|
|
180
|
-
const existing = this.pendingCrops.get(trackId);
|
|
181
|
-
if (!existing || pending.confidence > existing.confidence) this.pendingCrops.set(trackId, pending);
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
case "track-end":
|
|
185
|
-
if (det.objectState?.state === "leaving") {
|
|
186
|
-
this.pendingCrops.set(trackId, pending);
|
|
187
|
-
this.processOne(pending);
|
|
188
|
-
} else {
|
|
189
|
-
const existing = this.pendingCrops.get(trackId);
|
|
190
|
-
if (!existing || pending.confidence > existing.confidence) this.pendingCrops.set(trackId, pending);
|
|
191
|
-
}
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
async flushPending() {
|
|
197
|
-
const now = Date.now();
|
|
198
|
-
const toFlush = [];
|
|
199
|
-
for (const [trackId, pending] of this.pendingCrops) if (now - pending.receivedAt > 3e3) {
|
|
200
|
-
toFlush.push(pending);
|
|
201
|
-
this.pendingCrops.delete(trackId);
|
|
202
|
-
}
|
|
203
|
-
await Promise.all(toFlush.map((p) => this.processOne(p)));
|
|
204
|
-
}
|
|
205
|
-
async processOne(pending) {
|
|
206
|
-
if (this.encoders.length === 0) return;
|
|
207
|
-
try {
|
|
208
|
-
const { crop, width, height } = await extractCrop(pending.frameData, pending.frameWidth, pending.frameHeight, pending.bbox);
|
|
209
|
-
const encoder = this.encoders[this._encoderIndex % this.encoders.length];
|
|
210
|
-
this._encoderIndex++;
|
|
211
|
-
const { embedding: _embedding, inferenceMs } = await encoder.encode(crop, width, height);
|
|
212
|
-
const info = encoder.getInfo();
|
|
213
|
-
this._processedCount++;
|
|
214
|
-
this._totalInferenceMs += inferenceMs;
|
|
215
|
-
this.lastEmbedTime.set(pending.deviceId, Date.now());
|
|
216
|
-
this.pendingCrops.delete(pending.trackId);
|
|
217
|
-
const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`;
|
|
218
|
-
const payload = {
|
|
219
|
-
deviceId: Number(pending.deviceId),
|
|
220
|
-
trackId: pending.trackId,
|
|
221
|
-
class: pending.class,
|
|
222
|
-
embeddingId,
|
|
223
|
-
modelId: info.modelId,
|
|
224
|
-
embeddingDim: info.embeddingDim,
|
|
225
|
-
inferenceMs,
|
|
226
|
-
timestamp: Date.now()
|
|
227
|
-
};
|
|
228
|
-
this.eventBus.emit(createEvent(EventCategory.EnrichmentEmbeddingStored, {
|
|
229
|
-
type: "addon",
|
|
230
|
-
id: "enrichment-engine"
|
|
231
|
-
}, payload));
|
|
232
|
-
this.logger.debug("Embedded track", {
|
|
233
|
-
tags: { deviceId: Number(pending.deviceId) },
|
|
234
|
-
meta: {
|
|
235
|
-
class: pending.class,
|
|
236
|
-
trackId: pending.trackId,
|
|
237
|
-
inferenceMs: Number(inferenceMs.toFixed(1))
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
} catch (err) {
|
|
241
|
-
this.logger.warn("Failed to embed track", {
|
|
242
|
-
tags: { deviceId: Number(pending.deviceId) },
|
|
243
|
-
meta: {
|
|
244
|
-
trackId: pending.trackId,
|
|
245
|
-
error: String(err)
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
//#endregion
|
|
252
|
-
//#region src/_analytics-schemas/persistence-records.ts
|
|
253
|
-
/**
|
|
254
|
-
* Zod schemas for analytics-suite persisted record types.
|
|
255
|
-
* Used by persistence services to parse settings-store query results.
|
|
256
|
-
*/
|
|
257
|
-
var TrackPositionSchema = object({
|
|
258
|
-
x: number(),
|
|
259
|
-
y: number(),
|
|
260
|
-
timestamp: number(),
|
|
261
|
-
bbox: tuple([
|
|
262
|
-
number(),
|
|
263
|
-
number(),
|
|
264
|
-
number(),
|
|
265
|
-
number()
|
|
266
|
-
])
|
|
267
|
-
});
|
|
268
|
-
var TrackSnapshotSchema = object({
|
|
269
|
-
timestamp: number(),
|
|
270
|
-
position: TrackPositionSchema,
|
|
271
|
-
thumbnailPath: string()
|
|
272
|
-
});
|
|
273
|
-
object({
|
|
274
|
-
trackId: string(),
|
|
275
|
-
deviceId: string(),
|
|
276
|
-
className: string(),
|
|
277
|
-
label: string().optional(),
|
|
278
|
-
firstSeen: number(),
|
|
279
|
-
lastSeen: number(),
|
|
280
|
-
positions: array(TrackPositionSchema),
|
|
281
|
-
snapshots: array(TrackSnapshotSchema),
|
|
282
|
-
totalDistance: number(),
|
|
283
|
-
zonesVisited: array(string()),
|
|
284
|
-
active: boolean()
|
|
285
|
-
});
|
|
286
|
-
var SceneMonitorConfigSchema = object({ monitors: array(object({
|
|
287
|
-
id: string(),
|
|
288
|
-
label: string(),
|
|
289
|
-
roi: object({
|
|
290
|
-
x: number(),
|
|
291
|
-
y: number(),
|
|
292
|
-
w: number(),
|
|
293
|
-
h: number()
|
|
294
|
-
}),
|
|
295
|
-
enabled: boolean(),
|
|
296
|
-
states: array(object({
|
|
297
|
-
id: string(),
|
|
298
|
-
label: string(),
|
|
299
|
-
prompt: string().optional(),
|
|
300
|
-
textPrompts: array(string()).optional(),
|
|
301
|
-
referenceEmbeddings: array(array(number())).optional(),
|
|
302
|
-
threshold: number().optional(),
|
|
303
|
-
notify: boolean().optional(),
|
|
304
|
-
severity: _enum([
|
|
305
|
-
"info",
|
|
306
|
-
"warning",
|
|
307
|
-
"alert"
|
|
308
|
-
]).optional()
|
|
309
|
-
}))
|
|
310
|
-
})) });
|
|
311
|
-
//#endregion
|
|
312
|
-
//#region src/enrichment-engine/services/text-embedding-cache.ts
|
|
313
|
-
/**
|
|
314
|
-
* In-memory cache for text prompt embeddings.
|
|
315
|
-
*
|
|
316
|
-
* Keys follow the convention `${monitorId}:${text}` so that all entries
|
|
317
|
-
* belonging to a specific scene monitor can be invalidated in one call.
|
|
318
|
-
*/
|
|
319
|
-
var TextEmbeddingCache = class {
|
|
320
|
-
cache = /* @__PURE__ */ new Map();
|
|
321
|
-
get(key) {
|
|
322
|
-
return this.cache.get(key);
|
|
323
|
-
}
|
|
324
|
-
set(key, embedding) {
|
|
325
|
-
this.cache.set(key, embedding);
|
|
326
|
-
}
|
|
327
|
-
has(key) {
|
|
328
|
-
return this.cache.has(key);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Deletes all entries whose key starts with `${monitorId}:`.
|
|
332
|
-
* Used when a scene monitor's text prompts are updated.
|
|
333
|
-
*/
|
|
334
|
-
invalidateMonitor(monitorId) {
|
|
335
|
-
const prefix = `${monitorId}:`;
|
|
336
|
-
for (const key of this.cache.keys()) if (key.startsWith(prefix)) this.cache.delete(key);
|
|
337
|
-
}
|
|
338
|
-
clear() {
|
|
339
|
-
this.cache.clear();
|
|
340
|
-
}
|
|
341
|
-
get size() {
|
|
342
|
-
return this.cache.size;
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
//#endregion
|
|
346
|
-
//#region src/enrichment-engine/workers/scene-state-worker.ts
|
|
347
|
-
var SceneStateWorker = class {
|
|
348
|
-
config;
|
|
349
|
-
encoders;
|
|
350
|
-
eventBus;
|
|
351
|
-
store;
|
|
352
|
-
streamBrokerRegistry;
|
|
353
|
-
logger;
|
|
354
|
-
textCache = new TextEmbeddingCache();
|
|
355
|
-
monitorStates = /* @__PURE__ */ new Map();
|
|
356
|
-
pollTimer = null;
|
|
357
|
-
_activeCount = 0;
|
|
358
|
-
constructor(deps) {
|
|
359
|
-
this.config = deps.config;
|
|
360
|
-
this.encoders = deps.encoders;
|
|
361
|
-
this.eventBus = deps.eventBus;
|
|
362
|
-
this.store = deps.store;
|
|
363
|
-
this.streamBrokerRegistry = deps.streamBrokerRegistry;
|
|
364
|
-
this.logger = deps.logger;
|
|
365
|
-
}
|
|
366
|
-
async start() {
|
|
367
|
-
if (!this.config.enabled) {
|
|
368
|
-
this.logger.info("SceneStateWorker disabled");
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
this.pollTimer = setInterval(() => {
|
|
372
|
-
this.pollAll();
|
|
373
|
-
}, this.config.pollIntervalSec * 1e3);
|
|
374
|
-
this.logger.info("SceneStateWorker started", { meta: {
|
|
375
|
-
pollIntervalSec: this.config.pollIntervalSec,
|
|
376
|
-
hysteresis: this.config.hysteresisCount
|
|
377
|
-
} });
|
|
378
|
-
}
|
|
379
|
-
async stop() {
|
|
380
|
-
if (this.pollTimer) {
|
|
381
|
-
clearInterval(this.pollTimer);
|
|
382
|
-
this.pollTimer = null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
get activeCount() {
|
|
386
|
-
return this._activeCount;
|
|
387
|
-
}
|
|
388
|
-
async pollAll() {
|
|
389
|
-
const allConfigs = (await this.store.query.query({
|
|
390
|
-
collection: "device-settings",
|
|
391
|
-
filter: { where: { id: "enrichment:scene-monitor:%" } }
|
|
392
|
-
})).map((r) => ({
|
|
393
|
-
id: r.id,
|
|
394
|
-
data: SceneMonitorConfigSchema.parse(r.data)
|
|
395
|
-
}));
|
|
396
|
-
const deviceMonitors = /* @__PURE__ */ new Map();
|
|
397
|
-
for (const record of allConfigs) {
|
|
398
|
-
const deviceId = record.id.replace("enrichment:scene-monitor:", "");
|
|
399
|
-
const active = record.data.monitors.filter((m) => m.enabled);
|
|
400
|
-
if (active.length > 0) deviceMonitors.set(deviceId, active);
|
|
401
|
-
}
|
|
402
|
-
this._activeCount = [...deviceMonitors.values()].reduce((sum, ms) => sum + ms.length, 0);
|
|
403
|
-
await Promise.all([...deviceMonitors.entries()].map(([deviceId, monitors]) => this.pollCamera(deviceId, monitors)));
|
|
404
|
-
}
|
|
405
|
-
async pollCamera(deviceId, monitors) {
|
|
406
|
-
if (this.encoders.length === 0) return;
|
|
407
|
-
try {
|
|
408
|
-
const snapshot = await this.streamBrokerRegistry.getSnapshot(deviceId);
|
|
409
|
-
if (!snapshot) return;
|
|
410
|
-
const encoder = this.encoders[0];
|
|
411
|
-
for (const monitor of monitors) try {
|
|
412
|
-
await this.processMonitor(deviceId, monitor, snapshot, encoder);
|
|
413
|
-
} catch (err) {
|
|
414
|
-
this.logger.warn("SceneState error", {
|
|
415
|
-
tags: { deviceId: Number(deviceId) },
|
|
416
|
-
meta: {
|
|
417
|
-
monitorLabel: monitor.label,
|
|
418
|
-
error: String(err)
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
} catch (err) {
|
|
423
|
-
this.logger.warn("Failed to capture snapshot", {
|
|
424
|
-
tags: { deviceId: Number(deviceId) },
|
|
425
|
-
meta: { error: String(err) }
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
async processMonitor(deviceId, monitor, snapshot, encoder) {
|
|
430
|
-
const bbox = {
|
|
431
|
-
x: monitor.roi.x * snapshot.width,
|
|
432
|
-
y: monitor.roi.y * snapshot.height,
|
|
433
|
-
w: monitor.roi.w * snapshot.width,
|
|
434
|
-
h: monitor.roi.h * snapshot.height
|
|
435
|
-
};
|
|
436
|
-
const { crop, width, height } = await extractCrop(snapshot.data, snapshot.width, snapshot.height, bbox);
|
|
437
|
-
const { embedding: imageEmb } = await encoder.encode(crop, width, height);
|
|
438
|
-
let bestState = null;
|
|
439
|
-
let bestScore = -1;
|
|
440
|
-
for (const state of monitor.states) {
|
|
441
|
-
let score = 0;
|
|
442
|
-
if (state.referenceEmbeddings && state.referenceEmbeddings.length > 0) for (const refEmb of state.referenceEmbeddings) {
|
|
443
|
-
const sim = cosineSimilarity(imageEmb, new Float32Array(refEmb));
|
|
444
|
-
score = Math.max(score, sim);
|
|
445
|
-
}
|
|
446
|
-
else if (state.textPrompts && state.textPrompts.length > 0) for (const prompt of state.textPrompts) {
|
|
447
|
-
const cacheKey = `${monitor.id}:${state.id}:${prompt}`;
|
|
448
|
-
let textEmb = this.textCache.get(cacheKey);
|
|
449
|
-
if (!textEmb) {
|
|
450
|
-
textEmb = (await encoder.encodeText(prompt)).embedding;
|
|
451
|
-
this.textCache.set(cacheKey, textEmb);
|
|
452
|
-
}
|
|
453
|
-
const sim = cosineSimilarity(imageEmb, textEmb);
|
|
454
|
-
score = Math.max(score, sim);
|
|
455
|
-
}
|
|
456
|
-
if (score > bestScore) {
|
|
457
|
-
bestScore = score;
|
|
458
|
-
bestState = state.label;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
if (!bestState) return;
|
|
462
|
-
const key = `${deviceId}:${monitor.id}`;
|
|
463
|
-
let ms = this.monitorStates.get(key);
|
|
464
|
-
if (!ms) {
|
|
465
|
-
ms = {
|
|
466
|
-
currentState: null,
|
|
467
|
-
pendingState: null,
|
|
468
|
-
pendingCount: 0,
|
|
469
|
-
pendingConfidence: 0
|
|
470
|
-
};
|
|
471
|
-
this.monitorStates.set(key, ms);
|
|
472
|
-
}
|
|
473
|
-
if (bestState === ms.pendingState) {
|
|
474
|
-
ms.pendingCount++;
|
|
475
|
-
ms.pendingConfidence = bestScore;
|
|
476
|
-
} else {
|
|
477
|
-
ms.pendingState = bestState;
|
|
478
|
-
ms.pendingCount = 1;
|
|
479
|
-
ms.pendingConfidence = bestScore;
|
|
480
|
-
}
|
|
481
|
-
if (ms.pendingCount < this.config.hysteresisCount) return;
|
|
482
|
-
if (bestState === ms.currentState) return;
|
|
483
|
-
const previousState = ms.currentState ?? "unknown";
|
|
484
|
-
ms.currentState = bestState;
|
|
485
|
-
ms.pendingState = null;
|
|
486
|
-
ms.pendingCount = 0;
|
|
487
|
-
const data = {
|
|
488
|
-
deviceId: Number(deviceId),
|
|
489
|
-
monitorId: monitor.id,
|
|
490
|
-
monitorLabel: monitor.label,
|
|
491
|
-
previousState,
|
|
492
|
-
currentState: bestState,
|
|
493
|
-
confidence: bestScore,
|
|
494
|
-
timestamp: Date.now()
|
|
495
|
-
};
|
|
496
|
-
this.eventBus.emit({
|
|
497
|
-
id: `scene-state-${deviceId}-${monitor.id}-${Date.now()}`,
|
|
498
|
-
category: EventCategory.EnrichmentSceneStateChanged,
|
|
499
|
-
source: {
|
|
500
|
-
type: "device",
|
|
501
|
-
id: deviceId
|
|
502
|
-
},
|
|
503
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
504
|
-
data
|
|
505
|
-
});
|
|
506
|
-
this.logger.info("Scene state changed", {
|
|
507
|
-
tags: { deviceId: Number(deviceId) },
|
|
508
|
-
meta: {
|
|
509
|
-
monitorLabel: monitor.label,
|
|
510
|
-
previousState,
|
|
511
|
-
currentState: bestState,
|
|
512
|
-
confidence: Number(bestScore.toFixed(2))
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
function cosineSimilarity(a, b) {
|
|
518
|
-
let dot = 0;
|
|
519
|
-
let normA = 0;
|
|
520
|
-
let normB = 0;
|
|
521
|
-
for (let i = 0; i < a.length; i++) {
|
|
522
|
-
dot += a[i] * b[i];
|
|
523
|
-
normA += a[i] * a[i];
|
|
524
|
-
normB += b[i] * b[i];
|
|
525
|
-
}
|
|
526
|
-
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
527
|
-
}
|
|
528
|
-
//#endregion
|
|
529
|
-
//#region src/enrichment-engine/workers/activity-summary.ts
|
|
530
|
-
var ActivitySummaryWorker = class {
|
|
531
|
-
config;
|
|
532
|
-
eventBus;
|
|
533
|
-
store;
|
|
534
|
-
logger;
|
|
535
|
-
buffers = /* @__PURE__ */ new Map();
|
|
536
|
-
summaryTimer = null;
|
|
537
|
-
unsubDetection = null;
|
|
538
|
-
unsubSceneState = null;
|
|
539
|
-
_lastSummary = null;
|
|
540
|
-
constructor(deps) {
|
|
541
|
-
this.config = deps.config;
|
|
542
|
-
this.eventBus = deps.eventBus;
|
|
543
|
-
this.store = deps.store;
|
|
544
|
-
this.logger = deps.logger;
|
|
545
|
-
}
|
|
546
|
-
async start() {
|
|
547
|
-
if (!this.config.enabled) {
|
|
548
|
-
this.logger.info("ActivitySummaryWorker disabled");
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
this.unsubDetection = this.eventBus.subscribe({ category: EventCategory.DetectionResult }, (event) => {
|
|
552
|
-
this.handleDetection(event);
|
|
553
|
-
});
|
|
554
|
-
this.unsubSceneState = this.eventBus.subscribe({ category: EventCategory.EnrichmentSceneStateChanged }, (event) => {
|
|
555
|
-
this.handleSceneStateChange(event);
|
|
556
|
-
});
|
|
557
|
-
this.summaryTimer = setInterval(() => {
|
|
558
|
-
this.emitSummaries();
|
|
559
|
-
}, this.config.intervalSec * 1e3);
|
|
560
|
-
this.logger.info("ActivitySummaryWorker started", { meta: { intervalSec: this.config.intervalSec } });
|
|
561
|
-
}
|
|
562
|
-
async stop() {
|
|
563
|
-
this.unsubDetection?.();
|
|
564
|
-
this.unsubSceneState?.();
|
|
565
|
-
if (this.summaryTimer) {
|
|
566
|
-
clearInterval(this.summaryTimer);
|
|
567
|
-
this.summaryTimer = null;
|
|
568
|
-
}
|
|
569
|
-
await this.emitSummaries();
|
|
570
|
-
}
|
|
571
|
-
get lastSummary() {
|
|
572
|
-
return this._lastSummary;
|
|
573
|
-
}
|
|
574
|
-
getBuffer(deviceId) {
|
|
575
|
-
let buf = this.buffers.get(deviceId);
|
|
576
|
-
if (!buf) {
|
|
577
|
-
buf = {
|
|
578
|
-
tracks: /* @__PURE__ */ new Map(),
|
|
579
|
-
zoneEvents: [],
|
|
580
|
-
stateChanges: []
|
|
581
|
-
};
|
|
582
|
-
this.buffers.set(deviceId, buf);
|
|
583
|
-
}
|
|
584
|
-
return buf;
|
|
585
|
-
}
|
|
586
|
-
handleDetection(event) {
|
|
587
|
-
const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
|
|
588
|
-
if (!deviceId) return;
|
|
589
|
-
const results = event.data.analysisResults ?? [];
|
|
590
|
-
const buf = this.getBuffer(deviceId);
|
|
591
|
-
const now = Date.now();
|
|
592
|
-
for (const det of results) {
|
|
593
|
-
const detection = det.detection;
|
|
594
|
-
if (!detection) continue;
|
|
595
|
-
const trackId = detection.trackId ?? `unknown-${now}`;
|
|
596
|
-
const existing = buf.tracks.get(trackId);
|
|
597
|
-
if (existing) existing.lastSeen = now;
|
|
598
|
-
else buf.tracks.set(trackId, {
|
|
599
|
-
class: detection.class,
|
|
600
|
-
deviceId,
|
|
601
|
-
firstSeen: now,
|
|
602
|
-
lastSeen: now,
|
|
603
|
-
zones: /* @__PURE__ */ new Set()
|
|
604
|
-
});
|
|
605
|
-
const zoneEvents = det.zoneEvents ?? [];
|
|
606
|
-
for (const ze of zoneEvents) {
|
|
607
|
-
const track = buf.tracks.get(trackId);
|
|
608
|
-
if (track) track.zones.add(ze.zoneId);
|
|
609
|
-
if (ze.type === "zone-enter" || ze.type === "zone-exit") buf.zoneEvents.push({
|
|
610
|
-
type: ze.type === "zone-enter" ? "enter" : "exit",
|
|
611
|
-
zoneId: ze.zoneId,
|
|
612
|
-
trackId,
|
|
613
|
-
timestamp: ze.timestamp ?? now
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
handleSceneStateChange(event) {
|
|
619
|
-
const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
|
|
620
|
-
if (!deviceId) return;
|
|
621
|
-
const data = event.data;
|
|
622
|
-
this.getBuffer(deviceId).stateChanges.push({
|
|
623
|
-
monitorId: data.monitorId,
|
|
624
|
-
from: data.previousState,
|
|
625
|
-
to: data.currentState,
|
|
626
|
-
timestamp: data.timestamp
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
async emitSummaries() {
|
|
630
|
-
const now = Date.now();
|
|
631
|
-
for (const [deviceId, buf] of this.buffers) {
|
|
632
|
-
if (buf.tracks.size === 0 && buf.zoneEvents.length === 0 && buf.stateChanges.length === 0) continue;
|
|
633
|
-
const objectCounts = {};
|
|
634
|
-
for (const track of buf.tracks.values()) objectCounts[track.class] = (objectCounts[track.class] ?? 0) + 1;
|
|
635
|
-
const zoneMap = /* @__PURE__ */ new Map();
|
|
636
|
-
for (const ze of buf.zoneEvents) {
|
|
637
|
-
let z = zoneMap.get(ze.zoneId);
|
|
638
|
-
if (!z) {
|
|
639
|
-
z = {
|
|
640
|
-
entries: 0,
|
|
641
|
-
exits: 0,
|
|
642
|
-
dwellTimes: []
|
|
643
|
-
};
|
|
644
|
-
zoneMap.set(ze.zoneId, z);
|
|
645
|
-
}
|
|
646
|
-
if (ze.type === "enter") z.entries++;
|
|
647
|
-
else z.exits++;
|
|
648
|
-
}
|
|
649
|
-
const zoneActivity = [...zoneMap.entries()].map(([zoneId, z]) => ({
|
|
650
|
-
zoneId,
|
|
651
|
-
entries: z.entries,
|
|
652
|
-
exits: z.exits,
|
|
653
|
-
avgDwellMs: z.dwellTimes.length > 0 ? z.dwellTimes.reduce((a, b) => a + b, 0) / z.dwellTimes.length : 0
|
|
654
|
-
}));
|
|
655
|
-
const eventsPerMin = (buf.tracks.size + buf.zoneEvents.length) / (this.config.intervalSec / 60);
|
|
656
|
-
const activityLevel = eventsPerMin >= this.config.activityThresholds.high ? "high" : eventsPerMin >= this.config.activityThresholds.medium ? "medium" : eventsPerMin >= this.config.activityThresholds.low ? "low" : "none";
|
|
657
|
-
const summary = {
|
|
658
|
-
deviceId: Number(deviceId),
|
|
659
|
-
periodStart: now - this.config.intervalSec * 1e3,
|
|
660
|
-
periodEnd: now,
|
|
661
|
-
objectCounts,
|
|
662
|
-
zoneActivity,
|
|
663
|
-
stateChanges: [...buf.stateChanges],
|
|
664
|
-
activityLevel
|
|
665
|
-
};
|
|
666
|
-
this._lastSummary = summary;
|
|
667
|
-
this.eventBus.emit(createEvent(EventCategory.EnrichmentActivitySummary, {
|
|
668
|
-
type: "device",
|
|
669
|
-
id: deviceId
|
|
670
|
-
}, summary));
|
|
671
|
-
try {
|
|
672
|
-
await this.store.insert.mutate({
|
|
673
|
-
collection: "addon-settings",
|
|
674
|
-
record: {
|
|
675
|
-
id: `${deviceId}:${now}`,
|
|
676
|
-
data: { ...summary }
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
} catch {}
|
|
680
|
-
buf.tracks.clear();
|
|
681
|
-
buf.zoneEvents.length = 0;
|
|
682
|
-
buf.stateChanges.length = 0;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
//#endregion
|
|
687
|
-
//#region src/enrichment-engine/index.ts
|
|
688
|
-
/**
|
|
689
|
-
* Extended context shape injected at runtime by the server's capability wiring.
|
|
690
|
-
* Not part of the base AddonContext interface because capabilities are resolved
|
|
691
|
-
* after addon initialization.
|
|
692
|
-
*/
|
|
693
|
-
var EnrichmentEngineAddon = class extends BaseAddon {
|
|
694
|
-
embeddingDispatcher = null;
|
|
695
|
-
sceneStateWorker = null;
|
|
696
|
-
activitySummary = null;
|
|
697
|
-
/**
|
|
698
|
-
* Shared shm-ring reader cache for downstream frame access. Owned by the
|
|
699
|
-
* engine so the cached segments stay open across worker restarts and close
|
|
700
|
-
* exactly once on shutdown.
|
|
701
|
-
*/
|
|
702
|
-
readers = null;
|
|
703
|
-
currentFlags = {
|
|
704
|
-
embeddingEnabled: true,
|
|
705
|
-
sceneMonitorEnabled: true,
|
|
706
|
-
activitySummaryEnabled: true
|
|
707
|
-
};
|
|
708
|
-
constructor() {
|
|
709
|
-
super({});
|
|
710
|
-
}
|
|
711
|
-
async onInitialize() {
|
|
712
|
-
const config = await this.loadConfig();
|
|
713
|
-
const encoderCollection = this.capabilities?.getCollection?.("embedding-encoder") ?? [];
|
|
714
|
-
const streamBrokerRaw = this.capabilities?.get?.("stream-broker");
|
|
715
|
-
const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
716
|
-
const ownNodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
717
|
-
this.readers = new FrameRingReaderCache(this.ctx.logger.child("shm-readers"));
|
|
718
|
-
const getRemoteFrame = async (handle) => {
|
|
719
|
-
const remote = await this.ctx.api.decoder.getFrame.query({
|
|
720
|
-
handle,
|
|
721
|
-
nodeId: handle.nodeId
|
|
722
|
-
});
|
|
723
|
-
if (!remote) return null;
|
|
724
|
-
return {
|
|
725
|
-
data: Buffer.from(remote.data),
|
|
726
|
-
width: remote.width,
|
|
727
|
-
height: remote.height,
|
|
728
|
-
format: remote.format,
|
|
729
|
-
timestamp: remote.timestamp
|
|
730
|
-
};
|
|
731
|
-
};
|
|
732
|
-
this.embeddingDispatcher = new EmbeddingDispatcher({
|
|
733
|
-
config: config.embedding,
|
|
734
|
-
encoders: encoderCollection,
|
|
735
|
-
eventBus: this.ctx.eventBus,
|
|
736
|
-
logger: this.ctx.logger.child("EmbeddingDispatcher"),
|
|
737
|
-
ownNodeId,
|
|
738
|
-
readers: this.readers,
|
|
739
|
-
getRemoteFrame
|
|
740
|
-
});
|
|
741
|
-
this.sceneStateWorker = new SceneStateWorker({
|
|
742
|
-
config: config.sceneMonitor,
|
|
743
|
-
encoders: encoderCollection,
|
|
744
|
-
eventBus: this.ctx.eventBus,
|
|
745
|
-
store: this.ctx.api.settingsStore,
|
|
746
|
-
streamBrokerRegistry: streamBrokerRaw ?? { getSnapshot: async () => null },
|
|
747
|
-
logger: this.ctx.logger.child("SceneStateWorker")
|
|
748
|
-
});
|
|
749
|
-
this.activitySummary = new ActivitySummaryWorker({
|
|
750
|
-
config: config.activitySummary,
|
|
751
|
-
eventBus: this.ctx.eventBus,
|
|
752
|
-
store: this.ctx.api.settingsStore,
|
|
753
|
-
logger: this.ctx.logger.child("ActivitySummary")
|
|
754
|
-
});
|
|
755
|
-
this.currentFlags = {
|
|
756
|
-
embeddingEnabled: config.embedding.enabled,
|
|
757
|
-
sceneMonitorEnabled: config.sceneMonitor.enabled,
|
|
758
|
-
activitySummaryEnabled: config.activitySummary.enabled
|
|
759
|
-
};
|
|
760
|
-
await this.embeddingDispatcher.start();
|
|
761
|
-
await this.sceneStateWorker.start();
|
|
762
|
-
await this.activitySummary.start();
|
|
763
|
-
this.ctx.logger.info("Enrichment engine initialized with 3 workers");
|
|
764
|
-
}
|
|
765
|
-
async onShutdown() {
|
|
766
|
-
await this.embeddingDispatcher?.stop();
|
|
767
|
-
await this.sceneStateWorker?.stop();
|
|
768
|
-
await this.activitySummary?.stop();
|
|
769
|
-
this.embeddingDispatcher = null;
|
|
770
|
-
this.sceneStateWorker = null;
|
|
771
|
-
this.activitySummary = null;
|
|
772
|
-
this.readers?.close();
|
|
773
|
-
this.readers = null;
|
|
774
|
-
}
|
|
775
|
-
globalSettingsSchema() {
|
|
776
|
-
return this.schema({ sections: [{
|
|
777
|
-
id: "enrichment-engine-settings",
|
|
778
|
-
title: "Enrichment Engine",
|
|
779
|
-
columns: 2,
|
|
780
|
-
fields: [
|
|
781
|
-
{
|
|
782
|
-
type: "boolean",
|
|
783
|
-
key: "embeddingEnabled",
|
|
784
|
-
label: "Embedding Enabled",
|
|
785
|
-
description: "Compute face/object embeddings from detection crops.",
|
|
786
|
-
default: true
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
type: "boolean",
|
|
790
|
-
key: "sceneMonitorEnabled",
|
|
791
|
-
label: "Scene Monitor Enabled",
|
|
792
|
-
description: "Run periodic scene state capture for change detection.",
|
|
793
|
-
default: true
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
type: "boolean",
|
|
797
|
-
key: "activitySummaryEnabled",
|
|
798
|
-
label: "Activity Summary Enabled",
|
|
799
|
-
description: "Aggregate hourly activity summaries per camera.",
|
|
800
|
-
default: true
|
|
801
|
-
}
|
|
802
|
-
]
|
|
803
|
-
}] });
|
|
804
|
-
}
|
|
805
|
-
async updateGlobalSettings(patch) {
|
|
806
|
-
await super.updateGlobalSettings(patch);
|
|
807
|
-
const prev = this.currentFlags;
|
|
808
|
-
const next = {
|
|
809
|
-
embeddingEnabled: typeof patch["embeddingEnabled"] === "boolean" ? patch["embeddingEnabled"] : prev.embeddingEnabled,
|
|
810
|
-
sceneMonitorEnabled: typeof patch["sceneMonitorEnabled"] === "boolean" ? patch["sceneMonitorEnabled"] : prev.sceneMonitorEnabled,
|
|
811
|
-
activitySummaryEnabled: typeof patch["activitySummaryEnabled"] === "boolean" ? patch["activitySummaryEnabled"] : prev.activitySummaryEnabled
|
|
812
|
-
};
|
|
813
|
-
this.currentFlags = next;
|
|
814
|
-
if (prev.embeddingEnabled && !next.embeddingEnabled) {
|
|
815
|
-
this.ctx?.logger.info("Stopping embedding dispatcher (disabled via config)");
|
|
816
|
-
await this.embeddingDispatcher?.stop();
|
|
817
|
-
} else if (!prev.embeddingEnabled && next.embeddingEnabled) {
|
|
818
|
-
this.ctx?.logger.info("Starting embedding dispatcher (enabled via config)");
|
|
819
|
-
await this.embeddingDispatcher?.start();
|
|
820
|
-
}
|
|
821
|
-
if (prev.sceneMonitorEnabled && !next.sceneMonitorEnabled) {
|
|
822
|
-
this.ctx?.logger.info("Stopping scene state worker (disabled via config)");
|
|
823
|
-
await this.sceneStateWorker?.stop();
|
|
824
|
-
} else if (!prev.sceneMonitorEnabled && next.sceneMonitorEnabled) {
|
|
825
|
-
this.ctx?.logger.info("Starting scene state worker (enabled via config)");
|
|
826
|
-
await this.sceneStateWorker?.start();
|
|
827
|
-
}
|
|
828
|
-
if (prev.activitySummaryEnabled && !next.activitySummaryEnabled) {
|
|
829
|
-
this.ctx?.logger.info("Stopping activity summary worker (disabled via config)");
|
|
830
|
-
await this.activitySummary?.stop();
|
|
831
|
-
} else if (!prev.activitySummaryEnabled && next.activitySummaryEnabled) {
|
|
832
|
-
this.ctx?.logger.info("Starting activity summary worker (enabled via config)");
|
|
833
|
-
await this.activitySummary?.start();
|
|
834
|
-
}
|
|
835
|
-
this.ctx?.logger.info("Enrichment engine flags updated", { meta: { flags: this.currentFlags } });
|
|
836
|
-
}
|
|
837
|
-
async loadConfig() {
|
|
838
|
-
try {
|
|
839
|
-
const stored = asJsonObject(await this.ctx.api?.settingsStore.get.query({
|
|
840
|
-
collection: "addon-settings",
|
|
841
|
-
key: "enrichment:global"
|
|
842
|
-
}));
|
|
843
|
-
if (stored) return {
|
|
844
|
-
...DEFAULT_ENRICHMENT_CONFIG,
|
|
845
|
-
...stored
|
|
846
|
-
};
|
|
847
|
-
} catch {}
|
|
848
|
-
return DEFAULT_ENRICHMENT_CONFIG;
|
|
849
|
-
}
|
|
850
|
-
};
|
|
851
|
-
//#endregion
|
|
852
|
-
export { EnrichmentEngineAddon, EnrichmentEngineAddon as default };
|