@camstack/addon-post-analysis 0.1.1

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.
Files changed (80) hide show
  1. package/dist/embedding-encoder/index.js +928 -0
  2. package/dist/embedding-encoder/index.js.map +1 -0
  3. package/dist/embedding-encoder/index.mjs +888 -0
  4. package/dist/embedding-encoder/index.mjs.map +1 -0
  5. package/dist/enrichment-engine/index.js +774 -0
  6. package/dist/enrichment-engine/index.js.map +1 -0
  7. package/dist/enrichment-engine/index.mjs +774 -0
  8. package/dist/enrichment-engine/index.mjs.map +1 -0
  9. package/dist/ffmpeg-config-DRONlBsj.mjs +56 -0
  10. package/dist/ffmpeg-config-DRONlBsj.mjs.map +1 -0
  11. package/dist/ffmpeg-config-uANz3sV5.js +73 -0
  12. package/dist/ffmpeg-config-uANz3sV5.js.map +1 -0
  13. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/AudioHistoryChart.d.ts +4 -0
  14. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/AudioMetricsPanel.d.ts +10 -0
  15. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/DetectionHistoryChart.d.ts +4 -0
  16. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/MotionHistoryChart.d.ts +4 -0
  17. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/OccupancyHistoryChart.d.ts +4 -0
  18. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/OccupancyPanel.d.ts +10 -0
  19. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/chart-utils.d.ts +97 -0
  20. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/index.d.ts +27 -0
  21. package/dist/pipeline-analytics/@mf-types/widgets.d.ts +2 -0
  22. package/dist/pipeline-analytics/@mf-types.d.ts +3 -0
  23. package/dist/pipeline-analytics/@mf-types.zip +0 -0
  24. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs +12 -0
  25. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-46iNwbxa.mjs +16 -0
  26. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-Db6yh4op.mjs +15 -0
  27. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-DoWbefqS.mjs +104 -0
  28. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-52bfkwC8.mjs +85 -0
  29. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-CVrnrGED.mjs +62 -0
  30. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-DuO9h7li.mjs +85 -0
  31. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CmqNjq44.mjs +29 -0
  32. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-BsyrX6NO.mjs +36 -0
  33. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-Dp8hqYOB.mjs +45 -0
  34. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CA8cCIEl.mjs +6 -0
  35. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-BZjEt71l.mjs +34 -0
  36. package/dist/pipeline-analytics/_stub.js +1397 -0
  37. package/dist/pipeline-analytics/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Bfo-or09.mjs +156 -0
  38. package/dist/pipeline-analytics/client-DdXDZxzK.mjs +10063 -0
  39. package/dist/pipeline-analytics/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +211 -0
  40. package/dist/pipeline-analytics/hostInit-CqUKNfOr.mjs +168 -0
  41. package/dist/pipeline-analytics/index-B4OKsa9p.mjs +2603 -0
  42. package/dist/pipeline-analytics/index-B7r1koiV.mjs +19109 -0
  43. package/dist/pipeline-analytics/index-CRN37Hr9.mjs +15195 -0
  44. package/dist/pipeline-analytics/index-DRx99znz.mjs +1617 -0
  45. package/dist/pipeline-analytics/index-DyYvUfc7.mjs +725 -0
  46. package/dist/pipeline-analytics/index-k0CA0h_r.mjs +185 -0
  47. package/dist/pipeline-analytics/index-kIgjN-uq.mjs +435 -0
  48. package/dist/pipeline-analytics/index-xncRG7-x.mjs +2713 -0
  49. package/dist/pipeline-analytics/index.js +2563 -0
  50. package/dist/pipeline-analytics/index.js.map +1 -0
  51. package/dist/pipeline-analytics/index.mjs +2564 -0
  52. package/dist/pipeline-analytics/index.mjs.map +1 -0
  53. package/dist/pipeline-analytics/jsx-runtime-4ro1c69i.mjs +55 -0
  54. package/dist/pipeline-analytics/remoteEntry.js +2973 -0
  55. package/dist/pipeline-analytics/virtualExposes-8FzWTdq3.mjs +42 -0
  56. package/dist/playlist-generator-EhPaB7Hn.js +48 -0
  57. package/dist/playlist-generator-EhPaB7Hn.js.map +1 -0
  58. package/dist/playlist-generator-VTkgn53O.mjs +48 -0
  59. package/dist/playlist-generator-VTkgn53O.mjs.map +1 -0
  60. package/dist/recording/index.js +257 -0
  61. package/dist/recording/index.js.map +1 -0
  62. package/dist/recording/index.mjs +235 -0
  63. package/dist/recording/index.mjs.map +1 -0
  64. package/dist/recording-coordinator-C2sATEhe.js +1052 -0
  65. package/dist/recording-coordinator-C2sATEhe.js.map +1 -0
  66. package/dist/recording-coordinator-DuP3BUTV.mjs +1012 -0
  67. package/dist/recording-coordinator-DuP3BUTV.mjs.map +1 -0
  68. package/dist/recording-db-gOgaoQh0.js +348 -0
  69. package/dist/recording-db-gOgaoQh0.js.map +1 -0
  70. package/dist/recording-db-lIkSMTLq.mjs +348 -0
  71. package/dist/recording-db-lIkSMTLq.mjs.map +1 -0
  72. package/dist/recording-service-facade-B9lG6OFn.mjs +123 -0
  73. package/dist/recording-service-facade-B9lG6OFn.mjs.map +1 -0
  74. package/dist/recording-service-facade-Do1PKlAL.js +123 -0
  75. package/dist/recording-service-facade-Do1PKlAL.js.map +1 -0
  76. package/dist/storage-estimator-CRpoQc9j.js +72 -0
  77. package/dist/storage-estimator-CRpoQc9j.js.map +1 -0
  78. package/dist/storage-estimator-DzD8gWJH.mjs +72 -0
  79. package/dist/storage-estimator-DzD8gWJH.mjs.map +1 -0
  80. package/package.json +148 -0
