@camstack/addon-post-analysis 0.1.14 → 0.1.15

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 (48) hide show
  1. package/dist/embedding-encoder/index.js +1 -1
  2. package/dist/embedding-encoder/index.mjs +1 -1
  3. package/dist/enrichment-engine/index.js +75 -7
  4. package/dist/enrichment-engine/index.js.map +1 -1
  5. package/dist/enrichment-engine/index.mjs +75 -7
  6. package/dist/enrichment-engine/index.mjs.map +1 -1
  7. package/dist/{index-tm6O4bWa.mjs → index-CGAj-pkn.mjs} +531 -97
  8. package/dist/index-CGAj-pkn.mjs.map +1 -0
  9. package/dist/{index-DNpNyDJi.js → index-Dbx13pc7.js} +531 -97
  10. package/dist/index-Dbx13pc7.js.map +1 -0
  11. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/LiveStatsTab.d.ts +5 -0
  12. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/index.d.ts +2 -0
  13. package/dist/pipeline-analytics/@mf-types.zip +0 -0
  14. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +19 -0
  15. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BcWYbuKp.mjs +18 -0
  16. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-DuO9h7li.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-D-USVuHq.mjs} +4 -2
  17. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CmqNjq44.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-qQCPW8pT.mjs} +1 -1
  18. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CA8cCIEl.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-Bv9bYz9E.mjs} +1 -1
  19. package/dist/pipeline-analytics/_stub.js +497 -431
  20. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C7e1vVcD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DErNFTYO.mjs} +6 -6
  21. package/dist/pipeline-analytics/{client-DdXDZxzK.mjs → client-DHmQcIWy.mjs} +2990 -3217
  22. package/dist/pipeline-analytics/{hostInit-TFbPssJX.mjs → hostInit-DmzGJewr.mjs} +12 -12
  23. package/dist/pipeline-analytics/{index-DHiJ7A5x.mjs → index-BA65ZJOW.mjs} +1 -1
  24. package/dist/pipeline-analytics/{index-BP0-1QYT.mjs → index-BCEx31Mh.mjs} +3808 -3100
  25. package/dist/pipeline-analytics/{index-kIgjN-uq.mjs → index-CHnXxMRA.mjs} +1 -1
  26. package/dist/pipeline-analytics/index-CWkKuNLr.mjs +232 -0
  27. package/dist/pipeline-analytics/{index-B4OKsa9p.mjs → index-Crs1D0Uu.mjs} +1 -1
  28. package/dist/pipeline-analytics/{index-k0CA0h_r.mjs → index-DicaGC31.mjs} +1 -1
  29. package/dist/pipeline-analytics/index-gbflFMEY.mjs +36403 -0
  30. package/dist/pipeline-analytics/{index-DyYvUfc7.mjs → index-gpelkpEE.mjs} +1 -1
  31. package/dist/pipeline-analytics/index.js +73 -22
  32. package/dist/pipeline-analytics/index.js.map +1 -1
  33. package/dist/pipeline-analytics/index.mjs +73 -22
  34. package/dist/pipeline-analytics/index.mjs.map +1 -1
  35. package/dist/pipeline-analytics/{jsx-runtime-4ro1c69i.mjs → jsx-runtime-Wcfyyyt4.mjs} +1 -1
  36. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  37. package/dist/recording/index.js +2 -2
  38. package/dist/recording/index.mjs +2 -2
  39. package/dist/{recording-coordinator-BVFZsuQg.mjs → recording-coordinator-BjWd7HjD.mjs} +2 -2
  40. package/dist/{recording-coordinator-BVFZsuQg.mjs.map → recording-coordinator-BjWd7HjD.mjs.map} +1 -1
  41. package/dist/{recording-coordinator-CL3NPb1p.js → recording-coordinator-b-7Ast8s.js} +2 -2
  42. package/dist/{recording-coordinator-CL3NPb1p.js.map → recording-coordinator-b-7Ast8s.js.map} +1 -1
  43. package/package.json +5 -1
  44. package/dist/index-DNpNyDJi.js.map +0 -1
  45. package/dist/index-tm6O4bWa.mjs.map +0 -1
  46. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-CpCK52pE.mjs +0 -19
  47. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-DEj77u_A.mjs +0 -15
  48. package/dist/pipeline-analytics/index-BDPtaPA2.mjs +0 -20855
@@ -22,7 +22,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
25
- const index = require("../index-DNpNyDJi.js");
25
+ const index = require("../index-Dbx13pc7.js");
26
26
  const core = require("@camstack/core");
27
27
  const path = require("node:path");
28
28
  const fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { R as RUNTIME_TO_FORMAT$1, P as PYTHON_SCRIPT, f as BACKEND_TO_FORMAT$1, B as BaseAddon, g as embeddingEncoderCapability } from "../index-tm6O4bWa.mjs";
1
+ import { R as RUNTIME_TO_FORMAT$1, P as PYTHON_SCRIPT, f as BACKEND_TO_FORMAT$1, B as BaseAddon, g as embeddingEncoderCapability } from "../index-CGAj-pkn.mjs";
2
2
  import { ModelDownloadService } from "@camstack/core";
3
3
  import * as path from "node:path";
4
4
  import * as fs from "node:fs";
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const index = require("../index-DNpNyDJi.js");
3
+ const index = require("../index-Dbx13pc7.js");
4
+ const shmRing = require("@camstack/shm-ring");
4
5
  const sharp = require("sharp");
5
6
  const DEFAULT_ENRICHMENT_CONFIG = {
6
7
  enabled: false,
@@ -46,11 +47,20 @@ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
46
47
  }).extract({ left, top, width, height }).jpeg({ quality: 90 }).toBuffer();
47
48
  return { crop, width, height };
