@camstack/addon-post-analysis 0.1.19 → 0.2.0

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