@@ -0,0 +1,774 @@
1
+ "use strict";
2
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
+ const types = require("@camstack/types");
4
+ const sharp = require("sharp");
5
+ const zod = require("zod");
6
+ const DEFAULT_ENRICHMENT_CONFIG = {
7
+ enabled: false,
8
+ embedding: {
9
+ enabled: false,
10
+ modelId: "clip-vit-b32",
11
+ agentId: "local",
12
+ runtime: "node",
13
+ backend: "cpu",
14
+ classes: [],
15
+ minConfidence: 0.5,
16
+ maxPerSecPerCamera: 1,
17
+ cropStrategy: "first",
18
+ retentionDays: 30
19
+ },
20
+ sceneMonitor: {
21
+ enabled: false,
22
+ modelId: "clip-vit-b32",
23
+ pollIntervalSec: 10,
24
+ hysteresisCount: 3
25
+ },
26
+ activitySummary: {
27
+ enabled: false,
28
+ intervalSec: 60,
29
+ activityThresholds: {
30
+ low: 2,
31
+ medium: 10,
32
+ high: 30
33
+ }
34
+ }
35
+ };
36
+ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
37
+ const rawLeft = Math.round(bbox.x * frameWidth);
38
+ const rawTop = Math.round(bbox.y * frameHeight);
39
+ const rawWidth = Math.round(bbox.w * frameWidth);
40
+ const rawHeight = Math.round(bbox.h * frameHeight);
41
+ const left = Math.max(0, Math.min(rawLeft, frameWidth - 1));
42
+ const top = Math.max(0, Math.min(rawTop, frameHeight - 1));
43
+ const width = Math.max(1, Math.min(rawWidth, frameWidth - left));
44
+ const height = Math.max(1, Math.min(rawHeight, frameHeight - top));
45
+ const crop = await sharp(frameData, {
46
+ raw: { width: frameWidth, height: frameHeight, channels: 3 }
47
+ }).extract({ left, top, width, height }).jpeg({ quality: 90 }).toBuffer();
48
+ return { crop, width, height };
49
+ }
50
+ class EmbeddingDispatcher {
51
+ config;
52
+ encoders;
53
+ eventBus;
54
+ logger;
55
+ lastEmbedTime = /* @__PURE__ */ new Map();
56
+ pendingCrops = /* @__PURE__ */ new Map();
57
+ flushTimer = null;
58
+ unsubscribe = null;
59
+ _processedCount = 0;
60
+ _totalInferenceMs = 0;
61
+ _encoderIndex = 0;
62
+ constructor(deps) {
63
+ this.config = deps.config;
64
+ this.encoders = deps.encoders;
65
+ this.eventBus = deps.eventBus;
66
+ this.logger = deps.logger;
67
+ }
68
+ async start() {
69
+ if (!this.config.enabled) {
70
+ this.logger.info("EmbeddingDispatcher disabled");
71
+ return;
72
+ }
73
+ this.unsubscribe = this.eventBus.subscribe(
74
+ { category: types.EventCategory.DetectionResult },
75
+ (event) => {
76
+ void this.handleDetectionResult(event);
77
+ }
78
+ );
79
+ if (this.config.cropStrategy === "best-confidence") {
80
+ this.flushTimer = setInterval(() => {
81
+ void this.flushPending();
82
+ }, 1e3);
83
+ }
84
+ this.logger.info("EmbeddingDispatcher started", { meta: { strategy: this.config.cropStrategy, maxPerSec: this.config.maxPerSecPerCamera } });
85
+ }
86
+ async stop() {
87
+ this.unsubscribe?.();
88
+ this.unsubscribe = null;
89
+ if (this.flushTimer) {
90
+ clearInterval(this.flushTimer);
91
+ this.flushTimer = null;
92
+ }
93
+ await this.flushPending();
94
+ }
95
+ get processedCount() {
96
+ return this._processedCount;
97
+ }
98
+ get avgInferenceMs() {
99
+ return this._processedCount > 0 ? this._totalInferenceMs / this._processedCount : 0;
100
+ }
101
+ get queueDepth() {
102
+ return this.pendingCrops.size;
103
+ }
104
+ async handleDetectionResult(event) {
105
+ const data = event.data;
106
+ const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
107
+ if (!deviceId) return;
108
+ const detections = data.analysisResults ?? [];
109
+ const pipelineResult = data.pipelineResult;
110
+ for (const det of detections) {
111
+ const detection = det.detection;
112
+ if (!detection) continue;
113
+ if (this.config.classes.length > 0 && !this.config.classes.includes(detection.class)) continue;
114
+ if (detection.score < this.config.minConfidence) continue;
115
+ const now = Date.now();
116
+ const minInterval = 1e3 / this.config.maxPerSecPerCamera;
117
+ const lastTime = this.lastEmbedTime.get(deviceId) ?? 0;
118
+ if (now - lastTime < minInterval) continue;
119
+ const trackId = detection.trackId ?? `${deviceId}-${now}`;
120
+ const frameData = pipelineResult.frameData;
121
+ const frameWidth = pipelineResult.imageWidth;
122
+ const frameHeight = pipelineResult.imageHeight;
123
+ if (!frameData || !frameWidth || !frameHeight) continue;
124
+ const pending = {
125
+ trackId,
126
+ deviceId,
127
+ class: detection.class,
128
+ confidence: detection.score,
129
+ frameData,
130
+ frameWidth,
131
+ frameHeight,
132
+ bbox: detection.bbox,
133
+ receivedAt: now
134
+ };
135
+ switch (this.config.cropStrategy) {
136
+ case "first": {
137
+ if (!this.pendingCrops.has(trackId)) {
138
+ this.pendingCrops.set(trackId, pending);
139
+ void this.processOne(pending);
140
+ }
141
+ break;
142
+ }
143
+ case "best-confidence": {
144
+ const existing = this.pendingCrops.get(trackId);
145
+ if (!existing || pending.confidence > existing.confidence) {
146
+ this.pendingCrops.set(trackId, pending);
147
+ }
148
+ break;
149
+ }
150
+ case "track-end": {
151
+ const state = det.objectState?.state;
152
+ if (state === "leaving") {
153
+ this.pendingCrops.set(trackId, pending);
154
+ void this.processOne(pending);
155
+ } else {
156
+ const existing = this.pendingCrops.get(trackId);
157
+ if (!existing || pending.confidence > existing.confidence) {
158
+ this.pendingCrops.set(trackId, pending);
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ }
166
+ async flushPending() {
167
+ const now = Date.now();
168
+ const toFlush = [];
169
+ for (const [trackId, pending] of this.pendingCrops) {
170
+ if (now - pending.receivedAt > 3e3) {
171
+ toFlush.push(pending);
172
+ this.pendingCrops.delete(trackId);
173
+ }
174
+ }
175
+ await Promise.all(toFlush.map((p) => this.processOne(p)));
176
+ }
177
+ async processOne(pending) {
178
+ if (this.encoders.length === 0) return;
179
+ try {
180
+ const { crop, width, height } = await extractCrop(
181
+ pending.frameData,
182
+ pending.frameWidth,
183
+ pending.frameHeight,
184
+ pending.bbox
185
+ );
186
+ const encoder = this.encoders[this._encoderIndex % this.encoders.length];
187
+ this._encoderIndex++;
188
+ const { embedding: _embedding, inferenceMs } = await encoder.encode(crop, width, height);
189
+ const info = encoder.getInfo();
190
+ this._processedCount++;
191
+ this._totalInferenceMs += inferenceMs;
192
+ this.lastEmbedTime.set(pending.deviceId, Date.now());
193
+ this.pendingCrops.delete(pending.trackId);
194
+ const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`;
195
+ const payload = {
196
+ deviceId: Number(pending.deviceId),
197
+ trackId: pending.trackId,
198
+ class: pending.class,
199
+ embeddingId,
200
+ modelId: info.modelId,
201
+ embeddingDim: info.embeddingDim,
202
+ inferenceMs,
203
+ timestamp: Date.now()
204
+ };
205
+ this.eventBus.emit(types.createEvent(
206
+ types.EventCategory.EnrichmentEmbeddingStored,
207
+ { type: "addon", id: "enrichment-engine" },
208
+ payload
209
+ ));
210
+ this.logger.debug("Embedded track", {
211
+ tags: { deviceId: Number(pending.deviceId) },
212
+ meta: { class: pending.class, trackId: pending.trackId, inferenceMs: Number(inferenceMs.toFixed(1)) }
213
+ });
214
+ } catch (err) {
215
+ this.logger.warn("Failed to embed track", {
216
+ tags: { deviceId: Number(pending.deviceId) },
217
+ meta: { trackId: pending.trackId, error: String(err) }
218
+ });
219
+ }
220
+ }
221
+ }
222
+ const TrackPositionSchema = zod.z.object({
223
+ x: zod.z.number(),
224
+ y: zod.z.number(),
225
+ timestamp: zod.z.number(),
226
+ bbox: zod.z.tuple([zod.z.number(), zod.z.number(), zod.z.number(), zod.z.number()])
227
+ });
228
+ const TrackSnapshotSchema = zod.z.object({
229
+ timestamp: zod.z.number(),
230
+ position: TrackPositionSchema,
231
+ thumbnailPath: zod.z.string()
232
+ });
233
+ zod.z.object({
234
+ trackId: zod.z.string(),
235
+ deviceId: zod.z.string(),
236
+ className: zod.z.string(),
237
+ label: zod.z.string().optional(),
238
+ firstSeen: zod.z.number(),
239
+ lastSeen: zod.z.number(),
240
+ positions: zod.z.array(TrackPositionSchema),
241
+ snapshots: zod.z.array(TrackSnapshotSchema),
242
+ totalDistance: zod.z.number(),
243
+ zonesVisited: zod.z.array(zod.z.string()),
244
+ active: zod.z.boolean()
245
+ });
246
+ const SceneMonitorConfigSchema = zod.z.object({
247
+ monitors: zod.z.array(zod.z.object({
248
+ id: zod.z.string(),
249
+ label: zod.z.string(),
250
+ roi: zod.z.object({ x: zod.z.number(), y: zod.z.number(), w: zod.z.number(), h: zod.z.number() }),
251
+ enabled: zod.z.boolean(),
252
+ states: zod.z.array(zod.z.object({
253
+ id: zod.z.string(),
254
+ label: zod.z.string(),
255
+ prompt: zod.z.string().optional(),
256
+ textPrompts: zod.z.array(zod.z.string()).optional(),
257
+ referenceEmbeddings: zod.z.array(zod.z.array(zod.z.number())).optional(),
258
+ threshold: zod.z.number().optional(),
259
+ notify: zod.z.boolean().optional(),
260
+ severity: zod.z.enum(["info", "warning", "alert"]).optional()
261
+ }))
262
+ }))
263
+ });
264
+ class TextEmbeddingCache {
265
+ cache = /* @__PURE__ */ new Map();
266
+ get(key) {
267
+ return this.cache.get(key);
268
+ }
269
+ set(key, embedding) {
270
+ this.cache.set(key, embedding);
271
+ }
272
+ has(key) {
273
+ return this.cache.has(key);
274
+ }
275
+ /**
276
+ * Deletes all entries whose key starts with `${monitorId}:`.
277
+ * Used when a scene monitor's text prompts are updated.
278
+ */
279
+ invalidateMonitor(monitorId) {
280
+ const prefix = `${monitorId}:`;
281
+ for (const key of this.cache.keys()) {
282
+ if (key.startsWith(prefix)) {
283
+ this.cache.delete(key);
284
+ }
285
+ }
286
+ }
287
+ clear() {
288
+ this.cache.clear();
289
+ }
290
+ get size() {
291
+ return this.cache.size;
292
+ }
293
+ }
294
+ class SceneStateWorker {
295
+ config;
296
+ encoders;
297
+ eventBus;
298
+ store;
299
+ streamBrokerRegistry;
300
+ logger;
301
+ textCache = new TextEmbeddingCache();
302
+ monitorStates = /* @__PURE__ */ new Map();
303
+ pollTimer = null;
304
+ _activeCount = 0;
305
+ constructor(deps) {
306
+ this.config = deps.config;
307
+ this.encoders = deps.encoders;
308
+ this.eventBus = deps.eventBus;
309
+ this.store = deps.store;
310
+ this.streamBrokerRegistry = deps.streamBrokerRegistry;
311
+ this.logger = deps.logger;
312
+ }
313
+ async start() {
314
+ if (!this.config.enabled) {
315
+ this.logger.info("SceneStateWorker disabled");
316
+ return;
317
+ }
318
+ this.pollTimer = setInterval(
319
+ () => {
320
+ void this.pollAll();
321
+ },
322
+ this.config.pollIntervalSec * 1e3
323
+ );
324
+ this.logger.info("SceneStateWorker started", { meta: { pollIntervalSec: this.config.pollIntervalSec, hysteresis: this.config.hysteresisCount } });
325
+ }
326
+ async stop() {
327
+ if (this.pollTimer) {
328
+ clearInterval(this.pollTimer);
329
+ this.pollTimer = null;
330
+ }
331
+ }
332
+ get activeCount() {
333
+ return this._activeCount;
334
+ }
335
+ async pollAll() {
336
+ const rawConfigs = await this.store.query.query({ collection: "device-settings", filter: {
337
+ where: { id: "enrichment:scene-monitor:%" }
338
+ } });
339
+ const allConfigs = rawConfigs.map((r) => ({ id: r.id, data: SceneMonitorConfigSchema.parse(r.data) }));
340
+ const deviceMonitors = /* @__PURE__ */ new Map();
341
+ for (const record of allConfigs) {
342
+ const deviceId = record.id.replace("enrichment:scene-monitor:", "");
343
+ const config = record.data;
344
+ const active = config.monitors.filter((m) => m.enabled);
345
+ if (active.length > 0) {
346
+ deviceMonitors.set(deviceId, active);
347
+ }
348
+ }
349
+ this._activeCount = [...deviceMonitors.values()].reduce((sum, ms) => sum + ms.length, 0);
350
+ await Promise.all(
351
+ [...deviceMonitors.entries()].map(
352
+ ([deviceId, monitors]) => this.pollCamera(deviceId, monitors)
353
+ )
354
+ );
355
+ }
356
+ async pollCamera(deviceId, monitors) {
357
+ if (this.encoders.length === 0) return;
358
+ try {
359
+ const snapshot = await this.streamBrokerRegistry.getSnapshot(deviceId);
360
+ if (!snapshot) return;
361
+ const encoder = this.encoders[0];
362
+ for (const monitor of monitors) {
363
+ try {
364
+ await this.processMonitor(deviceId, monitor, snapshot, encoder);
365
+ } catch (err) {
366
+ this.logger.warn("SceneState error", { tags: { deviceId: Number(deviceId) }, meta: { monitorLabel: monitor.label, error: String(err) } });
367
+ }
368
+ }
369
+ } catch (err) {
370
+ this.logger.warn("Failed to capture snapshot", { tags: { deviceId: Number(deviceId) }, meta: { error: String(err) } });
371
+ }
372
+ }
373
+ async processMonitor(deviceId, monitor, snapshot, encoder) {
374
+ const bbox = {
375
+ x: monitor.roi.x * snapshot.width,
376
+ y: monitor.roi.y * snapshot.height,
377
+ w: monitor.roi.w * snapshot.width,
378
+ h: monitor.roi.h * snapshot.height
379
+ };
380
+ const { crop, width, height } = await extractCrop(snapshot.data, snapshot.width, snapshot.height, bbox);
381
+ const { embedding: imageEmb } = await encoder.encode(crop, width, height);
382
+ let bestState = null;
383
+ let bestScore = -1;
384
+ for (const state of monitor.states) {
385
+ let score = 0;
386
+ if (state.referenceEmbeddings && state.referenceEmbeddings.length > 0) {
387
+ for (const refEmb of state.referenceEmbeddings) {
388
+ const ref = new Float32Array(refEmb);
389
+ const sim = cosineSimilarity(imageEmb, ref);
390
+ score = Math.max(score, sim);
391
+ }
392
+ } else if (state.textPrompts && state.textPrompts.length > 0) {
393
+ for (const prompt of state.textPrompts) {
394
+ const cacheKey = `${monitor.id}:${state.id}:${prompt}`;
395
+ let textEmb = this.textCache.get(cacheKey);
396
+ if (!textEmb) {
397
+ const result = await encoder.encodeText(prompt);
398
+ textEmb = result.embedding;
399
+ this.textCache.set(cacheKey, textEmb);
400
+ }
401
+ const sim = cosineSimilarity(imageEmb, textEmb);
402
+ score = Math.max(score, sim);
403
+ }
404
+ }
405
+ if (score > bestScore) {
406
+ bestScore = score;
407
+ bestState = state.label;
408
+ }
409
+ }
410
+ if (!bestState) return;
411
+ const key = `${deviceId}:${monitor.id}`;
412
+ let ms = this.monitorStates.get(key);
413
+ if (!ms) {
414
+ ms = { currentState: null, pendingState: null, pendingCount: 0, pendingConfidence: 0 };
415
+ this.monitorStates.set(key, ms);
416
+ }
417
+ if (bestState === ms.pendingState) {
418
+ ms.pendingCount++;
419
+ ms.pendingConfidence = bestScore;
420
+ } else {
421
+ ms.pendingState = bestState;
422
+ ms.pendingCount = 1;
423
+ ms.pendingConfidence = bestScore;
424
+ }
425
+ if (ms.pendingCount < this.config.hysteresisCount) return;
426
+ if (bestState === ms.currentState) return;
427
+ const previousState = ms.currentState ?? "unknown";
428
+ ms.currentState = bestState;
429
+ ms.pendingState = null;
430
+ ms.pendingCount = 0;
431
+ const payload = {
432
+ deviceId: Number(deviceId),
433
+ monitorId: monitor.id,
434
+ monitorLabel: monitor.label,
435
+ previousState,
436
+ currentState: bestState,
437
+ confidence: bestScore,
438
+ timestamp: Date.now()
439
+ };
440
+ const data = { ...payload };
441
+ this.eventBus.emit({
442
+ id: `scene-state-${deviceId}-${monitor.id}-${Date.now()}`,
443
+ category: types.EventCategory.EnrichmentSceneStateChanged,
444
+ source: { type: "device", id: deviceId },
445
+ timestamp: /* @__PURE__ */ new Date(),
446
+ data
447
+ });
448
+ this.logger.info("Scene state changed", {
449
+ tags: { deviceId: Number(deviceId) },
450
+ meta: { monitorLabel: monitor.label, previousState, currentState: bestState, confidence: Number(bestScore.toFixed(2)) }
451
+ });
452
+ }
453
+ }
454
+ function cosineSimilarity(a, b) {
455
+ let dot = 0;
456
+ let normA = 0;
457
+ let normB = 0;
458
+ for (let i = 0; i < a.length; i++) {
459
+ dot += a[i] * b[i];
460
+ normA += a[i] * a[i];
461
+ normB += b[i] * b[i];
462
+ }
463
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
464
+ }
465
+ class ActivitySummaryWorker {
466
+ config;
467
+ eventBus;
468
+ store;
469
+ logger;
470
+ buffers = /* @__PURE__ */ new Map();
471
+ summaryTimer = null;
472
+ unsubDetection = null;
473
+ unsubSceneState = null;
474
+ _lastSummary = null;
475
+ constructor(deps) {
476
+ this.config = deps.config;
477
+ this.eventBus = deps.eventBus;
478
+ this.store = deps.store;
479
+ this.logger = deps.logger;
480
+ }
481
+ async start() {
482
+ if (!this.config.enabled) {
483
+ this.logger.info("ActivitySummaryWorker disabled");
484
+ return;
485
+ }
486
+ this.unsubDetection = this.eventBus.subscribe(
487
+ { category: types.EventCategory.DetectionResult },
488
+ (event) => {
489
+ this.handleDetection(event);
490
+ }
491
+ );
492
+ this.unsubSceneState = this.eventBus.subscribe(
493
+ { category: types.EventCategory.EnrichmentSceneStateChanged },
494
+ (event) => {
495
+ this.handleSceneStateChange(event);
496
+ }
497
+ );
498
+ this.summaryTimer = setInterval(
499
+ () => {
500
+ void this.emitSummaries();
501
+ },
502
+ this.config.intervalSec * 1e3
503
+ );
504
+ this.logger.info("ActivitySummaryWorker started", { meta: { intervalSec: this.config.intervalSec } });
505
+ }
506
+ async stop() {
507
+ this.unsubDetection?.();
508
+ this.unsubSceneState?.();
509
+ if (this.summaryTimer) {
510
+ clearInterval(this.summaryTimer);
511
+ this.summaryTimer = null;
512
+ }
513
+ await this.emitSummaries();
514
+ }
515
+ get lastSummary() {
516
+ return this._lastSummary;
517
+ }
518
+ getBuffer(deviceId) {
519
+ let buf = this.buffers.get(deviceId);
520
+ if (!buf) {
521
+ buf = { tracks: /* @__PURE__ */ new Map(), zoneEvents: [], stateChanges: [] };
522
+ this.buffers.set(deviceId, buf);
523
+ }
524
+ return buf;
525
+ }
526
+ handleDetection(event) {
527
+ const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
528
+ if (!deviceId) return;
529
+ const results = event.data.analysisResults ?? [];
530
+ const buf = this.getBuffer(deviceId);
531
+ const now = Date.now();
532
+ for (const det of results) {
533
+ const detection = det.detection;
534
+ if (!detection) continue;
535
+ const trackId = detection.trackId ?? `unknown-${now}`;
536
+ const existing = buf.tracks.get(trackId);
537
+ if (existing) {
538
+ existing.lastSeen = now;
539
+ } else {
540
+ buf.tracks.set(trackId, {
541
+ class: detection.class,
542
+ deviceId,
543
+ firstSeen: now,
544
+ lastSeen: now,
545
+ zones: /* @__PURE__ */ new Set()
546
+ });
547
+ }
548
+ const zoneEvents = det.zoneEvents ?? [];
549
+ for (const ze of zoneEvents) {
550
+ const track = buf.tracks.get(trackId);
551
+ if (track) track.zones.add(ze.zoneId);
552
+ if (ze.type === "zone-enter" || ze.type === "zone-exit") {
553
+ buf.zoneEvents.push({
554
+ type: ze.type === "zone-enter" ? "enter" : "exit",
555
+ zoneId: ze.zoneId,
556
+ trackId,
557
+ timestamp: ze.timestamp ?? now
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+ handleSceneStateChange(event) {
564
+ const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
565
+ if (!deviceId) return;
566
+ const data = event.data;
567
+ const buf = this.getBuffer(deviceId);
568
+ buf.stateChanges.push({
569
+ monitorId: data.monitorId,
570
+ from: data.previousState,
571
+ to: data.currentState,
572
+ timestamp: data.timestamp
573
+ });
574
+ }
575
+ async emitSummaries() {
576
+ const now = Date.now();
577
+ for (const [deviceId, buf] of this.buffers) {
578
+ if (buf.tracks.size === 0 && buf.zoneEvents.length === 0 && buf.stateChanges.length === 0) {
579
+ continue;
580
+ }
581
+ const objectCounts = {};
582
+ for (const track of buf.tracks.values()) {
583
+ objectCounts[track.class] = (objectCounts[track.class] ?? 0) + 1;
584
+ }
585
+ const zoneMap = /* @__PURE__ */ new Map();
586
+ for (const ze of buf.zoneEvents) {
587
+ let z = zoneMap.get(ze.zoneId);
588
+ if (!z) {
589
+ z = { entries: 0, exits: 0, dwellTimes: [] };
590
+ zoneMap.set(ze.zoneId, z);
591
+ }
592
+ if (ze.type === "enter") z.entries++;
593
+ else z.exits++;
594
+ }
595
+ const zoneActivity = [...zoneMap.entries()].map(([zoneId, z]) => ({
596
+ zoneId,
597
+ entries: z.entries,
598
+ exits: z.exits,
599
+ avgDwellMs: z.dwellTimes.length > 0 ? z.dwellTimes.reduce((a, b) => a + b, 0) / z.dwellTimes.length : 0
600
+ }));
601
+ const totalEvents = buf.tracks.size + buf.zoneEvents.length;
602
+ const eventsPerMin = totalEvents / (this.config.intervalSec / 60);
603
+ const activityLevel = eventsPerMin >= this.config.activityThresholds.high ? "high" : eventsPerMin >= this.config.activityThresholds.medium ? "medium" : eventsPerMin >= this.config.activityThresholds.low ? "low" : "none";
604
+ const summary = {
605
+ deviceId: Number(deviceId),
606
+ periodStart: now - this.config.intervalSec * 1e3,
607
+ periodEnd: now,
608
+ objectCounts,
609
+ zoneActivity,
610
+ stateChanges: [...buf.stateChanges],
611
+ activityLevel
612
+ };
613
+ this._lastSummary = summary;
614
+ this.eventBus.emit(types.createEvent(
615
+ types.EventCategory.EnrichmentActivitySummary,
616
+ { type: "device", id: deviceId },
617
+ summary
618
+ ));
619
+ try {
620
+ await this.store.insert.mutate({ collection: "addon-settings", record: {
621
+ id: `${deviceId}:${now}`,
622
+ data: { ...summary }
623
+ } });
624
+ } catch {
625
+ }
626
+ buf.tracks.clear();
627
+ buf.zoneEvents.length = 0;
628
+ buf.stateChanges.length = 0;
629
+ }
630
+ }
631
+ }
632
+ class EnrichmentEngineAddon extends types.BaseAddon {
633
+ embeddingDispatcher = null;
634
+ sceneStateWorker = null;
635
+ activitySummary = null;
636
+ currentFlags = {
637
+ embeddingEnabled: true,
638
+ sceneMonitorEnabled: true,
639
+ activitySummaryEnabled: true
640
+ };
641
+ constructor() {
642
+ super({});
643
+ }
644
+ async onInitialize() {
645
+ const config = await this.loadConfig();
646
+ const encoderCollection = this.capabilities?.getCollection?.("embedding-encoder") ?? [];
647
+ const streamBrokerRaw = this.capabilities?.get?.("stream-broker");
648
+ this.embeddingDispatcher = new EmbeddingDispatcher({
649
+ config: config.embedding,
650
+ encoders: encoderCollection,
651
+ eventBus: this.ctx.eventBus,
652
+ logger: this.ctx.logger.child("EmbeddingDispatcher")
653
+ });
654
+ this.sceneStateWorker = new SceneStateWorker({
655
+ config: config.sceneMonitor,
656
+ encoders: encoderCollection,
657
+ eventBus: this.ctx.eventBus,
658
+ store: this.ctx.api.settingsStore,
659
+ streamBrokerRegistry: streamBrokerRaw ?? { getSnapshot: async () => null },
660
+ logger: this.ctx.logger.child("SceneStateWorker")
661
+ });
662
+ this.activitySummary = new ActivitySummaryWorker({
663
+ config: config.activitySummary,
664
+ eventBus: this.ctx.eventBus,
665
+ store: this.ctx.api.settingsStore,
666
+ logger: this.ctx.logger.child("ActivitySummary")
667
+ });
668
+ this.currentFlags = {
669
+ embeddingEnabled: config.embedding.enabled,
670
+ sceneMonitorEnabled: config.sceneMonitor.enabled,
671
+ activitySummaryEnabled: config.activitySummary.enabled
672
+ };
673
+ await this.embeddingDispatcher.start();
674
+ await this.sceneStateWorker.start();
675
+ await this.activitySummary.start();
676
+ this.ctx.logger.info("Enrichment engine initialized with 3 workers");
677
+ }
678
+ async onShutdown() {
679
+ await this.embeddingDispatcher?.stop();
680
+ await this.sceneStateWorker?.stop();
681
+ await this.activitySummary?.stop();
682
+ this.embeddingDispatcher = null;
683
+ this.sceneStateWorker = null;
684
+ this.activitySummary = null;
685
+ }
686
+ // ── Three-level settings API (Phase 3) ─────────────────────────────
687
+ //
688
+ // Enrichment engine is post-detection infra. The three boolean toggle
689
+ // flags (embedding/scene/activity) live in `getGlobalSettings` until
690
+ // the dedicated post-detection UI exists. The underlying
691
+ // EnrichmentConfig blob (stored opaquely in 'addon-settings' →
692
+ // 'enrichment:global') is a separate concern — the flags below mirror
693
+ // the `.enabled` field of each worker's config.
694
+ globalSettingsSchema() {
695
+ return this.schema({
696
+ sections: [
697
+ {
698
+ id: "enrichment-engine-settings",
699
+ title: "Enrichment Engine",
700
+ columns: 2,
701
+ fields: [
702
+ {
703
+ type: "boolean",
704
+ key: "embeddingEnabled",
705
+ label: "Embedding Enabled",
706
+ description: "Compute face/object embeddings from detection crops.",
707
+ default: true
708
+ },
709
+ {
710
+ type: "boolean",
711
+ key: "sceneMonitorEnabled",
712
+ label: "Scene Monitor Enabled",
713
+ description: "Run periodic scene state capture for change detection.",
714
+ default: true
715
+ },
716
+ {
717
+ type: "boolean",
718
+ key: "activitySummaryEnabled",
719
+ label: "Activity Summary Enabled",
720
+ description: "Aggregate hourly activity summaries per camera.",
721
+ default: true
722
+ }
723
+ ]
724
+ }
725
+ ]
726
+ });
727
+ }
728
+ async updateGlobalSettings(patch) {
729
+ await this.ctx?.settings?.writeAddonStore(patch);
730
+ const prev = this.currentFlags;
731
+ const next = {
732
+ embeddingEnabled: typeof patch["embeddingEnabled"] === "boolean" ? patch["embeddingEnabled"] : prev.embeddingEnabled,
733
+ sceneMonitorEnabled: typeof patch["sceneMonitorEnabled"] === "boolean" ? patch["sceneMonitorEnabled"] : prev.sceneMonitorEnabled,
734
+ activitySummaryEnabled: typeof patch["activitySummaryEnabled"] === "boolean" ? patch["activitySummaryEnabled"] : prev.activitySummaryEnabled
735
+ };
736
+ this.currentFlags = next;
737
+ if (prev.embeddingEnabled && !next.embeddingEnabled) {
738
+ this.ctx?.logger.info("Stopping embedding dispatcher (disabled via config)");
739
+ await this.embeddingDispatcher?.stop();
740
+ } else if (!prev.embeddingEnabled && next.embeddingEnabled) {
741
+ this.ctx?.logger.info("Starting embedding dispatcher (enabled via config)");
742
+ await this.embeddingDispatcher?.start();
743
+ }
744
+ if (prev.sceneMonitorEnabled && !next.sceneMonitorEnabled) {
745
+ this.ctx?.logger.info("Stopping scene state worker (disabled via config)");
746
+ await this.sceneStateWorker?.stop();
747
+ } else if (!prev.sceneMonitorEnabled && next.sceneMonitorEnabled) {
748
+ this.ctx?.logger.info("Starting scene state worker (enabled via config)");
749
+ await this.sceneStateWorker?.start();
750
+ }
751
+ if (prev.activitySummaryEnabled && !next.activitySummaryEnabled) {
752
+ this.ctx?.logger.info("Stopping activity summary worker (disabled via config)");
753
+ await this.activitySummary?.stop();
754
+ } else if (!prev.activitySummaryEnabled && next.activitySummaryEnabled) {
755
+ this.ctx?.logger.info("Starting activity summary worker (enabled via config)");
756
+ await this.activitySummary?.start();
757
+ }
758
+ this.ctx?.logger.info("Enrichment engine flags updated", { meta: { flags: this.currentFlags } });
759
+ }
760
+ async loadConfig() {
761
+ try {
762
+ const stored = types.asJsonObject(await this.ctx.api?.settingsStore.get.query({ collection: "addon-settings", key: "enrichment:global" }));
763
+ if (stored) {
764
+ const merged = { ...DEFAULT_ENRICHMENT_CONFIG, ...stored };
765
+ return merged;
766
+ }
767
+ } catch {
768
+ }
769
+ return DEFAULT_ENRICHMENT_CONFIG;
770
+ }
771
+ }
772
+ exports.EnrichmentEngineAddon = EnrichmentEngineAddon;
773
+ exports.default = EnrichmentEngineAddon;
774
+ //# sourceMappingURL=index.js.map