48
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
+ }
49
56
  class EmbeddingDispatcher {
50
57
  config;
51
58
  encoders;
52
59
  eventBus;
53
60
  logger;
61
+ ownNodeId;
62
+ readers;
63
+ getRemoteFrame;
54
64
  lastEmbedTime = /* @__PURE__ */ new Map();
55
65
  pendingCrops = /* @__PURE__ */ new Map();
56
66
  flushTimer = null;
@@ -63,6 +73,9 @@ class EmbeddingDispatcher {
63
73
  this.encoders = deps.encoders;
64
74
  this.eventBus = deps.eventBus;
65
75
  this.logger = deps.logger;
76
+ this.ownNodeId = deps.ownNodeId;
77
+ this.readers = deps.readers;
78
+ this.getRemoteFrame = deps.getRemoteFrame;
66
79
  }
67
80
  async start() {
68
81
  if (!this.config.enabled) {
@@ -105,7 +118,41 @@ class EmbeddingDispatcher {
105
118
  const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
106
119
  if (!deviceId) return;
107
120
  const detections = data.analysisResults ?? [];
108
- const pipelineResult = data.pipelineResult;
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;
109
156
  for (const det of detections) {
110
157
  const detection = det.detection;
111
158
  if (!detection) continue;
@@ -116,10 +163,6 @@ class EmbeddingDispatcher {
116
163
  const lastTime = this.lastEmbedTime.get(deviceId) ?? 0;
117
164
  if (now - lastTime < minInterval) continue;
118
165
  const trackId = detection.trackId ?? `${deviceId}-${now}`;
119
- const frameData = pipelineResult.frameData;
120
- const frameWidth = pipelineResult.imageWidth;
121
- const frameHeight = pipelineResult.imageHeight;
122
- if (!frameData || !frameWidth || !frameHeight) continue;
123
166
  const pending = {
124
167
  trackId,
125
168
  deviceId,
@@ -632,6 +675,12 @@ class EnrichmentEngineAddon extends index.BaseAddon {
632
675
  embeddingDispatcher = null;
633
676
  sceneStateWorker = null;
634
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;
635
684
  currentFlags = {
636
685
  embeddingEnabled: true,
637
686
  sceneMonitorEnabled: true,
@@ -644,11 +693,28 @@ class EnrichmentEngineAddon extends index.BaseAddon {
644
693
  const config = await this.loadConfig();
645
694
  const encoderCollection = this.capabilities?.getCollection?.("embedding-encoder") ?? [];
646
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
+ };
647
710
  this.embeddingDispatcher = new EmbeddingDispatcher({
648
711
  config: config.embedding,
649
712
  encoders: encoderCollection,
650
713
  eventBus: this.ctx.eventBus,
651
- logger: this.ctx.logger.child("EmbeddingDispatcher")
714
+ logger: this.ctx.logger.child("EmbeddingDispatcher"),
715
+ ownNodeId,
716
+ readers: this.readers,
717
+ getRemoteFrame
652
718
  });
653
719
  this.sceneStateWorker = new SceneStateWorker({
654
720
  config: config.sceneMonitor,
@@ -681,6 +747,8 @@ class EnrichmentEngineAddon extends index.BaseAddon {
681
747
  this.embeddingDispatcher = null;
682
748
  this.sceneStateWorker = null;
683
749
  this.activitySummary = null;
750
+ this.readers?.close();
751
+ this.readers = null;
684
752
  }
685
753
  // ── Three-level settings API (Phase 3) ─────────────────────────────
686
754
  //
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/enrichment-engine/types.ts","../../src/enrichment-engine/services/crop-extractor.ts","../../src/enrichment-engine/workers/embedding-dispatcher.ts","../../src/_analytics-schemas/persistence-records.ts","../../src/enrichment-engine/services/text-embedding-cache.ts","../../src/enrichment-engine/workers/scene-state-worker.ts","../../src/enrichment-engine/workers/activity-summary.ts","../../src/enrichment-engine/index.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Enrichment Engine — Types\n// ---------------------------------------------------------------------------\n\n// --- Normalized Rectangle (0-1 coordinate space) ---\n\nexport interface NormalizedRect {\n readonly x: number // 0-1, left edge\n readonly y: number // 0-1, top edge\n readonly w: number // 0-1, width\n readonly h: number // 0-1, height\n}\n\n// --- Scene State ---\n\nexport interface SceneStateDefinition {\n readonly id: string\n readonly label: string // e.g. \"open\", \"closed\"\n readonly textPrompts: readonly string[] // zero-shot: [\"an open gate\"]\n readonly referenceEmbeddings: readonly (readonly number[])[] // optional: higher accuracy\n readonly notify: boolean\n readonly severity: 'info' | 'warning' | 'alert'\n}\n\nexport interface SceneMonitor {\n readonly id: string\n readonly label: string // e.g. \"Main Gate\"\n readonly roi: NormalizedRect // region of interest\n readonly enabled: boolean\n readonly states: readonly SceneStateDefinition[]\n}\n\nexport interface SceneMonitorConfig {\n readonly monitors: readonly SceneMonitor[]\n}\n\n// --- Enrichment Config (global) ---\n\nexport interface EnrichmentConfig {\n readonly enabled: boolean\n\n readonly embedding: {\n readonly enabled: boolean\n readonly modelId: string // 'clip-vit-b32' | 'clip-vit-b16' | 'siglip2-b16-256'\n readonly agentId: string | 'local'\n readonly runtime: 'node' | 'python'\n readonly backend: 'cpu' | 'coreml' | 'cuda'\n readonly classes: readonly string[] // [] = all classes\n readonly minConfidence: number\n readonly maxPerSecPerCamera: number\n readonly cropStrategy: 'first' | 'best-confidence' | 'track-end'\n readonly retentionDays: number\n }\n\n readonly sceneMonitor: {\n readonly enabled: boolean\n readonly modelId: string // can differ from embedding model\n readonly pollIntervalSec: number\n readonly hysteresisCount: number\n }\n\n readonly activitySummary: {\n readonly enabled: boolean\n readonly intervalSec: number\n readonly activityThresholds: {\n readonly low: number\n readonly medium: number\n readonly high: number\n }\n }\n}\n\n// --- Events ---\n\nexport interface EmbeddingStoredEvent {\n readonly deviceId: number\n readonly trackId: string\n readonly class: string\n readonly embeddingId: string // reference in vector store\n readonly modelId: string\n readonly embeddingDim: number\n readonly inferenceMs: number\n readonly timestamp: number\n}\n\nexport interface SceneStateChangedEvent {\n readonly deviceId: number\n readonly monitorId: string\n readonly monitorLabel: string\n readonly previousState: string\n readonly currentState: string\n readonly confidence: number\n readonly timestamp: number\n}\n\nexport interface ZoneActivityEntry {\n readonly zoneId: string\n readonly entries: number\n readonly exits: number\n readonly avgDwellMs: number\n}\n\nexport interface StateChangeEntry {\n readonly monitorId: string\n readonly from: string\n readonly to: string\n readonly timestamp: number\n}\n\nexport interface ActivitySummaryEvent {\n readonly deviceId: number\n readonly periodStart: number\n readonly periodEnd: number\n readonly objectCounts: Readonly<Record<string, number>>\n readonly zoneActivity: readonly ZoneActivityEntry[]\n readonly stateChanges: readonly StateChangeEntry[]\n readonly activityLevel: 'none' | 'low' | 'medium' | 'high'\n}\n\n// --- Defaults ---\n\nexport const DEFAULT_ENRICHMENT_CONFIG: EnrichmentConfig = {\n enabled: false,\n\n embedding: {\n enabled: false,\n modelId: 'clip-vit-b32',\n agentId: 'local',\n runtime: 'node',\n backend: 'cpu',\n classes: [],\n minConfidence: 0.5,\n maxPerSecPerCamera: 1,\n cropStrategy: 'first',\n retentionDays: 30,\n },\n\n sceneMonitor: {\n enabled: false,\n modelId: 'clip-vit-b32',\n pollIntervalSec: 10,\n hysteresisCount: 3,\n },\n\n activitySummary: {\n enabled: false,\n intervalSec: 60,\n activityThresholds: {\n low: 2,\n medium: 10,\n high: 30,\n },\n },\n}\n","import sharp from 'sharp'\n\n/**\n * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.\n * Coordinates are clamped to frame bounds to avoid out-of-range errors.\n */\nexport async function extractCrop(\n frameData: Buffer,\n frameWidth: number,\n frameHeight: number,\n bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number },\n): Promise<{ crop: Buffer; width: number; height: number }> {\n // Convert normalized coordinates to pixel values\n const rawLeft = Math.round(bbox.x * frameWidth)\n const rawTop = Math.round(bbox.y * frameHeight)\n const rawWidth = Math.round(bbox.w * frameWidth)\n const rawHeight = Math.round(bbox.h * frameHeight)\n\n // Clamp to frame bounds\n const left = Math.max(0, Math.min(rawLeft, frameWidth - 1))\n const top = Math.max(0, Math.min(rawTop, frameHeight - 1))\n const width = Math.max(1, Math.min(rawWidth, frameWidth - left))\n const height = Math.max(1, Math.min(rawHeight, frameHeight - top))\n\n const crop = await sharp(frameData, {\n raw: { width: frameWidth, height: frameHeight, channels: 3 },\n })\n .extract({ left, top, width, height })\n .jpeg({ quality: 90 })\n .toBuffer()\n\n return { crop, width, height }\n}\n","import { EventCategory, createEvent } from '@camstack/types'\nimport type { IEventBus, IScopedLogger, TypedSystemEvent } from '@camstack/types'\nimport type { EnrichmentConfig, EmbeddingStoredEvent } from '../types.js'\nimport { extractCrop } from '../services/crop-extractor.js'\n\ninterface EmbeddingEncoder {\n encode(crop: Buffer, width: number, height: number): Promise<{ embedding: Float32Array; inferenceMs: number }>\n getInfo(): { modelId: string; embeddingDim: number; ready: boolean }\n}\n\ninterface PendingCrop {\n readonly trackId: string\n readonly deviceId: string\n readonly class: string\n readonly confidence: number\n readonly frameData: Buffer\n readonly frameWidth: number\n readonly frameHeight: number\n readonly bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n readonly receivedAt: number\n}\n\n/** Local narrow of a pipeline analysis result item (runtime shape from detection pipeline). */\ninterface PipelineAnalysisItem {\n readonly detection?: {\n readonly class: string\n readonly score: number\n readonly trackId?: string\n readonly bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n }\n readonly objectState?: {\n readonly state: string\n }\n readonly zoneEvents?: readonly { readonly zoneId: string; readonly type: string; readonly timestamp?: number }[]\n}\n\n/** Extended pipeline result with frame data attached by the pipeline stage. */\ninterface PipelineResultWithFrame {\n readonly frameData?: Buffer\n readonly imageWidth?: number\n readonly imageHeight?: number\n [key: string]: unknown\n}\n\nexport interface EmbeddingDispatcherDeps {\n readonly config: EnrichmentConfig['embedding']\n readonly encoders: readonly EmbeddingEncoder[]\n readonly eventBus: IEventBus\n readonly logger: IScopedLogger\n}\n\nexport class EmbeddingDispatcher {\n private readonly config: EnrichmentConfig['embedding']\n private readonly encoders: readonly EmbeddingEncoder[]\n private readonly eventBus: IEventBus\n private readonly logger: IScopedLogger\n\n private readonly lastEmbedTime = new Map<string, number>()\n private readonly pendingCrops = new Map<string, PendingCrop>()\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private unsubscribe: (() => void) | null = null\n\n private _processedCount = 0\n private _totalInferenceMs = 0\n private _encoderIndex = 0\n\n constructor(deps: EmbeddingDispatcherDeps) {\n this.config = deps.config\n this.encoders = deps.encoders\n this.eventBus = deps.eventBus\n this.logger = deps.logger\n }\n\n async start(): Promise<void> {\n if (!this.config.enabled) {\n this.logger.info('EmbeddingDispatcher disabled')\n return\n }\n\n this.unsubscribe = this.eventBus.subscribe(\n { category: EventCategory.DetectionResult },\n (event) => { void this.handleDetectionResult(event) },\n )\n\n // Flush timer for best-confidence strategy\n if (this.config.cropStrategy === 'best-confidence') {\n this.flushTimer = setInterval(() => { void this.flushPending() }, 1000)\n }\n\n this.logger.info('EmbeddingDispatcher started', { meta: { strategy: this.config.cropStrategy, maxPerSec: this.config.maxPerSecPerCamera } })\n }\n\n async stop(): Promise<void> {\n this.unsubscribe?.()\n this.unsubscribe = null\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n // Flush remaining\n await this.flushPending()\n }\n\n get processedCount(): number { return this._processedCount }\n get avgInferenceMs(): number { return this._processedCount > 0 ? this._totalInferenceMs / this._processedCount : 0 }\n get queueDepth(): number { return this.pendingCrops.size }\n\n private async handleDetectionResult(event: TypedSystemEvent<'detection.result'>): Promise<void> {\n const data = event.data\n const deviceId = event.source.id !== undefined ? String(event.source.id) : ''\n if (!deviceId) return\n\n // analysisResults items are untyped pipeline outputs — narrow to local interface at boundary\n const detections = (data.analysisResults as readonly PipelineAnalysisItem[]) ?? []\n // pipelineResult may carry frame data attached by the analysis stage — narrow at boundary\n const pipelineResult = data.pipelineResult as unknown as PipelineResultWithFrame\n\n for (const det of detections) {\n const detection = det.detection\n if (!detection) continue\n\n // Class filter\n if (this.config.classes.length > 0 && !this.config.classes.includes(detection.class)) continue\n\n // Confidence filter\n if (detection.score < this.config.minConfidence) continue\n\n // Rate limit\n const now = Date.now()\n const minInterval = 1000 / this.config.maxPerSecPerCamera\n const lastTime = this.lastEmbedTime.get(deviceId) ?? 0\n if (now - lastTime < minInterval) continue\n\n const trackId = detection.trackId ?? `${deviceId}-${now}`\n const frameData = pipelineResult.frameData\n const frameWidth = pipelineResult.imageWidth\n const frameHeight = pipelineResult.imageHeight\n\n if (!frameData || !frameWidth || !frameHeight) continue\n\n const pending: PendingCrop = {\n trackId,\n deviceId,\n class: detection.class,\n confidence: detection.score,\n frameData,\n frameWidth,\n frameHeight,\n bbox: detection.bbox,\n receivedAt: now,\n }\n\n switch (this.config.cropStrategy) {\n case 'first': {\n if (!this.pendingCrops.has(trackId)) {\n this.pendingCrops.set(trackId, pending)\n void this.processOne(pending)\n }\n break\n }\n case 'best-confidence': {\n const existing = this.pendingCrops.get(trackId)\n if (!existing || pending.confidence > existing.confidence) {\n this.pendingCrops.set(trackId, pending)\n }\n break\n }\n case 'track-end': {\n const state = det.objectState?.state\n if (state === 'leaving') {\n this.pendingCrops.set(trackId, pending)\n void this.processOne(pending)\n } else {\n const existing = this.pendingCrops.get(trackId)\n if (!existing || pending.confidence > existing.confidence) {\n this.pendingCrops.set(trackId, pending)\n }\n }\n break\n }\n }\n }\n }\n\n private async flushPending(): Promise<void> {\n const now = Date.now()\n const toFlush: PendingCrop[] = []\n\n for (const [trackId, pending] of this.pendingCrops) {\n if (now - pending.receivedAt > 3000) {\n toFlush.push(pending)\n this.pendingCrops.delete(trackId)\n }\n }\n\n await Promise.all(toFlush.map(p => this.processOne(p)))\n }\n\n private async processOne(pending: PendingCrop): Promise<void> {\n if (this.encoders.length === 0) return\n\n try {\n const { crop, width, height } = await extractCrop(\n pending.frameData, pending.frameWidth, pending.frameHeight, pending.bbox,\n )\n\n // Round-robin across encoders\n const encoder = this.encoders[this._encoderIndex % this.encoders.length]!\n this._encoderIndex++\n\n const { embedding: _embedding, inferenceMs } = await encoder.encode(crop, width, height)\n const info = encoder.getInfo()\n\n this._processedCount++\n this._totalInferenceMs += inferenceMs\n this.lastEmbedTime.set(pending.deviceId, Date.now())\n this.pendingCrops.delete(pending.trackId)\n\n const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`\n\n const payload: EmbeddingStoredEvent = {\n deviceId: Number(pending.deviceId),\n trackId: pending.trackId,\n class: pending.class,\n embeddingId,\n modelId: info.modelId,\n embeddingDim: info.embeddingDim,\n inferenceMs,\n timestamp: Date.now(),\n }\n\n this.eventBus.emit(createEvent(\n EventCategory.EnrichmentEmbeddingStored,\n { type: 'addon', id: 'enrichment-engine' },\n payload,\n ))\n\n this.logger.debug('Embedded track', {\n tags: { deviceId: Number(pending.deviceId) },\n meta: { class: pending.class, trackId: pending.trackId, inferenceMs: Number(inferenceMs.toFixed(1)) },\n })\n } catch (err) {\n this.logger.warn('Failed to embed track', {\n tags: { deviceId: Number(pending.deviceId) },\n meta: { trackId: pending.trackId, error: String(err) },\n })\n }\n }\n}\n","/**\n * Zod schemas for analytics-suite persisted record types.\n * Used by persistence services to parse settings-store query results.\n */\nimport { z } from 'zod'\n\n// ── Track Trail ─────────────────────────────────────────────────────\n\nexport const TrackPositionSchema = z.object({\n x: z.number(),\n y: z.number(),\n timestamp: z.number(),\n bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]),\n})\n\nexport const TrackSnapshotSchema = z.object({\n timestamp: z.number(),\n position: TrackPositionSchema,\n thumbnailPath: z.string(),\n})\n\nexport const TrackTrailSchema = z.object({\n trackId: z.string(),\n deviceId: z.string(),\n className: z.string(),\n label: z.string().optional(),\n firstSeen: z.number(),\n lastSeen: z.number(),\n positions: z.array(TrackPositionSchema),\n snapshots: z.array(TrackSnapshotSchema),\n totalDistance: z.number(),\n zonesVisited: z.array(z.string()),\n active: z.boolean(),\n})\n\nexport type TrackTrail = z.infer<typeof TrackTrailSchema>\n\n// ── Scene Monitor ───────────────────────────────────────────────────\n\nexport const SceneMonitorConfigSchema = z.object({\n monitors: z.array(z.object({\n id: z.string(),\n label: z.string(),\n roi: z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() }),\n enabled: z.boolean(),\n states: z.array(z.object({\n id: z.string(),\n label: z.string(),\n prompt: z.string().optional(),\n textPrompts: z.array(z.string()).optional(),\n referenceEmbeddings: z.array(z.array(z.number())).optional(),\n threshold: z.number().optional(),\n notify: z.boolean().optional(),\n severity: z.enum(['info', 'warning', 'alert']).optional(),\n })),\n })),\n})\n\nexport type SceneMonitorConfig = z.infer<typeof SceneMonitorConfigSchema>\n\n/** Single scene monitor entry (element of SceneMonitorConfig.monitors). */\nexport type SceneMonitor = SceneMonitorConfig['monitors'][number]\n","/**\n * In-memory cache for text prompt embeddings.\n *\n * Keys follow the convention `${monitorId}:${text}` so that all entries\n * belonging to a specific scene monitor can be invalidated in one call.\n */\nexport class TextEmbeddingCache {\n private readonly cache = new Map<string, Float32Array>()\n\n get(key: string): Float32Array | undefined {\n return this.cache.get(key)\n }\n\n set(key: string, embedding: Float32Array): void {\n this.cache.set(key, embedding)\n }\n\n has(key: string): boolean {\n return this.cache.has(key)\n }\n\n /**\n * Deletes all entries whose key starts with `${monitorId}:`.\n * Used when a scene monitor's text prompts are updated.\n */\n invalidateMonitor(monitorId: string): void {\n const prefix = `${monitorId}:`\n for (const key of this.cache.keys()) {\n if (key.startsWith(prefix)) {\n this.cache.delete(key)\n }\n }\n }\n\n clear(): void {\n this.cache.clear()\n }\n\n get size(): number {\n return this.cache.size\n }\n}\n","import { EventCategory } from '@camstack/types'\nimport type { IEventBus, IScopedLogger, SettingsStoreClient } from '@camstack/types'\nimport type { EnrichmentConfig, SceneStateChangedEvent } from '../types.js'\nimport { SceneMonitorConfigSchema, type SceneMonitor } from '../../_analytics-schemas/persistence-records.js'\nimport { TextEmbeddingCache } from '../services/text-embedding-cache.js'\nimport { extractCrop } from '../services/crop-extractor.js'\n\ninterface EmbeddingEncoder {\n encode(crop: Buffer, width: number, height: number): Promise<{ embedding: Float32Array; inferenceMs: number }>\n encodeText(text: string): Promise<{ embedding: Float32Array }>\n getInfo(): { modelId: string; embeddingDim: number; ready: boolean }\n}\n\ninterface StreamBrokerRegistry {\n getSnapshot(deviceId: string): Promise<{ data: Buffer; width: number; height: number } | null>\n}\n\ninterface CameraMonitorState {\n currentState: string | null\n pendingState: string | null\n pendingCount: number\n pendingConfidence: number\n}\n\nexport interface SceneStateWorkerDeps {\n readonly config: EnrichmentConfig['sceneMonitor']\n readonly encoders: readonly EmbeddingEncoder[]\n readonly eventBus: IEventBus\n readonly store: SettingsStoreClient\n readonly streamBrokerRegistry: StreamBrokerRegistry\n readonly logger: IScopedLogger\n}\n\nexport class SceneStateWorker {\n private readonly config: EnrichmentConfig['sceneMonitor']\n private readonly encoders: readonly EmbeddingEncoder[]\n private readonly eventBus: IEventBus\n private readonly store: SettingsStoreClient\n private readonly streamBrokerRegistry: StreamBrokerRegistry\n private readonly logger: IScopedLogger\n\n private readonly textCache = new TextEmbeddingCache()\n private readonly monitorStates = new Map<string, CameraMonitorState>()\n private pollTimer: ReturnType<typeof setInterval> | null = null\n private _activeCount = 0\n\n constructor(deps: SceneStateWorkerDeps) {\n this.config = deps.config\n this.encoders = deps.encoders\n this.eventBus = deps.eventBus\n this.store = deps.store\n this.streamBrokerRegistry = deps.streamBrokerRegistry\n this.logger = deps.logger\n }\n\n async start(): Promise<void> {\n if (!this.config.enabled) {\n this.logger.info('SceneStateWorker disabled')\n return\n }\n\n this.pollTimer = setInterval(\n () => { void this.pollAll() },\n this.config.pollIntervalSec * 1000,\n )\n this.logger.info('SceneStateWorker started', { meta: { pollIntervalSec: this.config.pollIntervalSec, hysteresis: this.config.hysteresisCount } })\n }\n\n async stop(): Promise<void> {\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n get activeCount(): number { return this._activeCount }\n\n private async pollAll(): Promise<void> {\n // Load all scene monitor configs from settings\n const rawConfigs = await this.store.query.query({ collection: 'device-settings', filter: {\n where: { id: 'enrichment:scene-monitor:%' },\n } })\n const allConfigs = rawConfigs.map(r => ({ id: r.id, data: SceneMonitorConfigSchema.parse(r.data) }))\n\n const deviceMonitors = new Map<string, SceneMonitor[]>()\n for (const record of allConfigs) {\n const deviceId = record.id.replace('enrichment:scene-monitor:', '')\n const config = record.data\n const active = config.monitors.filter(m => m.enabled)\n if (active.length > 0) {\n deviceMonitors.set(deviceId, active)\n }\n }\n\n this._activeCount = [...deviceMonitors.values()].reduce((sum, ms) => sum + ms.length, 0)\n\n // Process each camera (1 snapshot per camera)\n await Promise.all(\n [...deviceMonitors.entries()].map(([deviceId, monitors]) =>\n this.pollCamera(deviceId, monitors),\n ),\n )\n }\n\n private async pollCamera(deviceId: string, monitors: SceneMonitor[]): Promise<void> {\n if (this.encoders.length === 0) return\n\n try {\n // Capture 1 snapshot\n const snapshot = await this.streamBrokerRegistry.getSnapshot(deviceId)\n if (!snapshot) return\n\n const encoder = this.encoders[0]!\n\n // Process each monitor (crop from same snapshot)\n for (const monitor of monitors) {\n try {\n await this.processMonitor(deviceId, monitor, snapshot, encoder)\n } catch (err) {\n this.logger.warn('SceneState error', { tags: { deviceId: Number(deviceId) }, meta: { monitorLabel: monitor.label, error: String(err) } })\n }\n }\n } catch (err) {\n this.logger.warn('Failed to capture snapshot', { tags: { deviceId: Number(deviceId) }, meta: { error: String(err) } })\n }\n }\n\n private async processMonitor(\n deviceId: string,\n monitor: SceneMonitor,\n snapshot: { data: Buffer; width: number; height: number },\n encoder: EmbeddingEncoder,\n ): Promise<void> {\n // Crop ROI (normalized → pixel coords)\n const bbox = {\n x: monitor.roi.x * snapshot.width,\n y: monitor.roi.y * snapshot.height,\n w: monitor.roi.w * snapshot.width,\n h: monitor.roi.h * snapshot.height,\n }\n const { crop, width, height } = await extractCrop(snapshot.data, snapshot.width, snapshot.height, bbox)\n\n // Encode crop\n const { embedding: imageEmb } = await encoder.encode(crop, width, height)\n\n // Find best matching state\n let bestState: string | null = null\n let bestScore = -1\n\n for (const state of monitor.states) {\n let score = 0\n\n if (state.referenceEmbeddings && state.referenceEmbeddings.length > 0) {\n for (const refEmb of state.referenceEmbeddings) {\n const ref = new Float32Array(refEmb)\n const sim = cosineSimilarity(imageEmb, ref)\n score = Math.max(score, sim)\n }\n } else if (state.textPrompts && state.textPrompts.length > 0) {\n for (const prompt of state.textPrompts) {\n const cacheKey = `${monitor.id}:${state.id}:${prompt}`\n let textEmb = this.textCache.get(cacheKey)\n if (!textEmb) {\n const result = await encoder.encodeText(prompt)\n textEmb = result.embedding\n this.textCache.set(cacheKey, textEmb)\n }\n const sim = cosineSimilarity(imageEmb, textEmb)\n score = Math.max(score, sim)\n }\n }\n\n if (score > bestScore) {\n bestScore = score\n bestState = state.label\n }\n }\n\n if (!bestState) return\n\n // Hysteresis\n const key = `${deviceId}:${monitor.id}`\n let ms = this.monitorStates.get(key)\n if (!ms) {\n ms = { currentState: null, pendingState: null, pendingCount: 0, pendingConfidence: 0 }\n this.monitorStates.set(key, ms)\n }\n\n if (bestState === ms.pendingState) {\n ms.pendingCount++\n ms.pendingConfidence = bestScore\n } else {\n ms.pendingState = bestState\n ms.pendingCount = 1\n ms.pendingConfidence = bestScore\n }\n\n if (ms.pendingCount < this.config.hysteresisCount) return\n if (bestState === ms.currentState) return\n\n // State changed!\n const previousState = ms.currentState ?? 'unknown'\n ms.currentState = bestState\n ms.pendingState = null\n ms.pendingCount = 0\n\n const payload: SceneStateChangedEvent = {\n deviceId: Number(deviceId),\n monitorId: monitor.id,\n monitorLabel: monitor.label,\n previousState,\n currentState: bestState,\n confidence: bestScore,\n timestamp: Date.now(),\n }\n\n const data: Record<string, unknown> = { ...payload }\n this.eventBus.emit({\n id: `scene-state-${deviceId}-${monitor.id}-${Date.now()}`,\n category: EventCategory.EnrichmentSceneStateChanged,\n source: { type: 'device', id: deviceId },\n timestamp: new Date(),\n data,\n })\n\n this.logger.info('Scene state changed', {\n tags: { deviceId: Number(deviceId) },\n meta: { monitorLabel: monitor.label, previousState, currentState: bestState, confidence: Number(bestScore.toFixed(2)) },\n })\n }\n}\n\nfunction cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0\n let normA = 0\n let normB = 0\n for (let i = 0; i < a.length; i++) {\n dot += a[i]! * b[i]!\n normA += a[i]! * a[i]!\n normB += b[i]! * b[i]!\n }\n return dot / (Math.sqrt(normA) * Math.sqrt(normB))\n}\n","import { EventCategory, createEvent } from '@camstack/types'\nimport type { IEventBus, IScopedLogger, SettingsStoreClient, TypedSystemEvent } from '@camstack/types'\nimport type { EnrichmentConfig, ActivitySummaryEvent, StateChangeEntry } from '../types.js'\n\n/** Local narrow of a pipeline analysis result item (runtime shape from detection pipeline). */\ninterface PipelineAnalysisItem {\n readonly detection?: {\n readonly class: string\n readonly trackId?: string\n }\n readonly zoneEvents?: readonly { readonly zoneId: string; readonly type: string; readonly timestamp?: number }[]\n}\n\ninterface TrackInfo {\n class: string\n deviceId: string\n firstSeen: number\n lastSeen: number\n zones: Set<string>\n}\n\ninterface ZoneEventInfo {\n type: 'enter' | 'exit'\n zoneId: string\n trackId: string\n timestamp: number\n}\n\ninterface ActivityBuffer {\n tracks: Map<string, TrackInfo>\n zoneEvents: ZoneEventInfo[]\n stateChanges: StateChangeEntry[]\n}\n\nexport interface ActivitySummaryWorkerDeps {\n readonly config: EnrichmentConfig['activitySummary']\n readonly eventBus: IEventBus\n readonly store: SettingsStoreClient\n readonly logger: IScopedLogger\n}\n\nexport class ActivitySummaryWorker {\n private readonly config: EnrichmentConfig['activitySummary']\n private readonly eventBus: IEventBus\n private readonly store: SettingsStoreClient\n private readonly logger: IScopedLogger\n\n private readonly buffers = new Map<string, ActivityBuffer>()\n private summaryTimer: ReturnType<typeof setInterval> | null = null\n private unsubDetection: (() => void) | null = null\n private unsubSceneState: (() => void) | null = null\n private _lastSummary: ActivitySummaryEvent | null = null\n\n constructor(deps: ActivitySummaryWorkerDeps) {\n this.config = deps.config\n this.eventBus = deps.eventBus\n this.store = deps.store\n this.logger = deps.logger\n }\n\n async start(): Promise<void> {\n if (!this.config.enabled) {\n this.logger.info('ActivitySummaryWorker disabled')\n return\n }\n\n this.unsubDetection = this.eventBus.subscribe(\n { category: EventCategory.DetectionResult },\n (event) => { this.handleDetection(event) },\n )\n\n this.unsubSceneState = this.eventBus.subscribe(\n { category: EventCategory.EnrichmentSceneStateChanged },\n (event) => { this.handleSceneStateChange(event) },\n )\n\n this.summaryTimer = setInterval(\n () => { void this.emitSummaries() },\n this.config.intervalSec * 1000,\n )\n\n this.logger.info('ActivitySummaryWorker started', { meta: { intervalSec: this.config.intervalSec } })\n }\n\n async stop(): Promise<void> {\n this.unsubDetection?.()\n this.unsubSceneState?.()\n if (this.summaryTimer) {\n clearInterval(this.summaryTimer)\n this.summaryTimer = null\n }\n // Emit final summaries\n await this.emitSummaries()\n }\n\n get lastSummary(): ActivitySummaryEvent | null { return this._lastSummary }\n\n private getBuffer(deviceId: string): ActivityBuffer {\n let buf = this.buffers.get(deviceId)\n if (!buf) {\n buf = { tracks: new Map(), zoneEvents: [], stateChanges: [] }\n this.buffers.set(deviceId, buf)\n }\n return buf\n }\n\n private handleDetection(event: TypedSystemEvent<'detection.result'>): void {\n const deviceId = event.source.id !== undefined ? String(event.source.id) : ''\n if (!deviceId) return\n\n // analysisResults items are untyped pipeline outputs — narrow to local interface at boundary\n const results = (event.data.analysisResults as readonly PipelineAnalysisItem[]) ?? []\n const buf = this.getBuffer(deviceId)\n const now = Date.now()\n\n for (const det of results) {\n const detection = det.detection\n if (!detection) continue\n\n const trackId = detection.trackId ?? `unknown-${now}`\n const existing = buf.tracks.get(trackId)\n\n if (existing) {\n existing.lastSeen = now\n } else {\n buf.tracks.set(trackId, {\n class: detection.class,\n deviceId,\n firstSeen: now,\n lastSeen: now,\n zones: new Set(),\n })\n }\n\n // Zone events\n const zoneEvents = det.zoneEvents ?? []\n for (const ze of zoneEvents) {\n const track = buf.tracks.get(trackId)\n if (track) track.zones.add(ze.zoneId)\n\n if (ze.type === 'zone-enter' || ze.type === 'zone-exit') {\n buf.zoneEvents.push({\n type: ze.type === 'zone-enter' ? 'enter' : 'exit',\n zoneId: ze.zoneId,\n trackId,\n timestamp: ze.timestamp ?? now,\n })\n }\n }\n }\n }\n\n private handleSceneStateChange(event: TypedSystemEvent<'enrichment.scene.state-changed'>): void {\n const deviceId = event.source.id !== undefined ? String(event.source.id) : ''\n if (!deviceId) return\n\n const data = event.data\n const buf = this.getBuffer(deviceId)\n buf.stateChanges.push({\n monitorId: data.monitorId,\n from: data.previousState,\n to: data.currentState,\n timestamp: data.timestamp,\n })\n }\n\n private async emitSummaries(): Promise<void> {\n const now = Date.now()\n\n for (const [deviceId, buf] of this.buffers) {\n if (buf.tracks.size === 0 && buf.zoneEvents.length === 0 && buf.stateChanges.length === 0) {\n continue\n }\n\n // Object counts by class\n const objectCounts: Record<string, number> = {}\n for (const track of buf.tracks.values()) {\n objectCounts[track.class] = (objectCounts[track.class] ?? 0) + 1\n }\n\n // Zone activity\n const zoneMap = new Map<string, { entries: number; exits: number; dwellTimes: number[] }>()\n for (const ze of buf.zoneEvents) {\n let z = zoneMap.get(ze.zoneId)\n if (!z) {\n z = { entries: 0, exits: 0, dwellTimes: [] }\n zoneMap.set(ze.zoneId, z)\n }\n if (ze.type === 'enter') z.entries++\n else z.exits++\n }\n\n const zoneActivity = [...zoneMap.entries()].map(([zoneId, z]) => ({\n zoneId,\n entries: z.entries,\n exits: z.exits,\n avgDwellMs: z.dwellTimes.length > 0\n ? z.dwellTimes.reduce((a, b) => a + b, 0) / z.dwellTimes.length\n : 0,\n }))\n\n // Activity level\n const totalEvents = buf.tracks.size + buf.zoneEvents.length\n const eventsPerMin = totalEvents / (this.config.intervalSec / 60)\n const activityLevel = eventsPerMin >= this.config.activityThresholds.high ? 'high'\n : eventsPerMin >= this.config.activityThresholds.medium ? 'medium'\n : eventsPerMin >= this.config.activityThresholds.low ? 'low'\n : 'none' as const\n\n const summary: ActivitySummaryEvent = {\n deviceId: Number(deviceId),\n periodStart: now - this.config.intervalSec * 1000,\n periodEnd: now,\n objectCounts,\n zoneActivity,\n stateChanges: [...buf.stateChanges],\n activityLevel,\n }\n\n this._lastSummary = summary\n\n this.eventBus.emit(createEvent(\n EventCategory.EnrichmentActivitySummary,\n { type: 'device', id: deviceId },\n summary,\n ))\n\n // Persist for timeline UI\n try {\n await this.store.insert.mutate({ collection: 'addon-settings', record: {\n id: `${deviceId}:${now}`,\n data: { ...summary },\n } })\n } catch {\n // Best-effort persistence\n }\n\n // Reset buffer\n buf.tracks.clear()\n buf.zoneEvents.length = 0\n buf.stateChanges.length = 0\n }\n }\n}\n","import { BaseAddon, asJsonObject } from '@camstack/types'\nimport type { EnrichmentConfig } from './types.js'\nimport { DEFAULT_ENRICHMENT_CONFIG } from './types.js'\nimport { EmbeddingDispatcher } from './workers/embedding-dispatcher.js'\nimport type { EmbeddingDispatcherDeps } from './workers/embedding-dispatcher.js'\nimport { SceneStateWorker } from './workers/scene-state-worker.js'\nimport type { SceneStateWorkerDeps } from './workers/scene-state-worker.js'\nimport { ActivitySummaryWorker } from './workers/activity-summary.js'\n\n/**\n * Extended context shape injected at runtime by the server's capability wiring.\n * Not part of the base AddonContext interface because capabilities are resolved\n * after addon initialization.\n */\n\nexport class EnrichmentEngineAddon extends BaseAddon {\n private embeddingDispatcher: EmbeddingDispatcher | null = null\n private sceneStateWorker: SceneStateWorker | null = null\n private activitySummary: ActivitySummaryWorker | null = null\n private currentFlags = {\n embeddingEnabled: true,\n sceneMonitorEnabled: true,\n activitySummaryEnabled: true,\n }\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<void> {\n const config = await this.loadConfig()\n\n // Encoders will be injected by the server via capability wiring.\n // The server extends AddonContext with a `capabilities` object at runtime.\n // Runtime bridges: capabilities.getCollection/get return unknown because\n // the registry holds heterogeneous providers. The shapes are guaranteed\n // by the capability declaration — see project_cast_elimination_checkpoint.md\n // pattern #7.\n const encoderCollection = this.capabilities?.getCollection?.('embedding-encoder') ?? []\n const streamBrokerRaw = this.capabilities?.get?.('stream-broker')\n\n this.embeddingDispatcher = new EmbeddingDispatcher({\n config: config.embedding,\n encoders: encoderCollection as EmbeddingDispatcherDeps['encoders'],\n eventBus: this.ctx.eventBus,\n logger: this.ctx.logger.child('EmbeddingDispatcher'),\n })\n\n this.sceneStateWorker = new SceneStateWorker({\n config: config.sceneMonitor,\n encoders: encoderCollection as SceneStateWorkerDeps['encoders'],\n eventBus: this.ctx.eventBus,\n store: this.ctx.api!.settingsStore,\n streamBrokerRegistry: (streamBrokerRaw ?? { getSnapshot: async () => null }) as SceneStateWorkerDeps['streamBrokerRegistry'],\n logger: this.ctx.logger.child('SceneStateWorker'),\n })\n\n this.activitySummary = new ActivitySummaryWorker({\n config: config.activitySummary,\n eventBus: this.ctx.eventBus,\n store: this.ctx.api!.settingsStore,\n logger: this.ctx.logger.child('ActivitySummary'),\n })\n\n this.currentFlags = {\n embeddingEnabled: config.embedding.enabled,\n sceneMonitorEnabled: config.sceneMonitor.enabled,\n activitySummaryEnabled: config.activitySummary.enabled,\n }\n\n await this.embeddingDispatcher.start()\n await this.sceneStateWorker.start()\n await this.activitySummary.start()\n\n this.ctx.logger.info('Enrichment engine initialized with 3 workers')\n }\n\n protected async onShutdown(): Promise<void> {\n await this.embeddingDispatcher?.stop()\n await this.sceneStateWorker?.stop()\n await this.activitySummary?.stop()\n this.embeddingDispatcher = null\n this.sceneStateWorker = null\n this.activitySummary = null\n }\n\n // ── Three-level settings API (Phase 3) ─────────────────────────────\n //\n // Enrichment engine is post-detection infra. The three boolean toggle\n // flags (embedding/scene/activity) live in `getGlobalSettings` until\n // the dedicated post-detection UI exists. The underlying\n // EnrichmentConfig blob (stored opaquely in 'addon-settings' →\n // 'enrichment:global') is a separate concern — the flags below mirror\n // the `.enabled` field of each worker's config.\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [\n {\n id: 'enrichment-engine-settings',\n title: 'Enrichment Engine',\n columns: 2,\n fields: [\n {\n type: 'boolean',\n key: 'embeddingEnabled',\n label: 'Embedding Enabled',\n description: 'Compute face/object embeddings from detection crops.',\n default: true,\n },\n {\n type: 'boolean',\n key: 'sceneMonitorEnabled',\n label: 'Scene Monitor Enabled',\n description: 'Run periodic scene state capture for change detection.',\n default: true,\n },\n {\n type: 'boolean',\n key: 'activitySummaryEnabled',\n label: 'Activity Summary Enabled',\n description: 'Aggregate hourly activity summaries per camera.',\n default: true,\n },\n ],\n },\n ],\n })\n }\n\n async updateGlobalSettings(patch: Record<string, unknown>): Promise<void> {\n await this.ctx?.settings?.writeAddonStore(patch)\n const prev = this.currentFlags\n const next = {\n embeddingEnabled: typeof patch['embeddingEnabled'] === 'boolean' ? patch['embeddingEnabled'] : prev.embeddingEnabled,\n sceneMonitorEnabled: typeof patch['sceneMonitorEnabled'] === 'boolean' ? patch['sceneMonitorEnabled'] : prev.sceneMonitorEnabled,\n activitySummaryEnabled: typeof patch['activitySummaryEnabled'] === 'boolean' ? patch['activitySummaryEnabled'] : prev.activitySummaryEnabled,\n }\n this.currentFlags = next\n\n // Toggle workers based on flag changes\n if (prev.embeddingEnabled && !next.embeddingEnabled) {\n this.ctx?.logger.info('Stopping embedding dispatcher (disabled via config)')\n await this.embeddingDispatcher?.stop()\n } else if (!prev.embeddingEnabled && next.embeddingEnabled) {\n this.ctx?.logger.info('Starting embedding dispatcher (enabled via config)')\n await this.embeddingDispatcher?.start()\n }\n\n if (prev.sceneMonitorEnabled && !next.sceneMonitorEnabled) {\n this.ctx?.logger.info('Stopping scene state worker (disabled via config)')\n await this.sceneStateWorker?.stop()\n } else if (!prev.sceneMonitorEnabled && next.sceneMonitorEnabled) {\n this.ctx?.logger.info('Starting scene state worker (enabled via config)')\n await this.sceneStateWorker?.start()\n }\n\n if (prev.activitySummaryEnabled && !next.activitySummaryEnabled) {\n this.ctx?.logger.info('Stopping activity summary worker (disabled via config)')\n await this.activitySummary?.stop()\n } else if (!prev.activitySummaryEnabled && next.activitySummaryEnabled) {\n this.ctx?.logger.info('Starting activity summary worker (enabled via config)')\n await this.activitySummary?.start()\n }\n\n this.ctx?.logger.info('Enrichment engine flags updated', { meta: { flags: this.currentFlags } })\n }\n\n private async loadConfig(): Promise<EnrichmentConfig> {\n try {\n const stored = asJsonObject(await this.ctx.api?.settingsStore.get.query({ collection: 'addon-settings', key: 'enrichment:global' }))\n if (stored) {\n // Shallow structural merge: only keys from DEFAULT_ENRICHMENT_CONFIG\n // are preserved; stored values override their counterparts when\n // present with a matching type.\n const merged: EnrichmentConfig = { ...DEFAULT_ENRICHMENT_CONFIG, ...stored }\n return merged\n }\n } catch {\n // First boot — use defaults\n }\n return DEFAULT_ENRICHMENT_CONFIG\n }\n}\n\n// Default export — kernel addon-loader prefers mod.default to identify the addon class\nexport default EnrichmentEngineAddon\n"],"names":["EventCategory","createEvent","z.object","z.number","z.tuple","z.string","z.array","z.boolean","z.enum","BaseAddon","asJsonObject"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/enrichment-engine/types.ts","../../src/enrichment-engine/services/crop-extractor.ts","../../src/enrichment-engine/services/resolve-frame.ts","../../src/enrichment-engine/workers/embedding-dispatcher.ts","../../src/_analytics-schemas/persistence-records.ts","../../src/enrichment-engine/services/text-embedding-cache.ts","../../src/enrichment-engine/workers/scene-state-worker.ts","../../src/enrichment-engine/workers/activity-summary.ts","../../src/enrichment-engine/index.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Enrichment Engine — Types\n// ---------------------------------------------------------------------------\n\n// --- Normalized Rectangle (0-1 coordinate space) ---\n\nexport interface NormalizedRect {\n readonly x: number // 0-1, left edge\n readonly y: number // 0-1, top edge\n readonly w: number // 0-1, width\n readonly h: number // 0-1, height\n}\n\n// --- Scene State ---\n\nexport interface SceneStateDefinition {\n readonly id: string\n readonly label: string // e.g. \"open\", \"closed\"\n readonly textPrompts: readonly string[] // zero-shot: [\"an open gate\"]\n readonly referenceEmbeddings: readonly (readonly number[])[] // optional: higher accuracy\n readonly notify: boolean\n readonly severity: 'info' | 'warning' | 'alert'\n}\n\nexport interface SceneMonitor {\n readonly id: string\n readonly label: string // e.g. \"Main Gate\"\n readonly roi: NormalizedRect // region of interest\n readonly enabled: boolean\n readonly states: readonly SceneStateDefinition[]\n}\n\nexport interface SceneMonitorConfig {\n readonly monitors: readonly SceneMonitor[]\n}\n\n// --- Enrichment Config (global) ---\n\nexport interface EnrichmentConfig {\n readonly enabled: boolean\n\n readonly embedding: {\n readonly enabled: boolean\n readonly modelId: string // 'clip-vit-b32' | 'clip-vit-b16' | 'siglip2-b16-256'\n readonly agentId: string | 'local'\n readonly runtime: 'node' | 'python'\n readonly backend: 'cpu' | 'coreml' | 'cuda'\n readonly classes: readonly string[] // [] = all classes\n readonly minConfidence: number\n readonly maxPerSecPerCamera: number\n readonly cropStrategy: 'first' | 'best-confidence' | 'track-end'\n readonly retentionDays: number\n }\n\n readonly sceneMonitor: {\n readonly enabled: boolean\n readonly modelId: string // can differ from embedding model\n readonly pollIntervalSec: number\n readonly hysteresisCount: number\n }\n\n readonly activitySummary: {\n readonly enabled: boolean\n readonly intervalSec: number\n readonly activityThresholds: {\n readonly low: number\n readonly medium: number\n readonly high: number\n }\n }\n}\n\n// --- Events ---\n\nexport interface EmbeddingStoredEvent {\n readonly deviceId: number\n readonly trackId: string\n readonly class: string\n readonly embeddingId: string // reference in vector store\n readonly modelId: string\n readonly embeddingDim: number\n readonly inferenceMs: number\n readonly timestamp: number\n}\n\nexport interface SceneStateChangedEvent {\n readonly deviceId: number\n readonly monitorId: string\n readonly monitorLabel: string\n readonly previousState: string\n readonly currentState: string\n readonly confidence: number\n readonly timestamp: number\n}\n\nexport interface ZoneActivityEntry {\n readonly zoneId: string\n readonly entries: number\n readonly exits: number\n readonly avgDwellMs: number\n}\n\nexport interface StateChangeEntry {\n readonly monitorId: string\n readonly from: string\n readonly to: string\n readonly timestamp: number\n}\n\nexport interface ActivitySummaryEvent {\n readonly deviceId: number\n readonly periodStart: number\n readonly periodEnd: number\n readonly objectCounts: Readonly<Record<string, number>>\n readonly zoneActivity: readonly ZoneActivityEntry[]\n readonly stateChanges: readonly StateChangeEntry[]\n readonly activityLevel: 'none' | 'low' | 'medium' | 'high'\n}\n\n// --- Defaults ---\n\nexport const DEFAULT_ENRICHMENT_CONFIG: EnrichmentConfig = {\n enabled: false,\n\n embedding: {\n enabled: false,\n modelId: 'clip-vit-b32',\n agentId: 'local',\n runtime: 'node',\n backend: 'cpu',\n classes: [],\n minConfidence: 0.5,\n maxPerSecPerCamera: 1,\n cropStrategy: 'first',\n retentionDays: 30,\n },\n\n sceneMonitor: {\n enabled: false,\n modelId: 'clip-vit-b32',\n pollIntervalSec: 10,\n hysteresisCount: 3,\n },\n\n activitySummary: {\n enabled: false,\n intervalSec: 60,\n activityThresholds: {\n low: 2,\n medium: 10,\n high: 30,\n },\n },\n}\n","import sharp from 'sharp'\n\n/**\n * Extracts a JPEG-encoded crop from a raw frame buffer using a normalized bounding box.\n * Coordinates are clamped to frame bounds to avoid out-of-range errors.\n */\nexport async function extractCrop(\n frameData: Buffer,\n frameWidth: number,\n frameHeight: number,\n bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number },\n): Promise<{ crop: Buffer; width: number; height: number }> {\n // Convert normalized coordinates to pixel values\n const rawLeft = Math.round(bbox.x * frameWidth)\n const rawTop = Math.round(bbox.y * frameHeight)\n const rawWidth = Math.round(bbox.w * frameWidth)\n const rawHeight = Math.round(bbox.h * frameHeight)\n\n // Clamp to frame bounds\n const left = Math.max(0, Math.min(rawLeft, frameWidth - 1))\n const top = Math.max(0, Math.min(rawTop, frameHeight - 1))\n const width = Math.max(1, Math.min(rawWidth, frameWidth - left))\n const height = Math.max(1, Math.min(rawHeight, frameHeight - top))\n\n const crop = await sharp(frameData, {\n raw: { width: frameWidth, height: frameHeight, channels: 3 },\n })\n .extract({ left, top, width, height })\n .jpeg({ quality: 90 })\n .toBuffer()\n\n return { crop, width, height }\n}\n","/**\n * `resolveFrame` — turn a serialisable `FrameHandle` back into pixels.\n *\n * The handle's `nodeId` decides the path: when the consumer is co-located\n * with the producing decoder, the pixels are read directly from the shared\n * memory ring (`readers.read(handle)`, a `null` means the slot was recycled\n * before the consumer got to it — a dropped frame, the correct latest-wins\n * behaviour for live video). Otherwise the caller-supplied\n * `getRemoteFrame(handle)` performs a node-routed `decoder.getFrame` call.\n *\n * Keeping the remote fetch as an injected callback keeps this helper free of\n * any cap-client / tRPC coupling and trivially testable.\n */\nimport type { FrameRingReaderCache } from '@camstack/shm-ring'\nimport type { DecodedFrame, FrameHandle } from '@camstack/types'\n\nexport interface ResolveFrameDeps {\n /** Cluster node id of the consumer — used to detect co-location. */\n readonly ownNodeId: string\n /** Opens (and caches) shm segments and reads a `FrameHandle` to pixels. */\n readonly readers: FrameRingReaderCache\n /**\n * Cross-node frame fetch. Wired by the caller to\n * `ctx.api.decoder.getFrame.query({ handle, nodeId: handle.nodeId })`.\n */\n readonly getRemoteFrame: (handle: FrameHandle) => Promise<DecodedFrame | null>\n}\n\n/**\n * Resolve the pixels a `FrameHandle` refers to. Local shm read when the\n * handle's `nodeId` matches `deps.ownNodeId`, else routed via\n * `deps.getRemoteFrame`. Returns `null` when the frame is no longer\n * available (slot recycled locally, or the remote node reports no frame).\n */\nexport async function resolveFrame(\n handle: FrameHandle,\n deps: ResolveFrameDeps,\n): Promise<DecodedFrame | null> {\n if (handle.nodeId === deps.ownNodeId) {\n return deps.readers.read(handle)\n }\n return deps.getRemoteFrame(handle)\n}\n","import { EventCategory, createEvent } from '@camstack/types'\nimport type {\n DecodedFrame,\n FrameHandle,\n IEventBus,\n IScopedLogger,\n TypedSystemEvent,\n} from '@camstack/types'\nimport type { FrameRingReaderCache } from '@camstack/shm-ring'\nimport type { EnrichmentConfig, EmbeddingStoredEvent } from '../types.js'\nimport { extractCrop } from '../services/crop-extractor.js'\nimport { resolveFrame } from '../services/resolve-frame.js'\n\ninterface EmbeddingEncoder {\n encode(crop: Buffer, width: number, height: number): Promise<{ embedding: Float32Array; inferenceMs: number }>\n getInfo(): { modelId: string; embeddingDim: number; ready: boolean }\n}\n\ninterface PendingCrop {\n readonly trackId: string\n readonly deviceId: string\n readonly class: string\n readonly confidence: number\n readonly frameData: Buffer\n readonly frameWidth: number\n readonly frameHeight: number\n readonly bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n readonly receivedAt: number\n}\n\n/** Local narrow of a pipeline analysis result item (runtime shape from detection pipeline). */\ninterface PipelineAnalysisItem {\n readonly detection?: {\n readonly class: string\n readonly score: number\n readonly trackId?: string\n readonly bbox: { readonly x: number; readonly y: number; readonly w: number; readonly h: number }\n }\n readonly objectState?: {\n readonly state: string\n }\n readonly zoneEvents?: readonly { readonly zoneId: string; readonly type: string; readonly timestamp?: number }[]\n}\n\nexport interface EmbeddingDispatcherDeps {\n readonly config: EnrichmentConfig['embedding']\n readonly encoders: readonly EmbeddingEncoder[]\n readonly eventBus: IEventBus\n readonly logger: IScopedLogger\n /** Cluster node id of the consumer — used to detect co-location with the decoder. */\n readonly ownNodeId: string\n /** Opens (and caches) shm segments and reads a `FrameHandle` to pixels. */\n readonly readers: FrameRingReaderCache\n /**\n * Cross-node frame fetch. Wired by the engine to\n * `ctx.api.decoder.getFrame.query({ handle, nodeId: handle.nodeId })`.\n */\n readonly getRemoteFrame: (handle: FrameHandle) => Promise<DecodedFrame | null>\n}\n\nexport class EmbeddingDispatcher {\n private readonly config: EnrichmentConfig['embedding']\n private readonly encoders: readonly EmbeddingEncoder[]\n private readonly eventBus: IEventBus\n private readonly logger: IScopedLogger\n private readonly ownNodeId: string\n private readonly readers: FrameRingReaderCache\n private readonly getRemoteFrame: (handle: FrameHandle) => Promise<DecodedFrame | null>\n\n private readonly lastEmbedTime = new Map<string, number>()\n private readonly pendingCrops = new Map<string, PendingCrop>()\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private unsubscribe: (() => void) | null = null\n\n private _processedCount = 0\n private _totalInferenceMs = 0\n private _encoderIndex = 0\n\n constructor(deps: EmbeddingDispatcherDeps) {\n this.config = deps.config\n this.encoders = deps.encoders\n this.eventBus = deps.eventBus\n this.logger = deps.logger\n this.ownNodeId = deps.ownNodeId\n this.readers = deps.readers\n this.getRemoteFrame = deps.getRemoteFrame\n }\n\n async start(): Promise<void> {\n if (!this.config.enabled) {\n this.logger.info('EmbeddingDispatcher disabled')\n return\n }\n\n this.unsubscribe = this.eventBus.subscribe(\n { category: EventCategory.DetectionResult },\n (event) => { void this.handleDetectionResult(event) },\n )\n\n // Flush timer for best-confidence strategy\n if (this.config.cropStrategy === 'best-confidence') {\n this.flushTimer = setInterval(() => { void this.flushPending() }, 1000)\n }\n\n this.logger.info('EmbeddingDispatcher started', { meta: { strategy: this.config.cropStrategy, maxPerSec: this.config.maxPerSecPerCamera } })\n }\n\n async stop(): Promise<void> {\n this.unsubscribe?.()\n this.unsubscribe = null\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n // Flush remaining\n await this.flushPending()\n }\n\n get processedCount(): number { return this._processedCount }\n get avgInferenceMs(): number { return this._processedCount > 0 ? this._totalInferenceMs / this._processedCount : 0 }\n get queueDepth(): number { return this.pendingCrops.size }\n\n private async handleDetectionResult(event: TypedSystemEvent<'detection.result'>): Promise<void> {\n const data = event.data\n const deviceId = event.source.id !== undefined ? String(event.source.id) : ''\n if (!deviceId) return\n\n // analysisResults items are untyped pipeline outputs — narrow to local interface at boundary\n const detections = (data.analysisResults as readonly PipelineAnalysisItem[]) ?? []\n\n // Resolve the underlying frame once per event via the shm frame plane.\n // The handle's `nodeId` decides the path — local shm read when co-located\n // with the producing decoder, else a node-routed `decoder.getFrame` call.\n const handle = data.frameHandle\n if (!handle) {\n this.logger.debug('skip: no frameHandle on DetectionResult', {\n meta: { deviceId },\n })\n return\n }\n\n let decoded: DecodedFrame | null\n try {\n decoded = await resolveFrame(handle, {\n ownNodeId: this.ownNodeId,\n readers: this.readers,\n getRemoteFrame: this.getRemoteFrame,\n })\n } catch (err) {\n this.logger.debug('skip: resolveFrame threw', {\n meta: { deviceId, shmId: handle.shmId, error: String(err) },\n })\n return\n }\n\n if (!decoded) {\n this.logger.debug('skip: frame recycled before resolve', {\n meta: { deviceId, shmId: handle.shmId },\n })\n return\n }\n\n // Defensive: the live detection stream is decoded as RGB by the pipeline\n // runner. `extractCrop` interprets the buffer as raw RGB — bail if not.\n if (decoded.format !== 'rgb') {\n this.logger.debug('skip: resolved frame is not RGB', {\n meta: { deviceId, format: decoded.format },\n })\n return\n }\n\n const frameData = Buffer.isBuffer(decoded.data) ? decoded.data : Buffer.from(decoded.data)\n const frameWidth = decoded.width\n const frameHeight = decoded.height\n\n for (const det of detections) {\n const detection = det.detection\n if (!detection) continue\n\n // Class filter\n if (this.config.classes.length > 0 && !this.config.classes.includes(detection.class)) continue\n\n // Confidence filter\n if (detection.score < this.config.minConfidence) continue\n\n // Rate limit\n const now = Date.now()\n const minInterval = 1000 / this.config.maxPerSecPerCamera\n const lastTime = this.lastEmbedTime.get(deviceId) ?? 0\n if (now - lastTime < minInterval) continue\n\n const trackId = detection.trackId ?? `${deviceId}-${now}`\n\n const pending: PendingCrop = {\n trackId,\n deviceId,\n class: detection.class,\n confidence: detection.score,\n frameData,\n frameWidth,\n frameHeight,\n bbox: detection.bbox,\n receivedAt: now,\n }\n\n switch (this.config.cropStrategy) {\n case 'first': {\n if (!this.pendingCrops.has(trackId)) {\n this.pendingCrops.set(trackId, pending)\n void this.processOne(pending)\n }\n break\n }\n case 'best-confidence': {\n const existing = this.pendingCrops.get(trackId)\n if (!existing || pending.confidence > existing.confidence) {\n this.pendingCrops.set(trackId, pending)\n }\n break\n }\n case 'track-end': {\n const state = det.objectState?.state\n if (state === 'leaving') {\n this.pendingCrops.set(trackId, pending)\n void this.processOne(pending)\n } else {\n const existing = this.pendingCrops.get(trackId)\n if (!existing || pending.confidence > existing.confidence) {\n this.pendingCrops.set(trackId, pending)\n }\n }\n break\n }\n }\n }\n }\n\n private async flushPending(): Promise<void> {\n const now = Date.now()\n const toFlush: PendingCrop[] = []\n\n for (const [trackId, pending] of this.pendingCrops) {\n if (now - pending.receivedAt > 3000) {\n toFlush.push(pending)\n this.pendingCrops.delete(trackId)\n }\n }\n\n await Promise.all(toFlush.map(p => this.processOne(p)))\n }\n\n private async processOne(pending: PendingCrop): Promise<void> {\n if (this.encoders.length === 0) return\n\n try {\n const { crop, width, height } = await extractCrop(\n pending.frameData, pending.frameWidth, pending.frameHeight, pending.bbox,\n )\n\n // Round-robin across encoders\n const encoder = this.encoders[this._encoderIndex % this.encoders.length]!\n this._encoderIndex++\n\n const { embedding: _embedding, inferenceMs } = await encoder.encode(crop, width, height)\n const info = encoder.getInfo()\n\n this._processedCount++\n this._totalInferenceMs += inferenceMs\n this.lastEmbedTime.set(pending.deviceId, Date.now())\n this.pendingCrops.delete(pending.trackId)\n\n const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`\n\n const payload: EmbeddingStoredEvent = {\n deviceId: Number(pending.deviceId),\n trackId: pending.trackId,\n class: pending.class,\n embeddingId,\n modelId: info.modelId,\n embeddingDim: info.embeddingDim,\n inferenceMs,\n timestamp: Date.now(),\n }\n\n this.eventBus.emit(createEvent(\n EventCategory.EnrichmentEmbeddingStored,\n { type: 'addon', id: 'enrichment-engine' },\n payload,\n ))\n\n this.logger.debug('Embedded track', {\n tags: { deviceId: Number(pending.deviceId) },\n meta: { class: pending.class, trackId: pending.trackId, inferenceMs: Number(inferenceMs.toFixed(1)) },\n })\n } catch (err) {\n this.logger.warn('Failed to embed track', {\n tags: { deviceId: Number(pending.deviceId) },\n meta: { trackId: pending.trackId, error: String(err) },\n })\n }\n }\n}\n","/**\n * Zod schemas for analytics-suite persisted record types.\n * Used by persistence services to parse settings-store query results.\n */\nimport { z } from 'zod'\n\n// ── Track Trail ─────────────────────────────────────────────────────\n\nexport const TrackPositionSchema = z.object({\n x: z.number(),\n y: z.number(),\n timestamp: z.number(),\n bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]),\n})\n\nexport const TrackSnapshotSchema = z.object({\n timestamp: z.number(),\n position: TrackPositionSchema,\n thumbnailPath: z.string(),\n})\n\nexport const TrackTrailSchema = z.object({\n trackId: z.string(),\n deviceId: z.string(),\n className: z.string(),\n label: z.string().optional(),\n firstSeen: z.number(),\n lastSeen: z.number(),\n positions: z.array(TrackPositionSchema),\n snapshots: z.array(TrackSnapshotSchema),\n totalDistance: z.number(),\n zonesVisited: z.array(z.string()),\n active: z.boolean(),\n})\n\nexport type TrackTrail = z.infer<typeof TrackTrailSchema>\n\n// ── Scene Monitor ───────────────────────────────────────────────────\n\nexport const SceneMonitorConfigSchema = z.object({\n monitors: z.array(z.object({\n id: z.string(),\n label: z.string(),\n roi: z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() }),\n enabled: z.boolean(),\n states: z.array(z.object({\n id: z.string(),\n label: z.string(),\n prompt: z.string().optional(),\n textPrompts: z.array(z.string()).optional(),\n referenceEmbeddings: z.array(z.array(z.number())).optional(),\n threshold: z.number().optional(),\n notify: z.boolean().optional(),\n severity: z.enum(['info', 'warning', 'alert']).optional(),\n })),\n })),\n})\n\nexport type SceneMonitorConfig = z.infer<typeof SceneMonitorConfigSchema>\n\n/** Single scene monitor entry (element of SceneMonitorConfig.monitors). */\nexport type SceneMonitor = SceneMonitorConfig['monitors'][number]\n","/**\n * In-memory cache for text prompt embeddings.\n *\n * Keys follow the convention `${monitorId}:${text}` so that all entries\n * belonging to a specific scene monitor can be invalidated in one call.\n */\nexport class TextEmbeddingCache {\n private readonly cache = new Map<string, Float32Array>()\n\n get(key: string): Float32Array | undefined {\n return this.cache.get(key)\n }\n\n set(key: string, embedding: Float32Array): void {\n this.cache.set(key, embedding)\n }\n\n has(key: string): boolean {\n return this.cache.has(key)\n }\n\n /**\n * Deletes all entries whose key starts with `${monitorId}:`.\n * Used when a scene monitor's text prompts are updated.\n */\n invalidateMonitor(monitorId: string): void {\n const prefix = `${monitorId}:`\n for (const key of this.cache.keys()) {\n if (key.startsWith(prefix)) {\n this.cache.delete(key)\n }\n }\n }\n\n clear(): void {\n this.cache.clear()\n }\n\n get size(): number {\n return this.cache.size\n }\n}\n","import { EventCategory } from '@camstack/types'\nimport type { IEventBus, IScopedLogger, SettingsStoreClient } from '@camstack/types'\nimport type { EnrichmentConfig, SceneStateChangedEvent } from '../types.js'\nimport { SceneMonitorConfigSchema, type SceneMonitor } from '../../_analytics-schemas/persistence-records.js'\nimport { TextEmbeddingCache } from '../services/text-embedding-cache.js'\nimport { extractCrop } from '../services/crop-extractor.js'\n\ninterface EmbeddingEncoder {\n encode(crop: Buffer, width: number, height: number): Promise<{ embedding: Float32Array; inferenceMs: number }>\n encodeText(text: string): Promise<{ embedding: Float32Array }>\n getInfo(): { modelId: string; embeddingDim: number; ready: boolean }\n}\n\ninterface StreamBrokerRegistry {\n getSnapshot(deviceId: string): Promise<{ data: Buffer; width: number; height: number } | null>\n}\n\ninterface CameraMonitorState {\n currentState: string | null\n pendingState: string | null\n pendingCount: number\n pendingConfidence: number\n}\n\nexport interface SceneStateWorkerDeps {\n readonly config: EnrichmentConfig['sceneMonitor']\n readonly encoders: readonly EmbeddingEncoder[]\n readonly eventBus: IEventBus\n readonly store: SettingsStoreClient\n readonly streamBrokerRegistry: StreamBrokerRegistry\n readonly logger: IScopedLogger\n}\n\nexport class SceneStateWorker {\n private readonly config: EnrichmentConfig['sceneMonitor']\n private readonly encoders: readonly EmbeddingEncoder[]\n private readonly eventBus: IEventBus\n private readonly store: SettingsStoreClient\n private readonly streamBrokerRegistry: StreamBrokerRegistry\n private readonly logger: IScopedLogger\n\n private readonly textCache = new TextEmbeddingCache()\n private readonly monitorStates = new Map<string, CameraMonitorState>()\n private pollTimer: ReturnType<typeof setInterval> | null = null\n private _activeCount = 0\n\n constructor(deps: SceneStateWorkerDeps) {\n this.config = deps.config\n this.encoders = deps.encoders\n this.eventBus = deps.eventBus\n this.store = deps.store\n this.streamBrokerRegistry = deps.streamBrokerRegistry\n this.logger = deps.logger\n }\n\n async start(): Promise<void> {\n if (!this.config.enabled) {\n this.logger.info('SceneStateWorker disabled')\n return\n }\n\n this.pollTimer = setInterval(\n () => { void this.pollAll() },\n this.config.pollIntervalSec * 1000,\n )\n this.logger.info('SceneStateWorker started', { meta: { pollIntervalSec: this.config.pollIntervalSec, hysteresis: this.config.hysteresisCount } })\n }\n\n async stop(): Promise<void> {\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n get activeCount(): number { return this._activeCount }\n\n private async pollAll(): Promise<void> {\n // Load all scene monitor configs from settings\n const rawConfigs = await this.store.query.query({ collection: 'device-settings', filter: {\n where: { id: 'enrichment:scene-monitor:%' },\n } })\n const allConfigs = rawConfigs.map(r => ({ id: r.id, data: SceneMonitorConfigSchema.parse(r.data) }))\n\n const deviceMonitors = new Map<string, SceneMonitor[]>()\n for (const record of allConfigs) {\n const deviceId = record.id.replace('enrichment:scene-monitor:', '')\n const config = record.data\n const active = config.monitors.filter(m => m.enabled)\n if (active.length > 0) {\n deviceMonitors.set(deviceId, active)\n }\n }\n\n this._activeCount = [...deviceMonitors.values()].reduce((sum, ms) => sum + ms.length, 0)\n\n // Process each camera (1 snapshot per camera)\n await Promise.all(\n [...deviceMonitors.entries()].map(([deviceId, monitors]) =>\n this.pollCamera(deviceId, monitors),\n ),\n )\n }\n\n private async pollCamera(deviceId: string, monitors: SceneMonitor[]): Promise<void> {\n if (this.encoders.length === 0) return\n\n try {\n // Capture 1 snapshot\n const snapshot = await this.streamBrokerRegistry.getSnapshot(deviceId)\n if (!snapshot) return\n\n const encoder = this.encoders[0]!\n\n // Process each monitor (crop from same snapshot)\n for (const monitor of monitors) {\n try {\n await this.processMonitor(deviceId, monitor, snapshot, encoder)\n } catch (err) {\n this.logger.warn('SceneState error', { tags: { deviceId: Number(deviceId) }, meta: { monitorLabel: monitor.label, error: String(err) } })\n }\n }\n } catch (err) {\n this.logger.warn('Failed to capture snapshot', { tags: { deviceId: Number(deviceId) }, meta: { error: String(err) } })\n }\n }\n\n private async processMonitor(\n deviceId: string,\n monitor: SceneMonitor,\n snapshot: { data: Buffer; width: number; height: number },\n encoder: EmbeddingEncoder,\n ): Promise<void> {\n // Crop ROI (normalized → pixel coords)\n const bbox = {\n x: monitor.roi.x * snapshot.width,\n y: monitor.roi.y * snapshot.height,\n w: monitor.roi.w * snapshot.width,\n h: monitor.roi.h * snapshot.height,\n }\n const { crop, width, height } = await extractCrop(snapshot.data, snapshot.width, snapshot.height, bbox)\n\n // Encode crop\n const { embedding: imageEmb } = await encoder.encode(crop, width, height)\n\n // Find best matching state\n let bestState: string | null = null\n let bestScore = -1\n\n for (const state of monitor.states) {\n let score = 0\n\n if (state.referenceEmbeddings && state.referenceEmbeddings.length > 0) {\n for (const refEmb of state.referenceEmbeddings) {\n const ref = new Float32Array(refEmb)\n const sim = cosineSimilarity(imageEmb, ref)\n score = Math.max(score, sim)\n }\n } else if (state.textPrompts && state.textPrompts.length > 0) {\n for (const prompt of state.textPrompts) {\n const cacheKey = `${monitor.id}:${state.id}:${prompt}`\n let textEmb = this.textCache.get(cacheKey)\n if (!textEmb) {\n const result = await encoder.encodeText(prompt)\n textEmb = result.embedding\n this.textCache.set(cacheKey, textEmb)\n }\n const sim = cosineSimilarity(imageEmb, textEmb)\n score = Math.max(score, sim)\n }\n }\n\n if (score > bestScore) {\n bestScore = score\n bestState = state.label\n }\n }\n\n if (!bestState) return\n\n // Hysteresis\n const key = `${deviceId}:${monitor.id}`\n let ms = this.monitorStates.get(key)\n if (!ms) {\n ms = { currentState: null, pendingState: null, pendingCount: 0, pendingConfidence: 0 }\n this.monitorStates.set(key, ms)\n }\n\n if (bestState === ms.pendingState) {\n ms.pendingCount++\n ms.pendingConfidence = bestScore\n } else {\n ms.pendingState = bestState\n ms.pendingCount = 1\n ms.pendingConfidence = bestScore\n }\n\n if (ms.pendingCount < this.config.hysteresisCount) return\n if (bestState === ms.currentState) return\n\n // State changed!\n const previousState = ms.currentState ?? 'unknown'\n ms.currentState = bestState\n ms.pendingState = null\n ms.pendingCount = 0\n\n const payload: SceneStateChangedEvent = {\n deviceId: Number(deviceId),\n monitorId: monitor.id,\n monitorLabel: monitor.label,\n previousState,\n currentState: bestState,\n confidence: bestScore,\n timestamp: Date.now(),\n }\n\n const data: Record<string, unknown> = { ...payload }\n this.eventBus.emit({\n id: `scene-state-${deviceId}-${monitor.id}-${Date.now()}`,\n category: EventCategory.EnrichmentSceneStateChanged,\n source: { type: 'device', id: deviceId },\n timestamp: new Date(),\n data,\n })\n\n this.logger.info('Scene state changed', {\n tags: { deviceId: Number(deviceId) },\n meta: { monitorLabel: monitor.label, previousState, currentState: bestState, confidence: Number(bestScore.toFixed(2)) },\n })\n }\n}\n\nfunction cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0\n let normA = 0\n let normB = 0\n for (let i = 0; i < a.length; i++) {\n dot += a[i]! * b[i]!\n normA += a[i]! * a[i]!\n normB += b[i]! * b[i]!\n }\n return dot / (Math.sqrt(normA) * Math.sqrt(normB))\n}\n","import { EventCategory, createEvent } from '@camstack/types'\nimport type { IEventBus, IScopedLogger, SettingsStoreClient, TypedSystemEvent } from '@camstack/types'\nimport type { EnrichmentConfig, ActivitySummaryEvent, StateChangeEntry } from '../types.js'\n\n/** Local narrow of a pipeline analysis result item (runtime shape from detection pipeline). */\ninterface PipelineAnalysisItem {\n readonly detection?: {\n readonly class: string\n readonly trackId?: string\n }\n readonly zoneEvents?: readonly { readonly zoneId: string; readonly type: string; readonly timestamp?: number }[]\n}\n\ninterface TrackInfo {\n class: string\n deviceId: string\n firstSeen: number\n lastSeen: number\n zones: Set<string>\n}\n\ninterface ZoneEventInfo {\n type: 'enter' | 'exit'\n zoneId: string\n trackId: string\n timestamp: number\n}\n\ninterface ActivityBuffer {\n tracks: Map<string, TrackInfo>\n zoneEvents: ZoneEventInfo[]\n stateChanges: StateChangeEntry[]\n}\n\nexport interface ActivitySummaryWorkerDeps {\n readonly config: EnrichmentConfig['activitySummary']\n readonly eventBus: IEventBus\n readonly store: SettingsStoreClient\n readonly logger: IScopedLogger\n}\n\nexport class ActivitySummaryWorker {\n private readonly config: EnrichmentConfig['activitySummary']\n private readonly eventBus: IEventBus\n private readonly store: SettingsStoreClient\n private readonly logger: IScopedLogger\n\n private readonly buffers = new Map<string, ActivityBuffer>()\n private summaryTimer: ReturnType<typeof setInterval> | null = null\n private unsubDetection: (() => void) | null = null\n private unsubSceneState: (() => void) | null = null\n private _lastSummary: ActivitySummaryEvent | null = null\n\n constructor(deps: ActivitySummaryWorkerDeps) {\n this.config = deps.config\n this.eventBus = deps.eventBus\n this.store = deps.store\n this.logger = deps.logger\n }\n\n async start(): Promise<void> {\n if (!this.config.enabled) {\n this.logger.info('ActivitySummaryWorker disabled')\n return\n }\n\n this.unsubDetection = this.eventBus.subscribe(\n { category: EventCategory.DetectionResult },\n (event) => { this.handleDetection(event) },\n )\n\n this.unsubSceneState = this.eventBus.subscribe(\n { category: EventCategory.EnrichmentSceneStateChanged },\n (event) => { this.handleSceneStateChange(event) },\n )\n\n this.summaryTimer = setInterval(\n () => { void this.emitSummaries() },\n this.config.intervalSec * 1000,\n )\n\n this.logger.info('ActivitySummaryWorker started', { meta: { intervalSec: this.config.intervalSec } })\n }\n\n async stop(): Promise<void> {\n this.unsubDetection?.()\n this.unsubSceneState?.()\n if (this.summaryTimer) {\n clearInterval(this.summaryTimer)\n this.summaryTimer = null\n }\n // Emit final summaries\n await this.emitSummaries()\n }\n\n get lastSummary(): ActivitySummaryEvent | null { return this._lastSummary }\n\n private getBuffer(deviceId: string): ActivityBuffer {\n let buf = this.buffers.get(deviceId)\n if (!buf) {\n buf = { tracks: new Map(), zoneEvents: [], stateChanges: [] }\n this.buffers.set(deviceId, buf)\n }\n return buf\n }\n\n private handleDetection(event: TypedSystemEvent<'detection.result'>): void {\n const deviceId = event.source.id !== undefined ? String(event.source.id) : ''\n if (!deviceId) return\n\n // analysisResults items are untyped pipeline outputs — narrow to local interface at boundary\n const results = (event.data.analysisResults as readonly PipelineAnalysisItem[]) ?? []\n const buf = this.getBuffer(deviceId)\n const now = Date.now()\n\n for (const det of results) {\n const detection = det.detection\n if (!detection) continue\n\n const trackId = detection.trackId ?? `unknown-${now}`\n const existing = buf.tracks.get(trackId)\n\n if (existing) {\n existing.lastSeen = now\n } else {\n buf.tracks.set(trackId, {\n class: detection.class,\n deviceId,\n firstSeen: now,\n lastSeen: now,\n zones: new Set(),\n })\n }\n\n // Zone events\n const zoneEvents = det.zoneEvents ?? []\n for (const ze of zoneEvents) {\n const track = buf.tracks.get(trackId)\n if (track) track.zones.add(ze.zoneId)\n\n if (ze.type === 'zone-enter' || ze.type === 'zone-exit') {\n buf.zoneEvents.push({\n type: ze.type === 'zone-enter' ? 'enter' : 'exit',\n zoneId: ze.zoneId,\n trackId,\n timestamp: ze.timestamp ?? now,\n })\n }\n }\n }\n }\n\n private handleSceneStateChange(event: TypedSystemEvent<'enrichment.scene.state-changed'>): void {\n const deviceId = event.source.id !== undefined ? String(event.source.id) : ''\n if (!deviceId) return\n\n const data = event.data\n const buf = this.getBuffer(deviceId)\n buf.stateChanges.push({\n monitorId: data.monitorId,\n from: data.previousState,\n to: data.currentState,\n timestamp: data.timestamp,\n })\n }\n\n private async emitSummaries(): Promise<void> {\n const now = Date.now()\n\n for (const [deviceId, buf] of this.buffers) {\n if (buf.tracks.size === 0 && buf.zoneEvents.length === 0 && buf.stateChanges.length === 0) {\n continue\n }\n\n // Object counts by class\n const objectCounts: Record<string, number> = {}\n for (const track of buf.tracks.values()) {\n objectCounts[track.class] = (objectCounts[track.class] ?? 0) + 1\n }\n\n // Zone activity\n const zoneMap = new Map<string, { entries: number; exits: number; dwellTimes: number[] }>()\n for (const ze of buf.zoneEvents) {\n let z = zoneMap.get(ze.zoneId)\n if (!z) {\n z = { entries: 0, exits: 0, dwellTimes: [] }\n zoneMap.set(ze.zoneId, z)\n }\n if (ze.type === 'enter') z.entries++\n else z.exits++\n }\n\n const zoneActivity = [...zoneMap.entries()].map(([zoneId, z]) => ({\n zoneId,\n entries: z.entries,\n exits: z.exits,\n avgDwellMs: z.dwellTimes.length > 0\n ? z.dwellTimes.reduce((a, b) => a + b, 0) / z.dwellTimes.length\n : 0,\n }))\n\n // Activity level\n const totalEvents = buf.tracks.size + buf.zoneEvents.length\n const eventsPerMin = totalEvents / (this.config.intervalSec / 60)\n const activityLevel = eventsPerMin >= this.config.activityThresholds.high ? 'high'\n : eventsPerMin >= this.config.activityThresholds.medium ? 'medium'\n : eventsPerMin >= this.config.activityThresholds.low ? 'low'\n : 'none' as const\n\n const summary: ActivitySummaryEvent = {\n deviceId: Number(deviceId),\n periodStart: now - this.config.intervalSec * 1000,\n periodEnd: now,\n objectCounts,\n zoneActivity,\n stateChanges: [...buf.stateChanges],\n activityLevel,\n }\n\n this._lastSummary = summary\n\n this.eventBus.emit(createEvent(\n EventCategory.EnrichmentActivitySummary,\n { type: 'device', id: deviceId },\n summary,\n ))\n\n // Persist for timeline UI\n try {\n await this.store.insert.mutate({ collection: 'addon-settings', record: {\n id: `${deviceId}:${now}`,\n data: { ...summary },\n } })\n } catch {\n // Best-effort persistence\n }\n\n // Reset buffer\n buf.tracks.clear()\n buf.zoneEvents.length = 0\n buf.stateChanges.length = 0\n }\n }\n}\n","import { BaseAddon, asJsonObject } from '@camstack/types'\nimport type { DecodedFrame, FrameHandle } from '@camstack/types'\nimport { FrameRingReaderCache } from '@camstack/shm-ring'\nimport type { EnrichmentConfig } from './types.js'\nimport { DEFAULT_ENRICHMENT_CONFIG } from './types.js'\nimport { EmbeddingDispatcher } from './workers/embedding-dispatcher.js'\nimport type { EmbeddingDispatcherDeps } from './workers/embedding-dispatcher.js'\nimport { SceneStateWorker } from './workers/scene-state-worker.js'\nimport type { SceneStateWorkerDeps } from './workers/scene-state-worker.js'\nimport { ActivitySummaryWorker } from './workers/activity-summary.js'\n\n/**\n * Extended context shape injected at runtime by the server's capability wiring.\n * Not part of the base AddonContext interface because capabilities are resolved\n * after addon initialization.\n */\n\nexport class EnrichmentEngineAddon extends BaseAddon {\n private embeddingDispatcher: EmbeddingDispatcher | null = null\n private sceneStateWorker: SceneStateWorker | null = null\n private activitySummary: ActivitySummaryWorker | null = null\n /**\n * Shared shm-ring reader cache for downstream frame access. Owned by the\n * engine so the cached segments stay open across worker restarts and close\n * exactly once on shutdown.\n */\n private readers: FrameRingReaderCache | null = null\n private currentFlags = {\n embeddingEnabled: true,\n sceneMonitorEnabled: true,\n activitySummaryEnabled: true,\n }\n constructor() { super({}) }\n\n protected async onInitialize(): Promise<void> {\n const config = await this.loadConfig()\n\n // Encoders will be injected by the server via capability wiring.\n // The server extends AddonContext with a `capabilities` object at runtime.\n // Runtime bridges: capabilities.getCollection/get return unknown because\n // the registry holds heterogeneous providers. The shapes are guaranteed\n // by the capability declaration — see project_cast_elimination_checkpoint.md\n // pattern #7.\n const encoderCollection = this.capabilities?.getCollection?.('embedding-encoder') ?? []\n const streamBrokerRaw = this.capabilities?.get?.('stream-broker')\n\n // Resolve this consumer's cluster node id once. When the runner lives in\n // a sub-broker the localNodeId is hierarchical (`hub/post-analysis`); we\n // strip the suffix so the value matches the `FrameHandle.nodeId` set by\n // the producing decoder (always the cluster-visible parent node).\n const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id\n const ownNodeId = rawNodeId.includes('/') ? rawNodeId.split('/')[0]! : rawNodeId\n\n this.readers = new FrameRingReaderCache(this.ctx.logger.child('shm-readers'))\n\n // Stable arrow held by the engine so it doesn't get re-allocated on every\n // detection event. Routes the cross-node read through the decoder cap with\n // `nodeId: handle.nodeId` per the cap's `nodeIdMode: 'routing'` default.\n // The cap schema infers `data: Uint8Array` (the wire shape); the engine\n // hands the dispatcher a `DecodedFrame` with `Buffer` so downstream\n // `extractCrop` (sharp) gets the Node-native type it expects.\n const getRemoteFrame = async (handle: FrameHandle): Promise<DecodedFrame | null> => {\n const remote = await this.ctx.api.decoder.getFrame.query({ handle, nodeId: handle.nodeId })\n if (!remote) return null\n return {\n data: Buffer.from(remote.data),\n width: remote.width,\n height: remote.height,\n format: remote.format,\n timestamp: remote.timestamp,\n }\n }\n\n this.embeddingDispatcher = new EmbeddingDispatcher({\n config: config.embedding,\n encoders: encoderCollection as EmbeddingDispatcherDeps['encoders'],\n eventBus: this.ctx.eventBus,\n logger: this.ctx.logger.child('EmbeddingDispatcher'),\n ownNodeId,\n readers: this.readers,\n getRemoteFrame,\n })\n\n this.sceneStateWorker = new SceneStateWorker({\n config: config.sceneMonitor,\n encoders: encoderCollection as SceneStateWorkerDeps['encoders'],\n eventBus: this.ctx.eventBus,\n store: this.ctx.api!.settingsStore,\n streamBrokerRegistry: (streamBrokerRaw ?? { getSnapshot: async () => null }) as SceneStateWorkerDeps['streamBrokerRegistry'],\n logger: this.ctx.logger.child('SceneStateWorker'),\n })\n\n this.activitySummary = new ActivitySummaryWorker({\n config: config.activitySummary,\n eventBus: this.ctx.eventBus,\n store: this.ctx.api!.settingsStore,\n logger: this.ctx.logger.child('ActivitySummary'),\n })\n\n this.currentFlags = {\n embeddingEnabled: config.embedding.enabled,\n sceneMonitorEnabled: config.sceneMonitor.enabled,\n activitySummaryEnabled: config.activitySummary.enabled,\n }\n\n await this.embeddingDispatcher.start()\n await this.sceneStateWorker.start()\n await this.activitySummary.start()\n\n this.ctx.logger.info('Enrichment engine initialized with 3 workers')\n }\n\n protected async onShutdown(): Promise<void> {\n await this.embeddingDispatcher?.stop()\n await this.sceneStateWorker?.stop()\n await this.activitySummary?.stop()\n this.embeddingDispatcher = null\n this.sceneStateWorker = null\n this.activitySummary = null\n // Engine owns the shm-readers cache — close exactly here, not inside\n // the dispatcher (the cache is shared, double-close would be a bug).\n this.readers?.close()\n this.readers = null\n }\n\n // ── Three-level settings API (Phase 3) ─────────────────────────────\n //\n // Enrichment engine is post-detection infra. The three boolean toggle\n // flags (embedding/scene/activity) live in `getGlobalSettings` until\n // the dedicated post-detection UI exists. The underlying\n // EnrichmentConfig blob (stored opaquely in 'addon-settings' →\n // 'enrichment:global') is a separate concern — the flags below mirror\n // the `.enabled` field of each worker's config.\n\n protected globalSettingsSchema() {\n return this.schema({\n sections: [\n {\n id: 'enrichment-engine-settings',\n title: 'Enrichment Engine',\n columns: 2,\n fields: [\n {\n type: 'boolean',\n key: 'embeddingEnabled',\n label: 'Embedding Enabled',\n description: 'Compute face/object embeddings from detection crops.',\n default: true,\n },\n {\n type: 'boolean',\n key: 'sceneMonitorEnabled',\n label: 'Scene Monitor Enabled',\n description: 'Run periodic scene state capture for change detection.',\n default: true,\n },\n {\n type: 'boolean',\n key: 'activitySummaryEnabled',\n label: 'Activity Summary Enabled',\n description: 'Aggregate hourly activity summaries per camera.',\n default: true,\n },\n ],\n },\n ],\n })\n }\n\n async updateGlobalSettings(patch: Record<string, unknown>): Promise<void> {\n await this.ctx?.settings?.writeAddonStore(patch)\n const prev = this.currentFlags\n const next = {\n embeddingEnabled: typeof patch['embeddingEnabled'] === 'boolean' ? patch['embeddingEnabled'] : prev.embeddingEnabled,\n sceneMonitorEnabled: typeof patch['sceneMonitorEnabled'] === 'boolean' ? patch['sceneMonitorEnabled'] : prev.sceneMonitorEnabled,\n activitySummaryEnabled: typeof patch['activitySummaryEnabled'] === 'boolean' ? patch['activitySummaryEnabled'] : prev.activitySummaryEnabled,\n }\n this.currentFlags = next\n\n // Toggle workers based on flag changes\n if (prev.embeddingEnabled && !next.embeddingEnabled) {\n this.ctx?.logger.info('Stopping embedding dispatcher (disabled via config)')\n await this.embeddingDispatcher?.stop()\n } else if (!prev.embeddingEnabled && next.embeddingEnabled) {\n this.ctx?.logger.info('Starting embedding dispatcher (enabled via config)')\n await this.embeddingDispatcher?.start()\n }\n\n if (prev.sceneMonitorEnabled && !next.sceneMonitorEnabled) {\n this.ctx?.logger.info('Stopping scene state worker (disabled via config)')\n await this.sceneStateWorker?.stop()\n } else if (!prev.sceneMonitorEnabled && next.sceneMonitorEnabled) {\n this.ctx?.logger.info('Starting scene state worker (enabled via config)')\n await this.sceneStateWorker?.start()\n }\n\n if (prev.activitySummaryEnabled && !next.activitySummaryEnabled) {\n this.ctx?.logger.info('Stopping activity summary worker (disabled via config)')\n await this.activitySummary?.stop()\n } else if (!prev.activitySummaryEnabled && next.activitySummaryEnabled) {\n this.ctx?.logger.info('Starting activity summary worker (enabled via config)')\n await this.activitySummary?.start()\n }\n\n this.ctx?.logger.info('Enrichment engine flags updated', { meta: { flags: this.currentFlags } })\n }\n\n private async loadConfig(): Promise<EnrichmentConfig> {\n try {\n const stored = asJsonObject(await this.ctx.api?.settingsStore.get.query({ collection: 'addon-settings', key: 'enrichment:global' }))\n if (stored) {\n // Shallow structural merge: only keys from DEFAULT_ENRICHMENT_CONFIG\n // are preserved; stored values override their counterparts when\n // present with a matching type.\n const merged: EnrichmentConfig = { ...DEFAULT_ENRICHMENT_CONFIG, ...stored }\n return merged\n }\n } catch {\n // First boot — use defaults\n }\n return DEFAULT_ENRICHMENT_CONFIG\n }\n}\n\n// Default export — kernel addon-loader prefers mod.default to identify the addon class\nexport default EnrichmentEngineAddon\n"],"names":["EventCategory","createEvent","z.object","z.number","z.tuple","z.string","z.array","z.boolean","z.enum","BaseAddon","FrameRingReaderCache","asJsonObject"],"mappings":"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"}
@@ -1,4 +1,5 @@
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-tm6O4bWa.mjs";
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-CGAj-pkn.mjs";
2
+ import { FrameRingReaderCache } from "@camstack/shm-ring";
2
3
  import sharp from "sharp";
3
4
  const DEFAULT_ENRICHMENT_CONFIG = {
4
5
  enabled: false,
@@ -44,11 +45,20 @@ async function extractCrop(frameData, frameWidth, frameHeight, bbox) {
44
45
  }).extract({ left, top, width, height }).jpeg({ quality: 90 }).toBuffer();
45
46
  return { crop, width, height };
46
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
+ }
47
54
  class EmbeddingDispatcher {
48
55
  config;
49
56
  encoders;
50
57
  eventBus;
51
58
  logger;
59
+ ownNodeId;
60
+ readers;
61
+ getRemoteFrame;
52
62
  lastEmbedTime = /* @__PURE__ */ new Map();
53
63
  pendingCrops = /* @__PURE__ */ new Map();
54
64
  flushTimer = null;
@@ -61,6 +71,9 @@ class EmbeddingDispatcher {
61
71
  this.encoders = deps.encoders;
62
72
  this.eventBus = deps.eventBus;
63
73
  this.logger = deps.logger;
74
+ this.ownNodeId = deps.ownNodeId;
75
+ this.readers = deps.readers;
76
+ this.getRemoteFrame = deps.getRemoteFrame;
64
77
  }
65
78
  async start() {
66
79
  if (!this.config.enabled) {
@@ -103,7 +116,41 @@ class EmbeddingDispatcher {
103
116
  const deviceId = event.source.id !== void 0 ? String(event.source.id) : "";
104
117
  if (!deviceId) return;
105
118
  const detections = data.analysisResults ?? [];
106
- const pipelineResult = data.pipelineResult;
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;
107
154
  for (const det of detections) {
108
155
  const detection = det.detection;
109
156
  if (!detection) continue;
@@ -114,10 +161,6 @@ class EmbeddingDispatcher {
114
161
  const lastTime = this.lastEmbedTime.get(deviceId) ?? 0;
115
162
  if (now - lastTime < minInterval) continue;
116
163
  const trackId = detection.trackId ?? `${deviceId}-${now}`;
117
- const frameData = pipelineResult.frameData;
118
- const frameWidth = pipelineResult.imageWidth;
119
- const frameHeight = pipelineResult.imageHeight;
120
- if (!frameData || !frameWidth || !frameHeight) continue;
121
164
  const pending = {
122
165
  trackId,
123
166
  deviceId,
@@ -630,6 +673,12 @@ class EnrichmentEngineAddon extends BaseAddon {
630
673
  embeddingDispatcher = null;
631
674
  sceneStateWorker = null;
632
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;
633
682
  currentFlags = {
634
683
  embeddingEnabled: true,
635
684
  sceneMonitorEnabled: true,
@@ -642,11 +691,28 @@ class EnrichmentEngineAddon extends BaseAddon {
642
691
  const config = await this.loadConfig();
643
692
  const encoderCollection = this.capabilities?.getCollection?.("embedding-encoder") ?? [];
644
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
+ };
645
708
  this.embeddingDispatcher = new EmbeddingDispatcher({
646
709
  config: config.embedding,
647
710
  encoders: encoderCollection,
648
711
  eventBus: this.ctx.eventBus,
649
- logger: this.ctx.logger.child("EmbeddingDispatcher")
712
+ logger: this.ctx.logger.child("EmbeddingDispatcher"),
713
+ ownNodeId,
714
+ readers: this.readers,
715
+ getRemoteFrame
650
716
  });
651
717
  this.sceneStateWorker = new SceneStateWorker({
652
718
  config: config.sceneMonitor,
@@ -679,6 +745,8 @@ class EnrichmentEngineAddon extends BaseAddon {
679
745
  this.embeddingDispatcher = null;
680
746
  this.sceneStateWorker = null;
681
747
  this.activitySummary = null;
748
+ this.readers?.close();
749
+ this.readers = null;
682
750
  }
683
751
  // ── Three-level settings API (Phase 3) ─────────────────────────────
684
752
  //