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