@camstack/lib-pipeline-analysis 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,1544 +1,1279 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
- }) : x)(function(x) {
10
- if (typeof require !== "undefined") return require.apply(this, arguments);
11
- throw Error('Dynamic require of "' + x + '" is not supported');
12
- });
13
- var __commonJS = (cb, mod) => function __require2() {
14
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
- };
16
- var __export = (target, all) => {
17
- for (var name in all)
18
- __defProp(target, name, { get: all[name], enumerable: true });
19
- };
20
- var __copyProps = (to, from, except, desc) => {
21
- if (from && typeof from === "object" || typeof from === "function") {
22
- for (let key of __getOwnPropNames(from))
23
- if (!__hasOwnProp.call(to, key) && key !== except)
24
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
1
+ // src/zones/geometry.ts
2
+ function pointInPolygon(point, polygon) {
3
+ if (polygon.length < 3) return false;
4
+ let inside = false;
5
+ const { x, y } = point;
6
+ const n = polygon.length;
7
+ for (let i = 0, j = n - 1; i < n; j = i++) {
8
+ const xi = polygon[i].x;
9
+ const yi = polygon[i].y;
10
+ const xj = polygon[j].x;
11
+ const yj = polygon[j].y;
12
+ const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
13
+ if (intersect) inside = !inside;
25
14
  }
26
- return to;
27
- };
28
- var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
29
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
- // If the importer is in node compatibility mode or this is not an ESM
31
- // file that has been converted to a CommonJS file using a Babel-
32
- // compatible transform (i.e. "__esModule" has not been set), then set
33
- // "default" to the CommonJS "module.exports" for node compatibility.
34
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
- mod
36
- ));
15
+ return inside;
16
+ }
17
+ function lineIntersection(a, b) {
18
+ const dx1 = a.p2.x - a.p1.x;
19
+ const dy1 = a.p2.y - a.p1.y;
20
+ const dx2 = b.p2.x - b.p1.x;
21
+ const dy2 = b.p2.y - b.p1.y;
22
+ const denom = dx1 * dy2 - dy1 * dx2;
23
+ if (Math.abs(denom) < 1e-10) return null;
24
+ const dx3 = b.p1.x - a.p1.x;
25
+ const dy3 = b.p1.y - a.p1.y;
26
+ const t = (dx3 * dy2 - dy3 * dx2) / denom;
27
+ const u = (dx3 * dy1 - dy3 * dx1) / denom;
28
+ if (t < 0 || t > 1 || u < 0 || u > 1) return null;
29
+ return {
30
+ x: a.p1.x + t * dx1,
31
+ y: a.p1.y + t * dy1
32
+ };
33
+ }
34
+ function tripwireCrossing(prev, curr, tripwire) {
35
+ const movement = { p1: prev, p2: curr };
36
+ const intersection = lineIntersection(movement, tripwire);
37
+ if (intersection === null) return null;
38
+ const twDx = tripwire.p2.x - tripwire.p1.x;
39
+ const twDy = tripwire.p2.y - tripwire.p1.y;
40
+ const movDx = curr.x - prev.x;
41
+ const movDy = curr.y - prev.y;
42
+ const cross = twDx * movDy - twDy * movDx;
43
+ const direction = cross > 0 ? "left" : "right";
44
+ return { crossed: true, direction };
45
+ }
46
+ function bboxCentroid(bbox) {
47
+ return {
48
+ x: bbox.x + bbox.w / 2,
49
+ y: bbox.y + bbox.h / 2
50
+ };
51
+ }
52
+ function normalizeToPixel(point, imageWidth, imageHeight) {
53
+ return {
54
+ x: point.x * imageWidth,
55
+ y: point.y * imageHeight
56
+ };
57
+ }
37
58
 
38
- // src/zones/geometry.js
39
- var require_geometry = __commonJS({
40
- "src/zones/geometry.js"(exports) {
41
- "use strict";
42
- Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.pointInPolygon = pointInPolygon;
44
- exports.lineIntersection = lineIntersection;
45
- exports.tripwireCrossing = tripwireCrossing;
46
- exports.bboxCentroid = bboxCentroid;
47
- exports.normalizeToPixel = normalizeToPixel;
48
- function pointInPolygon(point, polygon) {
49
- if (polygon.length < 3)
50
- return false;
51
- let inside = false;
52
- const { x, y } = point;
53
- const n = polygon.length;
54
- for (let i = 0, j = n - 1; i < n; j = i++) {
55
- const xi = polygon[i].x;
56
- const yi = polygon[i].y;
57
- const xj = polygon[j].x;
58
- const yj = polygon[j].y;
59
- const intersect = yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
60
- if (intersect)
61
- inside = !inside;
59
+ // src/tracker/hungarian.ts
60
+ function greedyAssignment(costMatrix, threshold) {
61
+ const numTracks = costMatrix.length;
62
+ const numDetections = numTracks > 0 ? costMatrix[0]?.length ?? 0 : 0;
63
+ const candidates = [];
64
+ for (let t = 0; t < numTracks; t++) {
65
+ for (let d = 0; d < numDetections; d++) {
66
+ const iou2 = costMatrix[t][d] ?? 0;
67
+ if (iou2 >= threshold) {
68
+ candidates.push({ iou: iou2, trackIdx: t, detIdx: d });
62
69
  }
63
- return inside;
64
- }
65
- function lineIntersection(a, b) {
66
- const dx1 = a.p2.x - a.p1.x;
67
- const dy1 = a.p2.y - a.p1.y;
68
- const dx2 = b.p2.x - b.p1.x;
69
- const dy2 = b.p2.y - b.p1.y;
70
- const denom = dx1 * dy2 - dy1 * dx2;
71
- if (Math.abs(denom) < 1e-10)
72
- return null;
73
- const dx3 = b.p1.x - a.p1.x;
74
- const dy3 = b.p1.y - a.p1.y;
75
- const t = (dx3 * dy2 - dy3 * dx2) / denom;
76
- const u = (dx3 * dy1 - dy3 * dx1) / denom;
77
- if (t < 0 || t > 1 || u < 0 || u > 1)
78
- return null;
79
- return {
80
- x: a.p1.x + t * dx1,
81
- y: a.p1.y + t * dy1
82
- };
83
- }
84
- function tripwireCrossing(prev, curr, tripwire) {
85
- const movement = { p1: prev, p2: curr };
86
- const intersection = lineIntersection(movement, tripwire);
87
- if (intersection === null)
88
- return null;
89
- const twDx = tripwire.p2.x - tripwire.p1.x;
90
- const twDy = tripwire.p2.y - tripwire.p1.y;
91
- const movDx = curr.x - prev.x;
92
- const movDy = curr.y - prev.y;
93
- const cross = twDx * movDy - twDy * movDx;
94
- const direction = cross > 0 ? "left" : "right";
95
- return { crossed: true, direction };
96
- }
97
- function bboxCentroid(bbox) {
98
- return {
99
- x: bbox.x + bbox.w / 2,
100
- y: bbox.y + bbox.h / 2
101
- };
102
- }
103
- function normalizeToPixel(point, imageWidth, imageHeight) {
104
- return {
105
- x: point.x * imageWidth,
106
- y: point.y * imageHeight
107
- };
108
70
  }
109
71
  }
110
- });
72
+ candidates.sort((a, b) => b.iou - a.iou);
73
+ const assignedTracks = /* @__PURE__ */ new Set();
74
+ const assignedDets = /* @__PURE__ */ new Set();
75
+ const matches = [];
76
+ for (const { trackIdx, detIdx } of candidates) {
77
+ if (assignedTracks.has(trackIdx) || assignedDets.has(detIdx)) continue;
78
+ matches.push([trackIdx, detIdx]);
79
+ assignedTracks.add(trackIdx);
80
+ assignedDets.add(detIdx);
81
+ }
82
+ const unmatchedTracks = [];
83
+ for (let t = 0; t < numTracks; t++) {
84
+ if (!assignedTracks.has(t)) unmatchedTracks.push(t);
85
+ }
86
+ const unmatchedDetections = [];
87
+ for (let d = 0; d < numDetections; d++) {
88
+ if (!assignedDets.has(d)) unmatchedDetections.push(d);
89
+ }
90
+ return { matches, unmatchedTracks, unmatchedDetections };
91
+ }
111
92
 
112
- // src/tracker/hungarian.js
113
- var require_hungarian = __commonJS({
114
- "src/tracker/hungarian.js"(exports) {
115
- "use strict";
116
- Object.defineProperty(exports, "__esModule", { value: true });
117
- exports.greedyAssignment = greedyAssignment2;
118
- function greedyAssignment2(costMatrix, threshold) {
119
- const numTracks = costMatrix.length;
120
- const numDetections = numTracks > 0 ? costMatrix[0]?.length ?? 0 : 0;
121
- const candidates = [];
122
- for (let t = 0; t < numTracks; t++) {
123
- for (let d = 0; d < numDetections; d++) {
124
- const iou = costMatrix[t][d] ?? 0;
125
- if (iou >= threshold) {
126
- candidates.push({ iou, trackIdx: t, detIdx: d });
127
- }
128
- }
129
- }
130
- candidates.sort((a, b) => b.iou - a.iou);
131
- const assignedTracks = /* @__PURE__ */ new Set();
132
- const assignedDets = /* @__PURE__ */ new Set();
133
- const matches = [];
134
- for (const { trackIdx, detIdx } of candidates) {
135
- if (assignedTracks.has(trackIdx) || assignedDets.has(detIdx))
136
- continue;
137
- matches.push([trackIdx, detIdx]);
138
- assignedTracks.add(trackIdx);
139
- assignedDets.add(detIdx);
140
- }
141
- const unmatchedTracks = [];
142
- for (let t = 0; t < numTracks; t++) {
143
- if (!assignedTracks.has(t))
144
- unmatchedTracks.push(t);
145
- }
146
- const unmatchedDetections = [];
147
- for (let d = 0; d < numDetections; d++) {
148
- if (!assignedDets.has(d))
149
- unmatchedDetections.push(d);
93
+ // src/tracker/sort-tracker.ts
94
+ var MAX_PATH_LENGTH = 300;
95
+ function iou(a, b) {
96
+ const ax1 = a.x;
97
+ const ay1 = a.y;
98
+ const ax2 = a.x + a.w;
99
+ const ay2 = a.y + a.h;
100
+ const bx1 = b.x;
101
+ const by1 = b.y;
102
+ const bx2 = b.x + b.w;
103
+ const by2 = b.y + b.h;
104
+ const interX1 = Math.max(ax1, bx1);
105
+ const interY1 = Math.max(ay1, by1);
106
+ const interX2 = Math.min(ax2, bx2);
107
+ const interY2 = Math.min(ay2, by2);
108
+ const interW = Math.max(0, interX2 - interX1);
109
+ const interH = Math.max(0, interY2 - interY1);
110
+ const interArea = interW * interH;
111
+ if (interArea === 0) return 0;
112
+ const aArea = a.w * a.h;
113
+ const bArea = b.w * b.h;
114
+ const unionArea = aArea + bArea - interArea;
115
+ return unionArea <= 0 ? 0 : interArea / unionArea;
116
+ }
117
+ function trackToTrackedDetection(track) {
118
+ return {
119
+ class: track.class,
120
+ originalClass: track.originalClass,
121
+ score: track.score,
122
+ bbox: track.bbox,
123
+ landmarks: track.landmarks,
124
+ trackId: track.id,
125
+ trackAge: track.hits,
126
+ velocity: track.velocity,
127
+ path: track.path.slice()
128
+ };
129
+ }
130
+ function pushToRingBuffer(buffer, item, maxLength) {
131
+ if (buffer.length >= maxLength) {
132
+ buffer.shift();
133
+ }
134
+ buffer.push(item);
135
+ }
136
+ var DEFAULT_TRACKER_CONFIG = {
137
+ maxAge: 30,
138
+ minHits: 3,
139
+ iouThreshold: 0.3
140
+ };
141
+ var SortTracker = class {
142
+ tracks = [];
143
+ lostTracks = [];
144
+ nextTrackId = 1;
145
+ config;
146
+ constructor(config = {}) {
147
+ this.config = { ...DEFAULT_TRACKER_CONFIG, ...config };
148
+ }
149
+ update(detections, frameTimestamp) {
150
+ this.lostTracks = [];
151
+ if (this.tracks.length === 0) {
152
+ for (const det of detections) {
153
+ this.createTrack(det, frameTimestamp);
150
154
  }
151
- return { matches, unmatchedTracks, unmatchedDetections };
155
+ return this.getConfirmedTracks();
152
156
  }
153
- }
154
- });
155
-
156
- // src/tracker/sort-tracker.js
157
- var require_sort_tracker = __commonJS({
158
- "src/tracker/sort-tracker.js"(exports) {
159
- "use strict";
160
- Object.defineProperty(exports, "__esModule", { value: true });
161
- exports.SortTracker = exports.DEFAULT_TRACKER_CONFIG = void 0;
162
- var hungarian_js_1 = require_hungarian();
163
- var MAX_PATH_LENGTH = 300;
164
- function iou(a, b) {
165
- const ax1 = a.x;
166
- const ay1 = a.y;
167
- const ax2 = a.x + a.w;
168
- const ay2 = a.y + a.h;
169
- const bx1 = b.x;
170
- const by1 = b.y;
171
- const bx2 = b.x + b.w;
172
- const by2 = b.y + b.h;
173
- const interX1 = Math.max(ax1, bx1);
174
- const interY1 = Math.max(ay1, by1);
175
- const interX2 = Math.min(ax2, bx2);
176
- const interY2 = Math.min(ay2, by2);
177
- const interW = Math.max(0, interX2 - interX1);
178
- const interH = Math.max(0, interY2 - interY1);
179
- const interArea = interW * interH;
180
- if (interArea === 0)
181
- return 0;
182
- const aArea = a.w * a.h;
183
- const bArea = b.w * b.h;
184
- const unionArea = aArea + bArea - interArea;
185
- return unionArea <= 0 ? 0 : interArea / unionArea;
157
+ if (detections.length === 0) {
158
+ this.ageTracksAndPruneLost(frameTimestamp);
159
+ return this.getConfirmedTracks();
186
160
  }
187
- function trackToTrackedDetection(track) {
188
- return {
189
- class: track.class,
190
- originalClass: track.originalClass,
191
- score: track.score,
192
- bbox: track.bbox,
193
- landmarks: track.landmarks,
194
- trackId: track.id,
195
- trackAge: track.hits,
196
- velocity: track.velocity,
197
- path: track.path.slice()
198
- };
161
+ const costMatrix = this.tracks.map(
162
+ (track) => detections.map((det) => iou(track.bbox, det.bbox))
163
+ );
164
+ const { matches, unmatchedTracks, unmatchedDetections } = greedyAssignment(
165
+ costMatrix,
166
+ this.config.iouThreshold
167
+ );
168
+ for (const [trackIdx, detIdx] of matches) {
169
+ const track = this.tracks[trackIdx];
170
+ const det = detections[detIdx];
171
+ const prevCx = track.bbox.x + track.bbox.w / 2;
172
+ const prevCy = track.bbox.y + track.bbox.h / 2;
173
+ const newCx = det.bbox.x + det.bbox.w / 2;
174
+ const newCy = det.bbox.y + det.bbox.h / 2;
175
+ pushToRingBuffer(track.path, track.bbox, MAX_PATH_LENGTH);
176
+ track.bbox = det.bbox;
177
+ track.class = det.class;
178
+ track.originalClass = det.originalClass;
179
+ track.score = det.score;
180
+ track.landmarks = det.landmarks;
181
+ track.age = 0;
182
+ track.hits++;
183
+ track.lastSeen = frameTimestamp;
184
+ track.velocity = { dx: newCx - prevCx, dy: newCy - prevCy };
199
185
  }
200
- function pushToRingBuffer(buffer, item, maxLength) {
201
- if (buffer.length >= maxLength) {
202
- buffer.shift();
203
- }
204
- buffer.push(item);
186
+ for (const trackIdx of unmatchedTracks) {
187
+ const track = this.tracks[trackIdx];
188
+ track.age++;
205
189
  }
206
- exports.DEFAULT_TRACKER_CONFIG = {
207
- maxAge: 30,
208
- minHits: 3,
209
- iouThreshold: 0.3
210
- };
211
- var SortTracker2 = class {
212
- tracks = [];
213
- lostTracks = [];
214
- nextTrackId = 1;
215
- config;
216
- constructor(config = {}) {
217
- this.config = { ...exports.DEFAULT_TRACKER_CONFIG, ...config };
218
- }
219
- update(detections, frameTimestamp) {
220
- this.lostTracks = [];
221
- if (this.tracks.length === 0) {
222
- for (const det of detections) {
223
- this.createTrack(det, frameTimestamp);
224
- }
225
- return this.getConfirmedTracks();
226
- }
227
- if (detections.length === 0) {
228
- this.ageTracksAndPruneLost(frameTimestamp);
229
- return this.getConfirmedTracks();
230
- }
231
- const costMatrix = this.tracks.map((track) => detections.map((det) => iou(track.bbox, det.bbox)));
232
- const { matches, unmatchedTracks, unmatchedDetections } = (0, hungarian_js_1.greedyAssignment)(costMatrix, this.config.iouThreshold);
233
- for (const [trackIdx, detIdx] of matches) {
234
- const track = this.tracks[trackIdx];
235
- const det = detections[detIdx];
236
- const prevCx = track.bbox.x + track.bbox.w / 2;
237
- const prevCy = track.bbox.y + track.bbox.h / 2;
238
- const newCx = det.bbox.x + det.bbox.w / 2;
239
- const newCy = det.bbox.y + det.bbox.h / 2;
240
- pushToRingBuffer(track.path, track.bbox, MAX_PATH_LENGTH);
241
- track.bbox = det.bbox;
242
- track.class = det.class;
243
- track.originalClass = det.originalClass;
244
- track.score = det.score;
245
- track.landmarks = det.landmarks;
246
- track.age = 0;
247
- track.hits++;
248
- track.lastSeen = frameTimestamp;
249
- track.velocity = { dx: newCx - prevCx, dy: newCy - prevCy };
250
- }
251
- for (const trackIdx of unmatchedTracks) {
252
- const track = this.tracks[trackIdx];
253
- track.age++;
254
- }
255
- const survived = [];
256
- for (const track of this.tracks) {
257
- if (track.age > this.config.maxAge) {
258
- track.lost = true;
259
- this.lostTracks.push(track);
260
- } else {
261
- survived.push(track);
262
- }
263
- }
264
- this.tracks = survived;
265
- for (const detIdx of unmatchedDetections) {
266
- const det = detections[detIdx];
267
- this.createTrack(det, frameTimestamp);
268
- }
269
- return this.getConfirmedTracks();
270
- }
271
- getActiveTracks() {
272
- return this.tracks.map(trackToTrackedDetection);
273
- }
274
- getLostTracks() {
275
- return this.lostTracks.map(trackToTrackedDetection);
276
- }
277
- reset() {
278
- this.tracks = [];
279
- this.lostTracks = [];
280
- this.nextTrackId = 1;
190
+ const survived = [];
191
+ for (const track of this.tracks) {
192
+ if (track.age > this.config.maxAge) {
193
+ track.lost = true;
194
+ this.lostTracks.push(track);
195
+ } else {
196
+ survived.push(track);
281
197
  }
282
- createTrack(det, timestamp) {
283
- const track = {
284
- id: String(this.nextTrackId++),
285
- bbox: det.bbox,
286
- class: det.class,
287
- originalClass: det.originalClass,
288
- score: det.score,
289
- landmarks: det.landmarks,
290
- age: 0,
291
- hits: 1,
292
- path: [],
293
- firstSeen: timestamp,
294
- lastSeen: timestamp,
295
- velocity: { dx: 0, dy: 0 },
296
- lost: false
297
- };
298
- this.tracks.push(track);
299
- return track;
300
- }
301
- getConfirmedTracks() {
302
- return this.tracks.filter((t) => t.hits >= this.config.minHits).map(trackToTrackedDetection);
303
- }
304
- ageTracksAndPruneLost(frameTimestamp) {
305
- const survived = [];
306
- for (const track of this.tracks) {
307
- track.age++;
308
- if (track.age > this.config.maxAge) {
309
- track.lost = true;
310
- this.lostTracks.push(track);
311
- } else {
312
- survived.push(track);
313
- }
314
- }
315
- this.tracks = survived;
316
- void frameTimestamp;
317
- }
318
- };
319
- exports.SortTracker = SortTracker2;
198
+ }
199
+ this.tracks = survived;
200
+ for (const detIdx of unmatchedDetections) {
201
+ const det = detections[detIdx];
202
+ this.createTrack(det, frameTimestamp);
203
+ }
204
+ return this.getConfirmedTracks();
320
205
  }
321
- });
322
-
323
- // src/state/state-analyzer.js
324
- var require_state_analyzer = __commonJS({
325
- "src/state/state-analyzer.js"(exports) {
326
- "use strict";
327
- Object.defineProperty(exports, "__esModule", { value: true });
328
- exports.StateAnalyzer = exports.DEFAULT_STATE_ANALYZER_CONFIG = void 0;
329
- exports.DEFAULT_STATE_ANALYZER_CONFIG = {
330
- stationaryThresholdSec: 10,
331
- loiteringThresholdSec: 60,
332
- velocityThreshold: 2,
333
- enteringFrames: 5
206
+ getActiveTracks() {
207
+ return this.tracks.map(trackToTrackedDetection);
208
+ }
209
+ getLostTracks() {
210
+ return this.lostTracks.map(trackToTrackedDetection);
211
+ }
212
+ reset() {
213
+ this.tracks = [];
214
+ this.lostTracks = [];
215
+ this.nextTrackId = 1;
216
+ }
217
+ createTrack(det, timestamp) {
218
+ const track = {
219
+ id: String(this.nextTrackId++),
220
+ bbox: det.bbox,
221
+ class: det.class,
222
+ originalClass: det.originalClass,
223
+ score: det.score,
224
+ landmarks: det.landmarks,
225
+ age: 0,
226
+ hits: 1,
227
+ path: [],
228
+ firstSeen: timestamp,
229
+ lastSeen: timestamp,
230
+ velocity: { dx: 0, dy: 0 },
231
+ lost: false
334
232
  };
335
- function magnitude(v) {
336
- return Math.sqrt(v.dx * v.dx + v.dy * v.dy);
337
- }
338
- var StateAnalyzer2 = class {
339
- states = /* @__PURE__ */ new Map();
340
- config;
341
- constructor(config = {}) {
342
- this.config = { ...exports.DEFAULT_STATE_ANALYZER_CONFIG, ...config };
343
- }
344
- analyze(tracks, timestamp) {
345
- const results = [];
346
- const activeIds = /* @__PURE__ */ new Set();
347
- for (const track of tracks) {
348
- activeIds.add(track.trackId);
349
- const prev = this.states.get(track.trackId);
350
- const centroid = {
351
- x: track.bbox.x + track.bbox.w / 2,
352
- y: track.bbox.y + track.bbox.h / 2
353
- };
354
- const speed = track.velocity ? magnitude(track.velocity) : 0;
355
- let enteredAt;
356
- let stationarySince;
357
- let totalDistancePx;
358
- if (!prev) {
359
- enteredAt = timestamp;
360
- stationarySince = speed <= this.config.velocityThreshold ? timestamp : void 0;
361
- totalDistancePx = 0;
362
- } else {
363
- enteredAt = prev.enteredAt;
364
- const dx = centroid.x - prev.lastPosition.x;
365
- const dy = centroid.y - prev.lastPosition.y;
366
- const dist = Math.sqrt(dx * dx + dy * dy);
367
- totalDistancePx = prev.totalDistancePx + dist;
368
- if (speed > this.config.velocityThreshold) {
369
- stationarySince = void 0;
370
- } else {
371
- stationarySince = prev.stationarySince ?? timestamp;
372
- }
373
- }
374
- const newInternal = {
375
- enteredAt,
376
- stationarySince,
377
- totalDistancePx,
378
- lastPosition: centroid
379
- };
380
- this.states.set(track.trackId, newInternal);
381
- const dwellTimeMs = timestamp - enteredAt;
382
- const state = this.computeObjectState(track.trackAge, speed, stationarySince, timestamp);
383
- results.push({
384
- trackId: track.trackId,
385
- state,
386
- stationarySince,
387
- enteredAt,
388
- totalDistancePx,
389
- dwellTimeMs
390
- });
391
- }
392
- for (const [id, internalState] of this.states) {
393
- if (!activeIds.has(id)) {
394
- results.push({
395
- trackId: id,
396
- state: "leaving",
397
- stationarySince: internalState.stationarySince,
398
- enteredAt: internalState.enteredAt,
399
- totalDistancePx: internalState.totalDistancePx,
400
- dwellTimeMs: timestamp - internalState.enteredAt
401
- });
402
- this.states.delete(id);
403
- }
404
- }
405
- return results;
406
- }
407
- computeObjectState(trackAge, speed, stationarySince, timestamp) {
408
- if (trackAge <= this.config.enteringFrames) {
409
- return "entering";
410
- }
411
- if (stationarySince !== void 0) {
412
- const stationaryDurationSec = (timestamp - stationarySince) / 1e3;
413
- if (stationaryDurationSec >= this.config.loiteringThresholdSec) {
414
- return "loitering";
415
- }
416
- if (stationaryDurationSec >= this.config.stationaryThresholdSec) {
417
- return "stationary";
418
- }
419
- }
420
- if (speed > this.config.velocityThreshold) {
421
- return "moving";
422
- }
423
- return "moving";
233
+ this.tracks.push(track);
234
+ return track;
235
+ }
236
+ getConfirmedTracks() {
237
+ return this.tracks.filter((t) => t.hits >= this.config.minHits).map(trackToTrackedDetection);
238
+ }
239
+ ageTracksAndPruneLost(frameTimestamp) {
240
+ const survived = [];
241
+ for (const track of this.tracks) {
242
+ track.age++;
243
+ if (track.age > this.config.maxAge) {
244
+ track.lost = true;
245
+ this.lostTracks.push(track);
246
+ } else {
247
+ survived.push(track);
424
248
  }
425
- };
426
- exports.StateAnalyzer = StateAnalyzer2;
249
+ }
250
+ this.tracks = survived;
251
+ void frameTimestamp;
427
252
  }
428
- });
253
+ };
429
254
 
430
- // src/zones/zone-evaluator.js
431
- var require_zone_evaluator = __commonJS({
432
- "src/zones/zone-evaluator.js"(exports) {
433
- "use strict";
434
- Object.defineProperty(exports, "__esModule", { value: true });
435
- exports.ZoneEvaluator = void 0;
436
- var geometry_js_1 = require_geometry();
437
- var ZoneEvaluator2 = class {
438
- /** Track which zones each track was in last frame */
439
- trackZoneState = /* @__PURE__ */ new Map();
440
- evaluate(tracks, zones, imageWidth, imageHeight, timestamp) {
441
- const events = [];
442
- const activeTrackIds = /* @__PURE__ */ new Set();
443
- for (const track of tracks) {
444
- activeTrackIds.add(track.trackId);
445
- const centroid = (0, geometry_js_1.bboxCentroid)(track.bbox);
446
- const prevZones = this.trackZoneState.get(track.trackId) ?? /* @__PURE__ */ new Set();
447
- const currZones = /* @__PURE__ */ new Set();
448
- for (const zone of zones) {
449
- if (zone.alertOnClasses && zone.alertOnClasses.length > 0) {
450
- if (!zone.alertOnClasses.includes(track.class))
451
- continue;
452
- }
453
- if (zone.type === "polygon") {
454
- const pixelPoints = zone.points.map((p) => (0, geometry_js_1.normalizeToPixel)(p, imageWidth, imageHeight));
455
- const inside = (0, geometry_js_1.pointInPolygon)(centroid, pixelPoints);
456
- if (inside) {
457
- currZones.add(zone.id);
458
- }
459
- if (inside && !prevZones.has(zone.id)) {
460
- events.push({
461
- type: "zone-enter",
462
- zoneId: zone.id,
463
- zoneName: zone.name,
464
- trackId: track.trackId,
465
- detection: track,
466
- timestamp
467
- });
468
- }
469
- if (!inside && prevZones.has(zone.id)) {
470
- events.push({
471
- type: "zone-exit",
472
- zoneId: zone.id,
473
- zoneName: zone.name,
474
- trackId: track.trackId,
475
- detection: track,
476
- timestamp
477
- });
478
- }
479
- } else if (zone.type === "tripwire" && track.path.length >= 1) {
480
- const prevBbox = track.path[track.path.length - 1];
481
- if (!prevBbox)
482
- continue;
483
- const prevCentroid = (0, geometry_js_1.bboxCentroid)(prevBbox);
484
- const p0 = zone.points[0];
485
- const p1 = zone.points[1];
486
- if (!p0 || !p1)
487
- continue;
488
- const tripwireLine = {
489
- p1: (0, geometry_js_1.normalizeToPixel)(p0, imageWidth, imageHeight),
490
- p2: (0, geometry_js_1.normalizeToPixel)(p1, imageWidth, imageHeight)
491
- };
492
- const cross = (0, geometry_js_1.tripwireCrossing)(prevCentroid, centroid, tripwireLine);
493
- if (cross?.crossed) {
494
- events.push({
495
- type: "tripwire-cross",
496
- zoneId: zone.id,
497
- zoneName: zone.name,
498
- trackId: track.trackId,
499
- detection: track,
500
- direction: cross.direction,
501
- timestamp
502
- });
503
- }
504
- }
505
- }
506
- this.trackZoneState.set(track.trackId, currZones);
507
- }
508
- for (const trackId of this.trackZoneState.keys()) {
509
- if (!activeTrackIds.has(trackId)) {
510
- this.trackZoneState.delete(trackId);
511
- }
255
+ // src/state/state-analyzer.ts
256
+ var DEFAULT_STATE_ANALYZER_CONFIG = {
257
+ stationaryThresholdSec: 10,
258
+ loiteringThresholdSec: 60,
259
+ velocityThreshold: 2,
260
+ enteringFrames: 5
261
+ };
262
+ function magnitude(v) {
263
+ return Math.sqrt(v.dx * v.dx + v.dy * v.dy);
264
+ }
265
+ var StateAnalyzer = class {
266
+ states = /* @__PURE__ */ new Map();
267
+ config;
268
+ constructor(config = {}) {
269
+ this.config = { ...DEFAULT_STATE_ANALYZER_CONFIG, ...config };
270
+ }
271
+ analyze(tracks, timestamp) {
272
+ const results = [];
273
+ const activeIds = /* @__PURE__ */ new Set();
274
+ for (const track of tracks) {
275
+ activeIds.add(track.trackId);
276
+ const prev = this.states.get(track.trackId);
277
+ const centroid = {
278
+ x: track.bbox.x + track.bbox.w / 2,
279
+ y: track.bbox.y + track.bbox.h / 2
280
+ };
281
+ const speed = track.velocity ? magnitude(track.velocity) : 0;
282
+ let enteredAt;
283
+ let stationarySince;
284
+ let totalDistancePx;
285
+ if (!prev) {
286
+ enteredAt = timestamp;
287
+ stationarySince = speed <= this.config.velocityThreshold ? timestamp : void 0;
288
+ totalDistancePx = 0;
289
+ } else {
290
+ enteredAt = prev.enteredAt;
291
+ const dx = centroid.x - prev.lastPosition.x;
292
+ const dy = centroid.y - prev.lastPosition.y;
293
+ const dist = Math.sqrt(dx * dx + dy * dy);
294
+ totalDistancePx = prev.totalDistancePx + dist;
295
+ if (speed > this.config.velocityThreshold) {
296
+ stationarySince = void 0;
297
+ } else {
298
+ stationarySince = prev.stationarySince ?? timestamp;
512
299
  }
513
- return events;
514
300
  }
515
- /** Returns a snapshot of the current track → zones mapping */
516
- getTrackZones() {
517
- return new Map(this.trackZoneState);
301
+ const newInternal = {
302
+ enteredAt,
303
+ stationarySince,
304
+ totalDistancePx,
305
+ lastPosition: centroid
306
+ };
307
+ this.states.set(track.trackId, newInternal);
308
+ const dwellTimeMs = timestamp - enteredAt;
309
+ const state = this.computeObjectState(track.trackAge, speed, stationarySince, timestamp);
310
+ results.push({
311
+ trackId: track.trackId,
312
+ state,
313
+ stationarySince,
314
+ enteredAt,
315
+ totalDistancePx,
316
+ dwellTimeMs
317
+ });
318
+ }
319
+ for (const [id, internalState] of this.states) {
320
+ if (!activeIds.has(id)) {
321
+ results.push({
322
+ trackId: id,
323
+ state: "leaving",
324
+ stationarySince: internalState.stationarySince,
325
+ enteredAt: internalState.enteredAt,
326
+ totalDistancePx: internalState.totalDistancePx,
327
+ dwellTimeMs: timestamp - internalState.enteredAt
328
+ });
329
+ this.states.delete(id);
518
330
  }
519
- };
520
- exports.ZoneEvaluator = ZoneEvaluator2;
331
+ }
332
+ return results;
521
333
  }
522
- });
523
-
524
- // src/events/event-filters.js
525
- var require_event_filters = __commonJS({
526
- "src/events/event-filters.js"(exports) {
527
- "use strict";
528
- Object.defineProperty(exports, "__esModule", { value: true });
529
- exports.EventFilter = exports.DEFAULT_EVENT_FILTER_CONFIG = void 0;
530
- exports.DEFAULT_EVENT_FILTER_CONFIG = {
531
- minTrackAge: 3,
532
- cooldownSec: 5,
533
- enabledTypes: [
534
- "object.entering",
535
- "object.leaving",
536
- "object.stationary",
537
- "object.loitering",
538
- "zone.enter",
539
- "zone.exit",
540
- "tripwire.cross"
541
- ]
542
- };
543
- var EventFilter2 = class {
544
- config;
545
- /** key: `${trackId}:${eventType}` → last emitted timestamp */
546
- lastEmitted = /* @__PURE__ */ new Map();
547
- constructor(config) {
548
- this.config = config;
549
- }
550
- shouldEmit(trackId, eventType, trackAge, timestamp) {
551
- if (!this.config.enabledTypes.includes(eventType))
552
- return false;
553
- if (trackAge < this.config.minTrackAge)
554
- return false;
555
- const key = `${trackId}:${eventType}`;
556
- const last = this.lastEmitted.get(key);
557
- if (last !== void 0 && timestamp - last < this.config.cooldownSec * 1e3)
558
- return false;
559
- this.lastEmitted.set(key, timestamp);
560
- return true;
561
- }
562
- /** Record an emission without a gate check — for events that bypass normal cooldown logic */
563
- recordEmission(trackId, eventType, timestamp) {
564
- const key = `${trackId}:${eventType}`;
565
- this.lastEmitted.set(key, timestamp);
566
- }
567
- /** Remove cooldown entries for tracks that are no longer active */
568
- cleanup(activeTrackIds) {
569
- for (const key of this.lastEmitted.keys()) {
570
- const colonIdx = key.indexOf(":");
571
- if (colonIdx === -1)
572
- continue;
573
- const trackId = key.slice(0, colonIdx);
574
- if (!activeTrackIds.has(trackId)) {
575
- this.lastEmitted.delete(key);
576
- }
577
- }
334
+ computeObjectState(trackAge, speed, stationarySince, timestamp) {
335
+ if (trackAge <= this.config.enteringFrames) {
336
+ return "entering";
337
+ }
338
+ if (stationarySince !== void 0) {
339
+ const stationaryDurationSec = (timestamp - stationarySince) / 1e3;
340
+ if (stationaryDurationSec >= this.config.loiteringThresholdSec) {
341
+ return "loitering";
578
342
  }
579
- /** Reset all cooldown state */
580
- reset() {
581
- this.lastEmitted.clear();
343
+ if (stationaryDurationSec >= this.config.stationaryThresholdSec) {
344
+ return "stationary";
582
345
  }
583
- };
584
- exports.EventFilter = EventFilter2;
346
+ }
347
+ if (speed > this.config.velocityThreshold) {
348
+ return "moving";
349
+ }
350
+ return "moving";
585
351
  }
586
- });
352
+ };
587
353
 
588
- // src/events/event-emitter.js
589
- var require_event_emitter = __commonJS({
590
- "src/events/event-emitter.js"(exports) {
591
- "use strict";
592
- Object.defineProperty(exports, "__esModule", { value: true });
593
- exports.DetectionEventEmitter = void 0;
594
- var node_crypto_1 = __require("crypto");
595
- var event_filters_js_1 = require_event_filters();
596
- var DetectionEventEmitter2 = class {
597
- filter;
598
- /** trackId last known ObjectState */
599
- previousStates = /* @__PURE__ */ new Map();
600
- constructor(filterConfig = {}) {
601
- this.filter = new event_filters_js_1.EventFilter({ ...event_filters_js_1.DEFAULT_EVENT_FILTER_CONFIG, ...filterConfig });
602
- }
603
- emit(tracks, states, zoneEvents, classifications, deviceId) {
604
- const events = [];
605
- const timestamp = Date.now();
606
- const trackMap = /* @__PURE__ */ new Map();
607
- for (const t of tracks)
608
- trackMap.set(t.trackId, t);
609
- const stateMap = /* @__PURE__ */ new Map();
610
- for (const s of states)
611
- stateMap.set(s.trackId, s);
612
- const classifMap = /* @__PURE__ */ new Map();
613
- for (const c of classifications)
614
- classifMap.set(c.trackId, c.classifications);
615
- for (const state of states) {
616
- const track = trackMap.get(state.trackId);
617
- if (!track)
618
- continue;
619
- const prevState = this.previousStates.get(state.trackId);
620
- const classifs = classifMap.get(state.trackId) ?? [];
621
- if (state.state === "entering" && prevState === void 0) {
622
- this.tryEmit(events, "object.entering", track, state, classifs, [], deviceId, timestamp);
623
- }
624
- if (state.state === "leaving") {
625
- this.tryEmit(events, "object.leaving", track, state, classifs, [], deviceId, timestamp);
626
- }
627
- if (state.state === "stationary" && prevState !== void 0 && prevState !== "stationary" && prevState !== "loitering") {
628
- this.tryEmit(events, "object.stationary", track, state, classifs, [], deviceId, timestamp);
629
- }
630
- if (state.state === "loitering" && prevState === "stationary") {
631
- this.tryEmit(events, "object.loitering", track, state, classifs, [], deviceId, timestamp);
632
- }
633
- if (state.state !== "leaving") {
634
- this.previousStates.set(state.trackId, state.state);
635
- } else {
636
- this.previousStates.delete(state.trackId);
637
- }
354
+ // src/zones/zone-evaluator.ts
355
+ var ZoneEvaluator = class {
356
+ /** Track which zones each track was in last frame */
357
+ trackZoneState = /* @__PURE__ */ new Map();
358
+ evaluate(tracks, zones, imageWidth, imageHeight, timestamp) {
359
+ const events = [];
360
+ const activeTrackIds = /* @__PURE__ */ new Set();
361
+ for (const track of tracks) {
362
+ activeTrackIds.add(track.trackId);
363
+ const centroid = bboxCentroid(track.bbox);
364
+ const prevZones = this.trackZoneState.get(track.trackId) ?? /* @__PURE__ */ new Set();
365
+ const currZones = /* @__PURE__ */ new Set();
366
+ for (const zone of zones) {
367
+ if (zone.alertOnClasses && zone.alertOnClasses.length > 0) {
368
+ if (!zone.alertOnClasses.includes(track.class)) continue;
638
369
  }
639
- for (const ze of zoneEvents) {
640
- const track = trackMap.get(ze.trackId);
641
- if (!track)
642
- continue;
643
- const state = stateMap.get(ze.trackId);
644
- if (!state)
645
- continue;
646
- let eventType;
647
- if (ze.type === "tripwire-cross") {
648
- eventType = "tripwire.cross";
649
- } else if (ze.type === "zone-enter") {
650
- eventType = "zone.enter";
651
- } else {
652
- eventType = "zone.exit";
370
+ if (zone.type === "polygon") {
371
+ const pixelPoints = zone.points.map((p) => normalizeToPixel(p, imageWidth, imageHeight));
372
+ const inside = pointInPolygon(centroid, pixelPoints);
373
+ if (inside) {
374
+ currZones.add(zone.id);
653
375
  }
654
- const classifs = classifMap.get(ze.trackId) ?? [];
655
- this.tryEmit(events, eventType, track, state, classifs, [ze], deviceId, timestamp);
656
- }
657
- const activeIds = new Set(tracks.map((t) => t.trackId));
658
- this.filter.cleanup(activeIds);
659
- return events;
660
- }
661
- tryEmit(events, eventType, track, state, classifications, zoneEvents, deviceId, timestamp) {
662
- if (!this.filter.shouldEmit(track.trackId, eventType, track.trackAge, timestamp))
663
- return;
664
- events.push({
665
- id: (0, node_crypto_1.randomUUID)(),
666
- type: eventType,
667
- timestamp,
668
- deviceId,
669
- detection: track,
670
- classifications,
671
- objectState: state,
672
- zoneEvents,
673
- trackPath: track.path
674
- });
675
- }
676
- reset() {
677
- this.previousStates.clear();
678
- this.filter.reset();
679
- }
680
- };
681
- exports.DetectionEventEmitter = DetectionEventEmitter2;
682
- }
683
- });
684
-
685
- // src/analytics/track-store.js
686
- var require_track_store = __commonJS({
687
- "src/analytics/track-store.js"(exports) {
688
- "use strict";
689
- Object.defineProperty(exports, "__esModule", { value: true });
690
- exports.TrackStore = void 0;
691
- var MAX_PATH_LENGTH = 300;
692
- var TrackStore2 = class {
693
- activeTracks = /* @__PURE__ */ new Map();
694
- update(tracks, states, zoneEvents, classifications) {
695
- const stateMap = /* @__PURE__ */ new Map();
696
- for (const s of states)
697
- stateMap.set(s.trackId, s);
698
- const classifMap = /* @__PURE__ */ new Map();
699
- for (const c of classifications)
700
- classifMap.set(c.trackId, c.classifications);
701
- const now = Date.now();
702
- for (const track of tracks) {
703
- let acc = this.activeTracks.get(track.trackId);
704
- if (!acc) {
705
- acc = {
376
+ if (inside && !prevZones.has(zone.id)) {
377
+ events.push({
378
+ type: "zone-enter",
379
+ zoneId: zone.id,
380
+ zoneName: zone.name,
706
381
  trackId: track.trackId,
707
- class: track.class,
708
- originalClass: track.originalClass,
709
- state: "entering",
710
- firstSeen: track.path.length === 0 ? now : now,
711
- lastSeen: now,
712
- path: [],
713
- zoneTransitions: [],
714
- events: []
715
- };
716
- this.activeTracks.set(track.trackId, acc);
717
- }
718
- acc.class = track.class;
719
- acc.originalClass = track.originalClass;
720
- acc.lastSeen = now;
721
- const state = stateMap.get(track.trackId);
722
- if (state)
723
- acc.state = state.state;
724
- const pathPoint = {
725
- timestamp: now,
726
- bbox: track.bbox,
727
- velocity: track.velocity ?? { dx: 0, dy: 0 }
728
- };
729
- if (acc.path.length >= MAX_PATH_LENGTH) {
730
- acc.path.shift();
382
+ detection: track,
383
+ timestamp
384
+ });
731
385
  }
732
- acc.path.push(pathPoint);
733
- const classifs = classifMap.get(track.trackId) ?? [];
734
- for (const c of classifs) {
735
- if (c.metadata?.type === "face-recognition" && typeof c.text === "string") {
736
- acc.identity = { name: c.text, confidence: c.score, matchedAt: now };
737
- } else if (c.metadata?.type === "plate-recognition" && typeof c.text === "string") {
738
- acc.plateText = { text: c.text, confidence: c.score, readAt: now };
739
- } else if (c.metadata?.type === "sub-class") {
740
- acc.subClass = { class: c.class, confidence: c.score };
741
- }
386
+ if (!inside && prevZones.has(zone.id)) {
387
+ events.push({
388
+ type: "zone-exit",
389
+ zoneId: zone.id,
390
+ zoneName: zone.name,
391
+ trackId: track.trackId,
392
+ detection: track,
393
+ timestamp
394
+ });
742
395
  }
743
- }
744
- for (const ze of zoneEvents) {
745
- const acc = this.activeTracks.get(ze.trackId);
746
- if (!acc)
747
- continue;
748
- if (ze.type === "zone-enter") {
749
- acc.zoneTransitions.push({
750
- zoneId: ze.zoneId,
751
- zoneName: ze.zoneName,
752
- entered: ze.timestamp,
753
- dwellMs: 0
396
+ } else if (zone.type === "tripwire" && track.path.length >= 1) {
397
+ const prevBbox = track.path[track.path.length - 1];
398
+ if (!prevBbox) continue;
399
+ const prevCentroid = bboxCentroid(prevBbox);
400
+ const p0 = zone.points[0];
401
+ const p1 = zone.points[1];
402
+ if (!p0 || !p1) continue;
403
+ const tripwireLine = {
404
+ p1: normalizeToPixel(p0, imageWidth, imageHeight),
405
+ p2: normalizeToPixel(p1, imageWidth, imageHeight)
406
+ };
407
+ const cross = tripwireCrossing(prevCentroid, centroid, tripwireLine);
408
+ if (cross?.crossed) {
409
+ events.push({
410
+ type: "tripwire-cross",
411
+ zoneId: zone.id,
412
+ zoneName: zone.name,
413
+ trackId: track.trackId,
414
+ detection: track,
415
+ direction: cross.direction,
416
+ timestamp
754
417
  });
755
- } else if (ze.type === "zone-exit") {
756
- let openIdx = -1;
757
- for (let i = acc.zoneTransitions.length - 1; i >= 0; i--) {
758
- const t = acc.zoneTransitions[i];
759
- if (t.zoneId === ze.zoneId && t.exited === void 0) {
760
- openIdx = i;
761
- break;
762
- }
763
- }
764
- if (openIdx >= 0) {
765
- const open = acc.zoneTransitions[openIdx];
766
- acc.zoneTransitions[openIdx] = {
767
- ...open,
768
- exited: ze.timestamp,
769
- dwellMs: ze.timestamp - open.entered
770
- };
771
- }
772
418
  }
773
419
  }
774
420
  }
775
- /** Attach a DetectionEvent to its associated track accumulator */
776
- addEvent(trackId, event) {
777
- const acc = this.activeTracks.get(trackId);
778
- if (!acc)
779
- return;
780
- acc.events.push(event);
781
- if (event.snapshot && !acc.bestSnapshot) {
782
- acc.bestSnapshot = event.snapshot;
783
- }
784
- }
785
- /** Get accumulated detail for an active track */
786
- getTrackDetail(trackId) {
787
- const acc = this.activeTracks.get(trackId);
788
- if (!acc)
789
- return null;
790
- return this.buildTrackDetail(acc);
791
- }
792
- /** Called when a track ends — returns complete TrackDetail and removes from active set */
793
- finishTrack(trackId) {
794
- const acc = this.activeTracks.get(trackId);
795
- if (!acc)
796
- return null;
797
- const detail = this.buildTrackDetail(acc);
798
- this.activeTracks.delete(trackId);
799
- return detail;
800
- }
801
- /** Get all active track IDs */
802
- getActiveTrackIds() {
803
- return Array.from(this.activeTracks.keys());
421
+ this.trackZoneState.set(track.trackId, currZones);
422
+ }
423
+ for (const trackId of this.trackZoneState.keys()) {
424
+ if (!activeTrackIds.has(trackId)) {
425
+ this.trackZoneState.delete(trackId);
804
426
  }
805
- /** Clear all accumulated state */
806
- reset() {
807
- this.activeTracks.clear();
427
+ }
428
+ return events;
429
+ }
430
+ /** Returns a snapshot of the current track → zones mapping */
431
+ getTrackZones() {
432
+ return new Map(this.trackZoneState);
433
+ }
434
+ };
435
+
436
+ // src/events/event-filters.ts
437
+ var DEFAULT_EVENT_FILTER_CONFIG = {
438
+ minTrackAge: 3,
439
+ cooldownSec: 5,
440
+ enabledTypes: [
441
+ "object.entering",
442
+ "object.leaving",
443
+ "object.stationary",
444
+ "object.loitering",
445
+ "zone.enter",
446
+ "zone.exit",
447
+ "tripwire.cross"
448
+ ]
449
+ };
450
+ var EventFilter = class {
451
+ constructor(config) {
452
+ this.config = config;
453
+ }
454
+ /** key: `${trackId}:${eventType}` → last emitted timestamp */
455
+ lastEmitted = /* @__PURE__ */ new Map();
456
+ shouldEmit(trackId, eventType, trackAge, timestamp) {
457
+ if (!this.config.enabledTypes.includes(eventType)) return false;
458
+ if (trackAge < this.config.minTrackAge) return false;
459
+ const key = `${trackId}:${eventType}`;
460
+ const last = this.lastEmitted.get(key);
461
+ if (last !== void 0 && timestamp - last < this.config.cooldownSec * 1e3) return false;
462
+ this.lastEmitted.set(key, timestamp);
463
+ return true;
464
+ }
465
+ /** Record an emission without a gate check — for events that bypass normal cooldown logic */
466
+ recordEmission(trackId, eventType, timestamp) {
467
+ const key = `${trackId}:${eventType}`;
468
+ this.lastEmitted.set(key, timestamp);
469
+ }
470
+ /** Remove cooldown entries for tracks that are no longer active */
471
+ cleanup(activeTrackIds) {
472
+ for (const key of this.lastEmitted.keys()) {
473
+ const colonIdx = key.indexOf(":");
474
+ if (colonIdx === -1) continue;
475
+ const trackId = key.slice(0, colonIdx);
476
+ if (!activeTrackIds.has(trackId)) {
477
+ this.lastEmitted.delete(key);
808
478
  }
809
- buildTrackDetail(acc) {
810
- return {
811
- trackId: acc.trackId,
812
- class: acc.class,
813
- originalClass: acc.originalClass,
814
- state: acc.state,
815
- firstSeen: acc.firstSeen,
816
- lastSeen: acc.lastSeen,
817
- totalDwellMs: acc.lastSeen - acc.firstSeen,
818
- path: acc.path,
819
- identity: acc.identity,
820
- plateText: acc.plateText,
821
- subClass: acc.subClass,
822
- zoneTransitions: acc.zoneTransitions,
823
- bestSnapshot: acc.bestSnapshot,
824
- events: acc.events
825
- };
479
+ }
480
+ }
481
+ /** Reset all cooldown state */
482
+ reset() {
483
+ this.lastEmitted.clear();
484
+ }
485
+ };
486
+
487
+ // src/events/event-emitter.ts
488
+ import { randomUUID } from "crypto";
489
+ var DetectionEventEmitter = class {
490
+ filter;
491
+ /** trackId → last known ObjectState */
492
+ previousStates = /* @__PURE__ */ new Map();
493
+ constructor(filterConfig = {}) {
494
+ this.filter = new EventFilter({ ...DEFAULT_EVENT_FILTER_CONFIG, ...filterConfig });
495
+ }
496
+ emit(tracks, states, zoneEvents, classifications, deviceId) {
497
+ const events = [];
498
+ const timestamp = Date.now();
499
+ const trackMap = /* @__PURE__ */ new Map();
500
+ for (const t of tracks) trackMap.set(t.trackId, t);
501
+ const stateMap = /* @__PURE__ */ new Map();
502
+ for (const s of states) stateMap.set(s.trackId, s);
503
+ const classifMap = /* @__PURE__ */ new Map();
504
+ for (const c of classifications) classifMap.set(c.trackId, c.classifications);
505
+ for (const state of states) {
506
+ const track = trackMap.get(state.trackId);
507
+ if (!track) continue;
508
+ const prevState = this.previousStates.get(state.trackId);
509
+ const classifs = classifMap.get(state.trackId) ?? [];
510
+ if (state.state === "entering" && prevState === void 0) {
511
+ this.tryEmit(events, "object.entering", track, state, classifs, [], deviceId, timestamp);
512
+ }
513
+ if (state.state === "leaving") {
514
+ this.tryEmit(events, "object.leaving", track, state, classifs, [], deviceId, timestamp);
515
+ }
516
+ if (state.state === "stationary" && prevState !== void 0 && prevState !== "stationary" && prevState !== "loitering") {
517
+ this.tryEmit(events, "object.stationary", track, state, classifs, [], deviceId, timestamp);
518
+ }
519
+ if (state.state === "loitering" && prevState === "stationary") {
520
+ this.tryEmit(events, "object.loitering", track, state, classifs, [], deviceId, timestamp);
521
+ }
522
+ if (state.state !== "leaving") {
523
+ this.previousStates.set(state.trackId, state.state);
524
+ } else {
525
+ this.previousStates.delete(state.trackId);
826
526
  }
827
- };
828
- exports.TrackStore = TrackStore2;
527
+ }
528
+ for (const ze of zoneEvents) {
529
+ const track = trackMap.get(ze.trackId);
530
+ if (!track) continue;
531
+ const state = stateMap.get(ze.trackId);
532
+ if (!state) continue;
533
+ let eventType;
534
+ if (ze.type === "tripwire-cross") {
535
+ eventType = "tripwire.cross";
536
+ } else if (ze.type === "zone-enter") {
537
+ eventType = "zone.enter";
538
+ } else {
539
+ eventType = "zone.exit";
540
+ }
541
+ const classifs = classifMap.get(ze.trackId) ?? [];
542
+ this.tryEmit(events, eventType, track, state, classifs, [ze], deviceId, timestamp);
543
+ }
544
+ const activeIds = new Set(tracks.map((t) => t.trackId));
545
+ this.filter.cleanup(activeIds);
546
+ return events;
829
547
  }
830
- });
548
+ tryEmit(events, eventType, track, state, classifications, zoneEvents, deviceId, timestamp) {
549
+ if (!this.filter.shouldEmit(track.trackId, eventType, track.trackAge, timestamp)) return;
550
+ events.push({
551
+ id: randomUUID(),
552
+ type: eventType,
553
+ timestamp,
554
+ deviceId,
555
+ detection: track,
556
+ classifications,
557
+ objectState: state,
558
+ zoneEvents,
559
+ trackPath: track.path
560
+ });
561
+ }
562
+ reset() {
563
+ this.previousStates.clear();
564
+ this.filter.reset();
565
+ }
566
+ };
831
567
 
832
- // src/analytics/live-state-manager.js
833
- var require_live_state_manager = __commonJS({
834
- "src/analytics/live-state-manager.js"(exports) {
835
- "use strict";
836
- Object.defineProperty(exports, "__esModule", { value: true });
837
- exports.LiveStateManager = void 0;
838
- var FpsCounter = class {
839
- frameTimes = [];
840
- windowMs = 5e3;
841
- tick(timestamp) {
842
- this.frameTimes.push(timestamp);
843
- const cutoff = timestamp - this.windowMs;
844
- while (this.frameTimes.length > 0 && this.frameTimes[0] < cutoff) {
845
- this.frameTimes.shift();
568
+ // src/analytics/track-store.ts
569
+ var MAX_PATH_LENGTH2 = 300;
570
+ var TrackStore = class {
571
+ activeTracks = /* @__PURE__ */ new Map();
572
+ update(tracks, states, zoneEvents, classifications) {
573
+ const stateMap = /* @__PURE__ */ new Map();
574
+ for (const s of states) stateMap.set(s.trackId, s);
575
+ const classifMap = /* @__PURE__ */ new Map();
576
+ for (const c of classifications) classifMap.set(c.trackId, c.classifications);
577
+ const now = Date.now();
578
+ for (const track of tracks) {
579
+ let acc = this.activeTracks.get(track.trackId);
580
+ if (!acc) {
581
+ acc = {
582
+ trackId: track.trackId,
583
+ class: track.class,
584
+ originalClass: track.originalClass,
585
+ state: "entering",
586
+ firstSeen: track.path.length === 0 ? now : now,
587
+ lastSeen: now,
588
+ path: [],
589
+ zoneTransitions: [],
590
+ events: []
591
+ };
592
+ this.activeTracks.set(track.trackId, acc);
593
+ }
594
+ acc.class = track.class;
595
+ acc.originalClass = track.originalClass;
596
+ acc.lastSeen = now;
597
+ const state = stateMap.get(track.trackId);
598
+ if (state) acc.state = state.state;
599
+ const pathPoint = {
600
+ timestamp: now,
601
+ bbox: track.bbox,
602
+ velocity: track.velocity ?? { dx: 0, dy: 0 }
603
+ };
604
+ if (acc.path.length >= MAX_PATH_LENGTH2) {
605
+ acc.path.shift();
606
+ }
607
+ acc.path.push(pathPoint);
608
+ const classifs = classifMap.get(track.trackId) ?? [];
609
+ for (const c of classifs) {
610
+ if (c.metadata?.type === "face-recognition" && typeof c.text === "string") {
611
+ acc.identity = { name: c.text, confidence: c.score, matchedAt: now };
612
+ } else if (c.metadata?.type === "plate-recognition" && typeof c.text === "string") {
613
+ acc.plateText = { text: c.text, confidence: c.score, readAt: now };
614
+ } else if (c.metadata?.type === "sub-class") {
615
+ acc.subClass = { class: c.class, confidence: c.score };
846
616
  }
847
617
  }
848
- getFps() {
849
- if (this.frameTimes.length < 2)
850
- return 0;
851
- const oldest = this.frameTimes[0];
852
- const newest = this.frameTimes[this.frameTimes.length - 1];
853
- const durationSec = (newest - oldest) / 1e3;
854
- if (durationSec <= 0)
855
- return 0;
856
- return (this.frameTimes.length - 1) / durationSec;
857
- }
858
- };
859
- var LiveStateManager2 = class {
860
- zones = [];
861
- fpsCounter = new FpsCounter();
862
- pipelineMsHistory = [];
863
- maxPipelineHistory = 30;
864
- setZones(zones) {
865
- this.zones = [...zones];
866
- }
867
- buildState(deviceId, tracks, states, trackZones, pipelineStatus, pipelineMs, lastEvent, timestamp) {
868
- this.fpsCounter.tick(timestamp);
869
- if (this.pipelineMsHistory.length >= this.maxPipelineHistory) {
870
- this.pipelineMsHistory.shift();
871
- }
872
- this.pipelineMsHistory.push(pipelineMs);
873
- const avgPipelineMs = this.pipelineMsHistory.length === 0 ? 0 : this.pipelineMsHistory.reduce((sum, v) => sum + v, 0) / this.pipelineMsHistory.length;
874
- const stateMap = /* @__PURE__ */ new Map();
875
- for (const s of states)
876
- stateMap.set(s.trackId, s);
877
- const trackSummaries = tracks.map((track) => {
878
- const state = stateMap.get(track.trackId);
879
- const inZones = Array.from(trackZones.get(track.trackId) ?? []);
880
- return {
881
- trackId: track.trackId,
882
- class: track.class,
883
- originalClass: track.originalClass,
884
- state: state?.state ?? "moving",
885
- bbox: track.bbox,
886
- velocity: track.velocity ?? { dx: 0, dy: 0 },
887
- dwellTimeMs: state?.dwellTimeMs ?? 0,
888
- inZones
889
- };
618
+ }
619
+ for (const ze of zoneEvents) {
620
+ const acc = this.activeTracks.get(ze.trackId);
621
+ if (!acc) continue;
622
+ if (ze.type === "zone-enter") {
623
+ acc.zoneTransitions.push({
624
+ zoneId: ze.zoneId,
625
+ zoneName: ze.zoneName,
626
+ entered: ze.timestamp,
627
+ dwellMs: 0
890
628
  });
891
- let movingObjects = 0;
892
- let stationaryObjects = 0;
893
- for (const summary of trackSummaries) {
894
- if (summary.state === "moving" || summary.state === "entering")
895
- movingObjects++;
896
- else if (summary.state === "stationary" || summary.state === "loitering")
897
- stationaryObjects++;
898
- }
899
- const objectCounts = {};
900
- for (const summary of trackSummaries) {
901
- objectCounts[summary.class] = (objectCounts[summary.class] ?? 0) + 1;
902
- }
903
- const zoneLiveStates = this.zones.map((zone) => {
904
- const tracksInZone = trackSummaries.filter((t) => t.inZones.includes(zone.id));
905
- const objectsByClass = {};
906
- const loiteringTracks = [];
907
- for (const t of tracksInZone) {
908
- objectsByClass[t.class] = (objectsByClass[t.class] ?? 0) + 1;
909
- if (t.state === "loitering")
910
- loiteringTracks.push(t.trackId);
629
+ } else if (ze.type === "zone-exit") {
630
+ let openIdx = -1;
631
+ for (let i = acc.zoneTransitions.length - 1; i >= 0; i--) {
632
+ const t = acc.zoneTransitions[i];
633
+ if (t.zoneId === ze.zoneId && t.exited === void 0) {
634
+ openIdx = i;
635
+ break;
911
636
  }
912
- const avgDwellTimeMs = tracksInZone.length === 0 ? 0 : tracksInZone.reduce((sum, t) => sum + t.dwellTimeMs, 0) / tracksInZone.length;
913
- return {
914
- zoneId: zone.id,
915
- zoneName: zone.name,
916
- occupied: tracksInZone.length > 0,
917
- objectCount: tracksInZone.length,
918
- objectsByClass,
919
- trackIds: tracksInZone.map((t) => t.trackId),
920
- avgDwellTimeMs,
921
- totalEntrancesToday: 0,
922
- // requires DB — left for server orchestrator
923
- totalExitsToday: 0,
924
- loiteringTracks
637
+ }
638
+ if (openIdx >= 0) {
639
+ const open = acc.zoneTransitions[openIdx];
640
+ acc.zoneTransitions[openIdx] = {
641
+ ...open,
642
+ exited: ze.timestamp,
643
+ dwellMs: ze.timestamp - open.entered
925
644
  };
926
- });
927
- return {
928
- deviceId,
929
- lastFrameTimestamp: timestamp,
930
- activeObjects: trackSummaries.length,
931
- movingObjects,
932
- stationaryObjects,
933
- objectCounts,
934
- tracks: trackSummaries,
935
- zones: zoneLiveStates,
936
- lastEvent,
937
- pipelineStatus,
938
- avgPipelineMs,
939
- currentFps: this.fpsCounter.getFps()
940
- };
941
- }
942
- reset() {
943
- this.pipelineMsHistory.length = 0;
645
+ }
944
646
  }
647
+ }
648
+ }
649
+ /** Attach a DetectionEvent to its associated track accumulator */
650
+ addEvent(trackId, event) {
651
+ const acc = this.activeTracks.get(trackId);
652
+ if (!acc) return;
653
+ acc.events.push(event);
654
+ if (event.snapshot && !acc.bestSnapshot) {
655
+ acc.bestSnapshot = event.snapshot;
656
+ }
657
+ }
658
+ /** Get accumulated detail for an active track */
659
+ getTrackDetail(trackId) {
660
+ const acc = this.activeTracks.get(trackId);
661
+ if (!acc) return null;
662
+ return this.buildTrackDetail(acc);
663
+ }
664
+ /** Called when a track ends — returns complete TrackDetail and removes from active set */
665
+ finishTrack(trackId) {
666
+ const acc = this.activeTracks.get(trackId);
667
+ if (!acc) return null;
668
+ const detail = this.buildTrackDetail(acc);
669
+ this.activeTracks.delete(trackId);
670
+ return detail;
671
+ }
672
+ /** Get all active track IDs */
673
+ getActiveTrackIds() {
674
+ return Array.from(this.activeTracks.keys());
675
+ }
676
+ /** Clear all accumulated state */
677
+ reset() {
678
+ this.activeTracks.clear();
679
+ }
680
+ buildTrackDetail(acc) {
681
+ return {
682
+ trackId: acc.trackId,
683
+ class: acc.class,
684
+ originalClass: acc.originalClass,
685
+ state: acc.state,
686
+ firstSeen: acc.firstSeen,
687
+ lastSeen: acc.lastSeen,
688
+ totalDwellMs: acc.lastSeen - acc.firstSeen,
689
+ path: acc.path,
690
+ identity: acc.identity,
691
+ plateText: acc.plateText,
692
+ subClass: acc.subClass,
693
+ zoneTransitions: acc.zoneTransitions,
694
+ bestSnapshot: acc.bestSnapshot,
695
+ events: acc.events
945
696
  };
946
- exports.LiveStateManager = LiveStateManager2;
947
697
  }
948
- });
698
+ };
949
699
 
950
- // src/analytics/heatmap-aggregator.js
951
- var require_heatmap_aggregator = __commonJS({
952
- "src/analytics/heatmap-aggregator.js"(exports) {
953
- "use strict";
954
- Object.defineProperty(exports, "__esModule", { value: true });
955
- exports.HeatmapAggregator = void 0;
956
- var HeatmapAggregator2 = class {
957
- width;
958
- height;
959
- gridSize;
960
- grid;
961
- maxCount = 0;
962
- constructor(width, height, gridSize) {
963
- this.width = width;
964
- this.height = height;
965
- this.gridSize = gridSize;
966
- if (gridSize <= 0)
967
- throw new Error("gridSize must be > 0");
968
- this.grid = new Float32Array(gridSize * gridSize);
969
- }
970
- /**
971
- * Add a normalized point (0–1 range) to the heatmap.
972
- * Points outside [0, 1] are clamped to the grid boundary.
973
- */
974
- addPoint(x, y) {
975
- const col = Math.min(Math.floor(x * this.gridSize), this.gridSize - 1);
976
- const row = Math.min(Math.floor(y * this.gridSize), this.gridSize - 1);
977
- const idx = row * this.gridSize + col;
978
- this.grid[idx] += 1;
979
- if (this.grid[idx] > this.maxCount) {
980
- this.maxCount = this.grid[idx];
981
- }
982
- }
983
- /** Add a pixel-coordinate point. Normalizes against the configured width/height. */
984
- addPixelPoint(px, py) {
985
- this.addPoint(px / this.width, py / this.height);
986
- }
987
- getHeatmap() {
988
- return {
989
- width: this.width,
990
- height: this.height,
991
- gridSize: this.gridSize,
992
- cells: new Float32Array(this.grid),
993
- maxCount: this.maxCount
994
- };
995
- }
996
- reset() {
997
- this.grid.fill(0);
998
- this.maxCount = 0;
999
- }
1000
- /** Total number of points accumulated */
1001
- get totalPoints() {
1002
- let total = 0;
1003
- for (let i = 0; i < this.grid.length; i++) {
1004
- total += this.grid[i];
1005
- }
1006
- return total;
1007
- }
700
+ // src/analytics/live-state-manager.ts
701
+ var FpsCounter = class {
702
+ frameTimes = [];
703
+ windowMs = 5e3;
704
+ tick(timestamp) {
705
+ this.frameTimes.push(timestamp);
706
+ const cutoff = timestamp - this.windowMs;
707
+ while (this.frameTimes.length > 0 && this.frameTimes[0] < cutoff) {
708
+ this.frameTimes.shift();
709
+ }
710
+ }
711
+ getFps() {
712
+ if (this.frameTimes.length < 2) return 0;
713
+ const oldest = this.frameTimes[0];
714
+ const newest = this.frameTimes[this.frameTimes.length - 1];
715
+ const durationSec = (newest - oldest) / 1e3;
716
+ if (durationSec <= 0) return 0;
717
+ return (this.frameTimes.length - 1) / durationSec;
718
+ }
719
+ };
720
+ var LiveStateManager = class {
721
+ zones = [];
722
+ fpsCounter = new FpsCounter();
723
+ pipelineMsHistory = [];
724
+ maxPipelineHistory = 30;
725
+ setZones(zones) {
726
+ this.zones = [...zones];
727
+ }
728
+ buildState(deviceId, tracks, states, trackZones, pipelineStatus, pipelineMs, lastEvent, timestamp) {
729
+ this.fpsCounter.tick(timestamp);
730
+ if (this.pipelineMsHistory.length >= this.maxPipelineHistory) {
731
+ this.pipelineMsHistory.shift();
732
+ }
733
+ this.pipelineMsHistory.push(pipelineMs);
734
+ const avgPipelineMs = this.pipelineMsHistory.length === 0 ? 0 : this.pipelineMsHistory.reduce((sum, v) => sum + v, 0) / this.pipelineMsHistory.length;
735
+ const stateMap = /* @__PURE__ */ new Map();
736
+ for (const s of states) stateMap.set(s.trackId, s);
737
+ const trackSummaries = tracks.map((track) => {
738
+ const state = stateMap.get(track.trackId);
739
+ const inZones = Array.from(trackZones.get(track.trackId) ?? []);
740
+ return {
741
+ trackId: track.trackId,
742
+ class: track.class,
743
+ originalClass: track.originalClass,
744
+ state: state?.state ?? "moving",
745
+ bbox: track.bbox,
746
+ velocity: track.velocity ?? { dx: 0, dy: 0 },
747
+ dwellTimeMs: state?.dwellTimeMs ?? 0,
748
+ inZones
749
+ };
750
+ });
751
+ let movingObjects = 0;
752
+ let stationaryObjects = 0;
753
+ for (const summary of trackSummaries) {
754
+ if (summary.state === "moving" || summary.state === "entering") movingObjects++;
755
+ else if (summary.state === "stationary" || summary.state === "loitering") stationaryObjects++;
756
+ }
757
+ const objectCounts = {};
758
+ for (const summary of trackSummaries) {
759
+ objectCounts[summary.class] = (objectCounts[summary.class] ?? 0) + 1;
760
+ }
761
+ const zoneLiveStates = this.zones.map((zone) => {
762
+ const tracksInZone = trackSummaries.filter((t) => t.inZones.includes(zone.id));
763
+ const objectsByClass = {};
764
+ const loiteringTracks = [];
765
+ for (const t of tracksInZone) {
766
+ objectsByClass[t.class] = (objectsByClass[t.class] ?? 0) + 1;
767
+ if (t.state === "loitering") loiteringTracks.push(t.trackId);
768
+ }
769
+ const avgDwellTimeMs = tracksInZone.length === 0 ? 0 : tracksInZone.reduce((sum, t) => sum + t.dwellTimeMs, 0) / tracksInZone.length;
770
+ return {
771
+ zoneId: zone.id,
772
+ zoneName: zone.name,
773
+ occupied: tracksInZone.length > 0,
774
+ objectCount: tracksInZone.length,
775
+ objectsByClass,
776
+ trackIds: tracksInZone.map((t) => t.trackId),
777
+ avgDwellTimeMs,
778
+ totalEntrancesToday: 0,
779
+ // requires DB — left for server orchestrator
780
+ totalExitsToday: 0,
781
+ loiteringTracks
782
+ };
783
+ });
784
+ return {
785
+ deviceId,
786
+ lastFrameTimestamp: timestamp,
787
+ activeObjects: trackSummaries.length,
788
+ movingObjects,
789
+ stationaryObjects,
790
+ objectCounts,
791
+ tracks: trackSummaries,
792
+ zones: zoneLiveStates,
793
+ lastEvent,
794
+ pipelineStatus,
795
+ avgPipelineMs,
796
+ currentFps: this.fpsCounter.getFps()
1008
797
  };
1009
- exports.HeatmapAggregator = HeatmapAggregator2;
1010
798
  }
1011
- });
799
+ reset() {
800
+ this.pipelineMsHistory.length = 0;
801
+ }
802
+ };
1012
803
 
1013
- // src/analytics/analytics-provider.js
1014
- var require_analytics_provider = __commonJS({
1015
- "src/analytics/analytics-provider.js"(exports) {
1016
- "use strict";
1017
- Object.defineProperty(exports, "__esModule", { value: true });
1018
- exports.AnalyticsProvider = void 0;
1019
- var AnalyticsProvider2 = class {
1020
- liveStateManager;
1021
- trackStore;
1022
- getZoneHistoryCb;
1023
- getHeatmapCb;
1024
- lastLiveState;
1025
- constructor(liveStateManager, trackStore, getZoneHistoryCb, getHeatmapCb, lastLiveState = null) {
1026
- this.liveStateManager = liveStateManager;
1027
- this.trackStore = trackStore;
1028
- this.getZoneHistoryCb = getZoneHistoryCb;
1029
- this.getHeatmapCb = getHeatmapCb;
1030
- this.lastLiveState = lastLiveState;
1031
- }
1032
- /** Called by the orchestrator each frame to update the cached live state */
1033
- updateLiveState(state) {
1034
- this.lastLiveState = state;
1035
- }
1036
- getLiveState(_deviceId) {
1037
- return this.lastLiveState;
1038
- }
1039
- getTracks(_deviceId, filter) {
1040
- const tracks = this.lastLiveState?.tracks ?? [];
1041
- if (!filter)
1042
- return [...tracks];
1043
- return tracks.filter((t) => {
1044
- if (filter.class && !filter.class.includes(t.class))
1045
- return false;
1046
- if (filter.state && !filter.state.includes(t.state))
1047
- return false;
1048
- if (filter.inZone && !t.inZones.includes(filter.inZone))
1049
- return false;
1050
- if (filter.minDwellMs !== void 0 && t.dwellTimeMs < filter.minDwellMs)
1051
- return false;
1052
- return true;
1053
- });
1054
- }
1055
- getZoneState(_deviceId, zoneId) {
1056
- return this.lastLiveState?.zones.find((z) => z.zoneId === zoneId) ?? null;
1057
- }
1058
- async getZoneHistory(deviceId, zoneId, options) {
1059
- if (this.getZoneHistoryCb) {
1060
- return this.getZoneHistoryCb(deviceId, zoneId, options);
1061
- }
1062
- return [];
1063
- }
1064
- async getHeatmap(deviceId, options) {
1065
- if (this.getHeatmapCb) {
1066
- return this.getHeatmapCb(deviceId, options);
1067
- }
1068
- const gridSize = options.resolution;
1069
- return {
1070
- width: 1920,
1071
- height: 1080,
1072
- gridSize,
1073
- cells: new Float32Array(gridSize * gridSize),
1074
- maxCount: 0
1075
- };
1076
- }
1077
- getTrackDetail(_deviceId, trackId) {
1078
- return this.trackStore.getTrackDetail(trackId);
1079
- }
1080
- getCameraStatus(_deviceId) {
1081
- return this.lastLiveState ? [...this.lastLiveState.pipelineStatus] : [];
1082
- }
804
+ // src/analytics/heatmap-aggregator.ts
805
+ var HeatmapAggregator = class {
806
+ constructor(width, height, gridSize) {
807
+ this.width = width;
808
+ this.height = height;
809
+ this.gridSize = gridSize;
810
+ if (gridSize <= 0) throw new Error("gridSize must be > 0");
811
+ this.grid = new Float32Array(gridSize * gridSize);
812
+ }
813
+ grid;
814
+ maxCount = 0;
815
+ /**
816
+ * Add a normalized point (0–1 range) to the heatmap.
817
+ * Points outside [0, 1] are clamped to the grid boundary.
818
+ */
819
+ addPoint(x, y) {
820
+ const col = Math.min(Math.floor(x * this.gridSize), this.gridSize - 1);
821
+ const row = Math.min(Math.floor(y * this.gridSize), this.gridSize - 1);
822
+ const idx = row * this.gridSize + col;
823
+ this.grid[idx] += 1;
824
+ if (this.grid[idx] > this.maxCount) {
825
+ this.maxCount = this.grid[idx];
826
+ }
827
+ }
828
+ /** Add a pixel-coordinate point. Normalizes against the configured width/height. */
829
+ addPixelPoint(px, py) {
830
+ this.addPoint(px / this.width, py / this.height);
831
+ }
832
+ getHeatmap() {
833
+ return {
834
+ width: this.width,
835
+ height: this.height,
836
+ gridSize: this.gridSize,
837
+ cells: new Float32Array(this.grid),
838
+ maxCount: this.maxCount
1083
839
  };
1084
- exports.AnalyticsProvider = AnalyticsProvider2;
1085
840
  }
1086
- });
841
+ reset() {
842
+ this.grid.fill(0);
843
+ this.maxCount = 0;
844
+ }
845
+ /** Total number of points accumulated */
846
+ get totalPoints() {
847
+ let total = 0;
848
+ for (let i = 0; i < this.grid.length; i++) {
849
+ total += this.grid[i];
850
+ }
851
+ return total;
852
+ }
853
+ };
1087
854
 
1088
- // src/snapshots/snapshot-manager.js
1089
- var require_snapshot_manager = __commonJS({
1090
- "src/snapshots/snapshot-manager.js"(exports) {
1091
- "use strict";
1092
- var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
1093
- if (k2 === void 0) k2 = k;
1094
- var desc = Object.getOwnPropertyDescriptor(m, k);
1095
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
1096
- desc = { enumerable: true, get: function() {
1097
- return m[k];
1098
- } };
1099
- }
1100
- Object.defineProperty(o, k2, desc);
1101
- }) : (function(o, m, k, k2) {
1102
- if (k2 === void 0) k2 = k;
1103
- o[k2] = m[k];
1104
- }));
1105
- var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? (function(o, v) {
1106
- Object.defineProperty(o, "default", { enumerable: true, value: v });
1107
- }) : function(o, v) {
1108
- o["default"] = v;
855
+ // src/analytics/analytics-provider.ts
856
+ var AnalyticsProvider = class {
857
+ constructor(liveStateManager, trackStore, getZoneHistoryCb, getHeatmapCb, lastLiveState = null) {
858
+ this.liveStateManager = liveStateManager;
859
+ this.trackStore = trackStore;
860
+ this.getZoneHistoryCb = getZoneHistoryCb;
861
+ this.getHeatmapCb = getHeatmapCb;
862
+ this.lastLiveState = lastLiveState;
863
+ }
864
+ /** Called by the orchestrator each frame to update the cached live state */
865
+ updateLiveState(state) {
866
+ this.lastLiveState = state;
867
+ }
868
+ getLiveState(_deviceId) {
869
+ return this.lastLiveState;
870
+ }
871
+ getTracks(_deviceId, filter) {
872
+ const tracks = this.lastLiveState?.tracks ?? [];
873
+ if (!filter) return [...tracks];
874
+ return tracks.filter((t) => {
875
+ if (filter.class && !filter.class.includes(t.class)) return false;
876
+ if (filter.state && !filter.state.includes(t.state)) return false;
877
+ if (filter.inZone && !t.inZones.includes(filter.inZone)) return false;
878
+ if (filter.minDwellMs !== void 0 && t.dwellTimeMs < filter.minDwellMs) return false;
879
+ return true;
1109
880
  });
1110
- var __importStar = exports && exports.__importStar || /* @__PURE__ */ (function() {
1111
- var ownKeys = function(o) {
1112
- ownKeys = Object.getOwnPropertyNames || function(o2) {
1113
- var ar = [];
1114
- for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
1115
- return ar;
1116
- };
1117
- return ownKeys(o);
1118
- };
1119
- return function(mod) {
1120
- if (mod && mod.__esModule) return mod;
1121
- var result = {};
1122
- if (mod != null) {
1123
- for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
1124
- }
1125
- __setModuleDefault(result, mod);
1126
- return result;
1127
- };
1128
- })();
1129
- Object.defineProperty(exports, "__esModule", { value: true });
1130
- exports.SnapshotManager = exports.DEFAULT_SNAPSHOT_CONFIG = void 0;
1131
- exports.DEFAULT_SNAPSHOT_CONFIG = {
1132
- enabled: false,
1133
- saveThumbnail: true,
1134
- saveAnnotatedFrame: false,
1135
- saveDebugThumbnails: false
881
+ }
882
+ getZoneState(_deviceId, zoneId) {
883
+ return this.lastLiveState?.zones.find((z) => z.zoneId === zoneId) ?? null;
884
+ }
885
+ async getZoneHistory(deviceId, zoneId, options) {
886
+ if (this.getZoneHistoryCb) {
887
+ return this.getZoneHistoryCb(deviceId, zoneId, options);
888
+ }
889
+ return [];
890
+ }
891
+ async getHeatmap(deviceId, options) {
892
+ if (this.getHeatmapCb) {
893
+ return this.getHeatmapCb(deviceId, options);
894
+ }
895
+ const gridSize = options.resolution;
896
+ return {
897
+ width: 1920,
898
+ height: 1080,
899
+ gridSize,
900
+ cells: new Float32Array(gridSize * gridSize),
901
+ maxCount: 0
1136
902
  };
1137
- var SnapshotManager2 = class {
1138
- config;
1139
- constructor(config) {
1140
- this.config = config;
1141
- }
1142
- async capture(frame, detection, allDetections, eventId) {
1143
- if (!this.config.enabled)
1144
- return void 0;
1145
- await this.ensureOutputDir();
1146
- let thumbnailPath;
1147
- let annotatedFramePath;
1148
- if (this.config.saveThumbnail) {
1149
- thumbnailPath = await this.saveThumbnail(frame, detection.bbox, eventId);
1150
- }
1151
- if (this.config.saveAnnotatedFrame) {
1152
- annotatedFramePath = await this.saveAnnotatedFrame(frame, allDetections, eventId);
1153
- }
1154
- if (!thumbnailPath && !annotatedFramePath)
1155
- return void 0;
1156
- return {
1157
- thumbnailPath: thumbnailPath ?? "",
1158
- annotatedFramePath: annotatedFramePath ?? ""
1159
- };
1160
- }
1161
- async saveThumbnail(frame, bbox, eventId) {
1162
- const sharp = await Promise.resolve().then(() => __importStar(__require("sharp")));
1163
- const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
1164
- const left = Math.max(0, Math.round(bbox.x));
1165
- const top = Math.max(0, Math.round(bbox.y));
1166
- const width = Math.min(Math.round(bbox.w), frameWidth - left);
1167
- const height = Math.min(Math.round(bbox.h), frameHeight - top);
1168
- if (width <= 0 || height <= 0) {
1169
- throw new Error(`Invalid crop dimensions for event ${eventId}: ${JSON.stringify({ left, top, width, height, frameWidth, frameHeight })}`);
1170
- }
1171
- const relativePath = `${eventId}_thumb.jpg`;
1172
- const inputOptions = this.sharpInputOptions(frame);
1173
- const buffer = await sharp.default(frame.data, inputOptions).extract({ left, top, width, height }).jpeg({ quality: 85 }).toBuffer();
1174
- await this.writeOutput(relativePath, buffer);
1175
- return relativePath;
1176
- }
1177
- async saveAnnotatedFrame(frame, detections, eventId) {
1178
- const sharp = await Promise.resolve().then(() => __importStar(__require("sharp")));
1179
- const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
1180
- const relativePath = `${eventId}_annotated.jpg`;
1181
- const svgBoxes = detections.map((det) => {
1182
- const x = Math.max(0, Math.round(det.bbox.x));
1183
- const y = Math.max(0, Math.round(det.bbox.y));
1184
- const w = Math.min(Math.round(det.bbox.w), frameWidth - x);
1185
- const h = Math.min(Math.round(det.bbox.h), frameHeight - y);
1186
- const label = `${det.class} ${(det.score * 100).toFixed(0)}%`;
1187
- return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="lime" stroke-width="2"/>
903
+ }
904
+ getTrackDetail(_deviceId, trackId) {
905
+ return this.trackStore.getTrackDetail(trackId);
906
+ }
907
+ getCameraStatus(_deviceId) {
908
+ return this.lastLiveState ? [...this.lastLiveState.pipelineStatus] : [];
909
+ }
910
+ };
911
+
912
+ // src/snapshots/snapshot-manager.ts
913
+ var DEFAULT_SNAPSHOT_CONFIG = {
914
+ enabled: false,
915
+ saveThumbnail: true,
916
+ saveAnnotatedFrame: false,
917
+ saveDebugThumbnails: false
918
+ };
919
+ var SnapshotManager = class {
920
+ constructor(config) {
921
+ this.config = config;
922
+ }
923
+ async capture(frame, detection, allDetections, eventId) {
924
+ if (!this.config.enabled) return void 0;
925
+ await this.ensureOutputDir();
926
+ let thumbnailPath;
927
+ let annotatedFramePath;
928
+ if (this.config.saveThumbnail) {
929
+ thumbnailPath = await this.saveThumbnail(frame, detection.bbox, eventId);
930
+ }
931
+ if (this.config.saveAnnotatedFrame) {
932
+ annotatedFramePath = await this.saveAnnotatedFrame(frame, allDetections, eventId);
933
+ }
934
+ if (!thumbnailPath && !annotatedFramePath) return void 0;
935
+ return {
936
+ thumbnailPath: thumbnailPath ?? "",
937
+ annotatedFramePath: annotatedFramePath ?? ""
938
+ };
939
+ }
940
+ async saveThumbnail(frame, bbox, eventId) {
941
+ const sharp = await import("sharp");
942
+ const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
943
+ const left = Math.max(0, Math.round(bbox.x));
944
+ const top = Math.max(0, Math.round(bbox.y));
945
+ const width = Math.min(Math.round(bbox.w), frameWidth - left);
946
+ const height = Math.min(Math.round(bbox.h), frameHeight - top);
947
+ if (width <= 0 || height <= 0) {
948
+ throw new Error(`Invalid crop dimensions for event ${eventId}: ${JSON.stringify({ left, top, width, height, frameWidth, frameHeight })}`);
949
+ }
950
+ const relativePath = `${eventId}_thumb.jpg`;
951
+ const inputOptions = this.sharpInputOptions(frame);
952
+ const buffer = await sharp.default(frame.data, inputOptions).extract({ left, top, width, height }).jpeg({ quality: 85 }).toBuffer();
953
+ await this.writeOutput(relativePath, buffer);
954
+ return relativePath;
955
+ }
956
+ async saveAnnotatedFrame(frame, detections, eventId) {
957
+ const sharp = await import("sharp");
958
+ const { frameWidth, frameHeight } = await this.resolveFrameDimensions(frame);
959
+ const relativePath = `${eventId}_annotated.jpg`;
960
+ const svgBoxes = detections.map((det) => {
961
+ const x = Math.max(0, Math.round(det.bbox.x));
962
+ const y = Math.max(0, Math.round(det.bbox.y));
963
+ const w = Math.min(Math.round(det.bbox.w), frameWidth - x);
964
+ const h = Math.min(Math.round(det.bbox.h), frameHeight - y);
965
+ const label = `${det.class} ${(det.score * 100).toFixed(0)}%`;
966
+ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="lime" stroke-width="2"/>
1188
967
  <text x="${x + 2}" y="${Math.max(y - 2, 10)}" font-size="12" fill="lime" font-family="sans-serif">${label}</text>`;
1189
- }).join("\n");
1190
- const svgOverlay = Buffer.from(`<svg width="${frameWidth}" height="${frameHeight}" xmlns="http://www.w3.org/2000/svg">${svgBoxes}</svg>`);
1191
- const inputOptions = this.sharpInputOptions(frame);
1192
- const buffer = await sharp.default(frame.data, inputOptions).composite([{ input: svgOverlay, top: 0, left: 0 }]).jpeg({ quality: 85 }).toBuffer();
1193
- await this.writeOutput(relativePath, buffer);
1194
- return relativePath;
1195
- }
1196
- /**
1197
- * Write output using mediaStorage if available, otherwise fall back to fs.writeFile.
1198
- */
1199
- async writeOutput(relativePath, data) {
1200
- if (this.config.mediaStorage) {
1201
- await this.config.mediaStorage.writeFile(relativePath, data);
1202
- } else if (this.config.outputDir) {
1203
- const { writeFile } = await Promise.resolve().then(() => __importStar(__require("fs/promises")));
1204
- const { join } = await Promise.resolve().then(() => __importStar(__require("path")));
1205
- await writeFile(join(this.config.outputDir, relativePath), data);
1206
- }
1207
- }
1208
- /**
1209
- * Resolve actual frame dimensions. JPEG frames from decoders may report
1210
- * width=0, height=0 — in that case read the real size from sharp metadata.
1211
- */
1212
- async resolveFrameDimensions(frame) {
1213
- if (frame.width > 0 && frame.height > 0) {
1214
- return { frameWidth: frame.width, frameHeight: frame.height };
1215
- }
1216
- if (frame.format === "jpeg") {
1217
- const sharp = await Promise.resolve().then(() => __importStar(__require("sharp")));
1218
- const meta = await sharp.default(frame.data).metadata();
1219
- return {
1220
- frameWidth: meta.width ?? 0,
1221
- frameHeight: meta.height ?? 0
1222
- };
1223
- }
1224
- return { frameWidth: frame.width, frameHeight: frame.height };
1225
- }
1226
- sharpInputOptions(frame) {
1227
- if (frame.format === "jpeg") {
1228
- return {};
1229
- }
1230
- const channels = frame.format === "rgb" ? 3 : 3;
1231
- return {
1232
- raw: {
1233
- width: frame.width,
1234
- height: frame.height,
1235
- channels
1236
- }
1237
- };
1238
- }
1239
- async ensureOutputDir() {
1240
- if (this.config.outputDir) {
1241
- const { mkdir } = await Promise.resolve().then(() => __importStar(__require("fs/promises")));
1242
- await mkdir(this.config.outputDir, { recursive: true });
1243
- }
968
+ }).join("\n");
969
+ const svgOverlay = Buffer.from(
970
+ `<svg width="${frameWidth}" height="${frameHeight}" xmlns="http://www.w3.org/2000/svg">${svgBoxes}</svg>`
971
+ );
972
+ const inputOptions = this.sharpInputOptions(frame);
973
+ const buffer = await sharp.default(frame.data, inputOptions).composite([{ input: svgOverlay, top: 0, left: 0 }]).jpeg({ quality: 85 }).toBuffer();
974
+ await this.writeOutput(relativePath, buffer);
975
+ return relativePath;
976
+ }
977
+ /**
978
+ * Write output using mediaStorage if available, otherwise fall back to fs.writeFile.
979
+ */
980
+ async writeOutput(relativePath, data) {
981
+ if (this.config.mediaStorage) {
982
+ await this.config.mediaStorage.writeFile(relativePath, data);
983
+ } else if (this.config.outputDir) {
984
+ const { writeFile } = await import("fs/promises");
985
+ const { join } = await import("path");
986
+ await writeFile(join(this.config.outputDir, relativePath), data);
987
+ }
988
+ }
989
+ /**
990
+ * Resolve actual frame dimensions. JPEG frames from decoders may report
991
+ * width=0, height=0 — in that case read the real size from sharp metadata.
992
+ */
993
+ async resolveFrameDimensions(frame) {
994
+ if (frame.width > 0 && frame.height > 0) {
995
+ return { frameWidth: frame.width, frameHeight: frame.height };
996
+ }
997
+ if (frame.format === "jpeg") {
998
+ const sharp = await import("sharp");
999
+ const meta = await sharp.default(frame.data).metadata();
1000
+ return {
1001
+ frameWidth: meta.width ?? 0,
1002
+ frameHeight: meta.height ?? 0
1003
+ };
1004
+ }
1005
+ return { frameWidth: frame.width, frameHeight: frame.height };
1006
+ }
1007
+ sharpInputOptions(frame) {
1008
+ if (frame.format === "jpeg") {
1009
+ return {};
1010
+ }
1011
+ const channels = frame.format === "rgb" ? 3 : 3;
1012
+ return {
1013
+ raw: {
1014
+ width: frame.width,
1015
+ height: frame.height,
1016
+ channels
1244
1017
  }
1245
1018
  };
1246
- exports.SnapshotManager = SnapshotManager2;
1247
1019
  }
1248
- });
1020
+ async ensureOutputDir() {
1021
+ if (this.config.outputDir) {
1022
+ const { mkdir } = await import("fs/promises");
1023
+ await mkdir(this.config.outputDir, { recursive: true });
1024
+ }
1025
+ }
1026
+ };
1249
1027
 
1250
- // src/pipeline/analysis-pipeline.js
1251
- var require_analysis_pipeline = __commonJS({
1252
- "src/pipeline/analysis-pipeline.js"(exports) {
1253
- "use strict";
1254
- Object.defineProperty(exports, "__esModule", { value: true });
1255
- exports.AnalysisPipeline = void 0;
1256
- var sort_tracker_js_1 = require_sort_tracker();
1257
- var state_analyzer_js_1 = require_state_analyzer();
1258
- var zone_evaluator_js_1 = require_zone_evaluator();
1259
- var event_emitter_js_1 = require_event_emitter();
1260
- var track_store_js_1 = require_track_store();
1261
- var live_state_manager_js_1 = require_live_state_manager();
1262
- var heatmap_aggregator_js_1 = require_heatmap_aggregator();
1263
- var analytics_provider_js_1 = require_analytics_provider();
1264
- var snapshot_manager_js_1 = require_snapshot_manager();
1265
- var AnalysisPipeline2 = class {
1266
- id = "pipeline-analysis";
1267
- manifest = {
1268
- id: "pipeline-analysis",
1269
- name: "Pipeline Analysis",
1270
- version: "0.1.0",
1271
- description: "Object tracking, state analysis, zone evaluation, and event emission",
1272
- packageName: "@camstack/lib-pipeline-analysis"
1273
- };
1274
- config;
1275
- cameras = /* @__PURE__ */ new Map();
1276
- knownFaces = [];
1277
- knownPlates = [];
1278
- listeners = /* @__PURE__ */ new Map();
1279
- /** Optional callback: server orchestrator can hook in to persist finished tracks */
1280
- onTrackFinished;
1281
- constructor(config = {}) {
1282
- this.config = {
1283
- tracker: config.tracker ?? {},
1284
- stateAnalyzer: config.stateAnalyzer ?? {},
1285
- eventFilter: config.eventFilter ?? {},
1286
- snapshot: config.snapshot ?? {},
1287
- heatmapGridSize: config.heatmapGridSize ?? 32,
1288
- defaultFrameWidth: config.defaultFrameWidth ?? 1920,
1289
- defaultFrameHeight: config.defaultFrameHeight ?? 1080
1290
- };
1291
- }
1292
- async initialize(_ctx) {
1293
- }
1294
- async shutdown() {
1295
- this.cameras.clear();
1296
- this.listeners.clear();
1297
- }
1298
- async processFrame(deviceId, frame, pipelineResult) {
1299
- const camera = this.getOrCreateCamera(deviceId);
1300
- const frameWidth = frame.width > 0 ? frame.width : this.config.defaultFrameWidth;
1301
- const frameHeight = frame.height > 0 ? frame.height : this.config.defaultFrameHeight;
1302
- const timestamp = frame.timestamp;
1303
- const detections = pipelineResult ? this.extractDetections(pipelineResult) : [];
1304
- const tracked = camera.tracker.update(detections, timestamp);
1305
- const states = camera.stateAnalyzer.analyze(tracked, timestamp);
1306
- const zoneEvents = camera.zoneEvaluator.evaluate(tracked, camera.zones, frameWidth, frameHeight, timestamp);
1307
- const classificationsByTrack = pipelineResult ? this.matchClassifications(pipelineResult, tracked.map((t) => t.trackId)) : [];
1308
- camera.trackStore.update(tracked, states, zoneEvents, classificationsByTrack);
1309
- const events = camera.eventEmitter.emit(tracked, states, zoneEvents, classificationsByTrack, deviceId);
1310
- for (const event of events) {
1311
- const snapshot = await camera.snapshotManager.capture(frame, event.detection, tracked, event.id);
1312
- if (snapshot) {
1313
- ;
1314
- event.snapshot = snapshot;
1315
- }
1316
- camera.trackStore.addEvent(event.detection.trackId, event);
1317
- }
1318
- for (const track of tracked) {
1319
- const cx = (track.bbox.x + track.bbox.w / 2) / frameWidth;
1320
- const cy = (track.bbox.y + track.bbox.h / 2) / frameHeight;
1321
- camera.heatmapAggregator.addPoint(cx, cy);
1322
- }
1323
- for (const lostTrack of camera.tracker.getLostTracks()) {
1324
- const detail = camera.trackStore.finishTrack(lostTrack.trackId);
1325
- if (detail && this.onTrackFinished) {
1326
- this.onTrackFinished(deviceId, detail);
1327
- }
1328
- }
1329
- const pipelineMs = pipelineResult?.totalMs ?? 0;
1330
- const pipelineStatus = [];
1331
- const liveState = camera.liveStateManager.buildState(deviceId, tracked, states, camera.zoneEvaluator.getTrackZones(), pipelineStatus, pipelineMs, events[events.length - 1], timestamp);
1332
- camera.lastLiveState = liveState;
1333
- camera.analyticsProvider.updateLiveState(liveState);
1334
- const cameraListeners = this.listeners.get(deviceId);
1335
- if (cameraListeners) {
1336
- for (const cb of cameraListeners) {
1337
- cb(liveState);
1338
- }
1339
- }
1340
- return events;
1341
- }
1342
- setCameraPipeline(deviceId, _config) {
1343
- this.getOrCreateCamera(deviceId);
1344
- }
1345
- setCameraZones(deviceId, zones) {
1346
- const camera = this.getOrCreateCamera(deviceId);
1347
- camera.zones = [...zones];
1348
- camera.liveStateManager.setZones(zones);
1349
- }
1350
- setKnownFaces(faces) {
1351
- this.knownFaces = [...faces];
1352
- }
1353
- setKnownPlates(plates) {
1354
- this.knownPlates = [...plates];
1355
- }
1356
- onLiveStateChange(deviceId, callback) {
1357
- if (!this.listeners.has(deviceId)) {
1358
- this.listeners.set(deviceId, /* @__PURE__ */ new Set());
1359
- }
1360
- this.listeners.get(deviceId).add(callback);
1361
- return () => {
1362
- this.listeners.get(deviceId)?.delete(callback);
1363
- };
1364
- }
1365
- // ICameraAnalyticsProvider delegates to per-camera AnalyticsProvider
1366
- getLiveState(deviceId) {
1367
- return this.cameras.get(deviceId)?.lastLiveState ?? null;
1368
- }
1369
- getTracks(deviceId, filter) {
1370
- const camera = this.cameras.get(deviceId);
1371
- if (!camera)
1372
- return [];
1373
- return camera.analyticsProvider.getTracks(deviceId, filter);
1374
- }
1375
- getZoneState(deviceId, zoneId) {
1376
- const camera = this.cameras.get(deviceId);
1377
- if (!camera)
1378
- return null;
1379
- return camera.analyticsProvider.getZoneState(deviceId, zoneId);
1380
- }
1381
- async getZoneHistory(deviceId, zoneId, options) {
1382
- const camera = this.cameras.get(deviceId);
1383
- if (!camera)
1384
- return [];
1385
- return camera.analyticsProvider.getZoneHistory(deviceId, zoneId, options);
1386
- }
1387
- async getHeatmap(deviceId, options) {
1388
- const camera = this.cameras.get(deviceId);
1389
- if (!camera) {
1390
- const gridSize = options.resolution;
1391
- return {
1392
- width: this.config.defaultFrameWidth,
1393
- height: this.config.defaultFrameHeight,
1394
- gridSize,
1395
- cells: new Float32Array(gridSize * gridSize),
1396
- maxCount: 0
1397
- };
1398
- }
1399
- return camera.heatmapAggregator.getHeatmap();
1400
- }
1401
- getTrackDetail(deviceId, trackId) {
1402
- const camera = this.cameras.get(deviceId);
1403
- if (!camera)
1404
- return null;
1405
- return camera.trackStore.getTrackDetail(trackId);
1406
- }
1407
- getCameraStatus(deviceId) {
1408
- const camera = this.cameras.get(deviceId);
1409
- if (!camera)
1410
- return [];
1411
- return camera.analyticsProvider.getCameraStatus(deviceId);
1028
+ // src/pipeline/analysis-pipeline.ts
1029
+ var AnalysisPipeline = class {
1030
+ id = "pipeline-analysis";
1031
+ manifest = {
1032
+ id: "pipeline-analysis",
1033
+ name: "Pipeline Analysis",
1034
+ version: "0.1.0",
1035
+ description: "Object tracking, state analysis, zone evaluation, and event emission",
1036
+ packageName: "@camstack/lib-pipeline-analysis"
1037
+ };
1038
+ config;
1039
+ cameras = /* @__PURE__ */ new Map();
1040
+ knownFaces = [];
1041
+ knownPlates = [];
1042
+ listeners = /* @__PURE__ */ new Map();
1043
+ /** Optional callback: server orchestrator can hook in to persist finished tracks */
1044
+ onTrackFinished;
1045
+ constructor(config = {}) {
1046
+ this.config = {
1047
+ tracker: config.tracker ?? {},
1048
+ stateAnalyzer: config.stateAnalyzer ?? {},
1049
+ eventFilter: config.eventFilter ?? {},
1050
+ snapshot: config.snapshot ?? {},
1051
+ heatmapGridSize: config.heatmapGridSize ?? 32,
1052
+ defaultFrameWidth: config.defaultFrameWidth ?? 1920,
1053
+ defaultFrameHeight: config.defaultFrameHeight ?? 1080
1054
+ };
1055
+ }
1056
+ async initialize(_ctx) {
1057
+ }
1058
+ async shutdown() {
1059
+ this.cameras.clear();
1060
+ this.listeners.clear();
1061
+ }
1062
+ async processFrame(deviceId, frame, pipelineResult) {
1063
+ const camera = this.getOrCreateCamera(deviceId);
1064
+ const frameWidth = frame.width > 0 ? frame.width : this.config.defaultFrameWidth;
1065
+ const frameHeight = frame.height > 0 ? frame.height : this.config.defaultFrameHeight;
1066
+ const timestamp = frame.timestamp;
1067
+ const detections = pipelineResult ? this.extractDetections(pipelineResult) : [];
1068
+ const tracked = camera.tracker.update(detections, timestamp);
1069
+ const states = camera.stateAnalyzer.analyze(tracked, timestamp);
1070
+ const zoneEvents = camera.zoneEvaluator.evaluate(
1071
+ tracked,
1072
+ camera.zones,
1073
+ frameWidth,
1074
+ frameHeight,
1075
+ timestamp
1076
+ );
1077
+ const classificationsByTrack = pipelineResult ? this.matchClassifications(pipelineResult, tracked.map((t) => t.trackId)) : [];
1078
+ camera.trackStore.update(tracked, states, zoneEvents, classificationsByTrack);
1079
+ const events = camera.eventEmitter.emit(
1080
+ tracked,
1081
+ states,
1082
+ zoneEvents,
1083
+ classificationsByTrack,
1084
+ deviceId
1085
+ );
1086
+ for (const event of events) {
1087
+ const snapshot = await camera.snapshotManager.capture(frame, event.detection, tracked, event.id);
1088
+ if (snapshot) {
1089
+ ;
1090
+ event.snapshot = snapshot;
1091
+ }
1092
+ camera.trackStore.addEvent(event.detection.trackId, event);
1093
+ }
1094
+ for (const track of tracked) {
1095
+ const cx = (track.bbox.x + track.bbox.w / 2) / frameWidth;
1096
+ const cy = (track.bbox.y + track.bbox.h / 2) / frameHeight;
1097
+ camera.heatmapAggregator.addPoint(cx, cy);
1098
+ }
1099
+ for (const lostTrack of camera.tracker.getLostTracks()) {
1100
+ const detail = camera.trackStore.finishTrack(lostTrack.trackId);
1101
+ if (detail && this.onTrackFinished) {
1102
+ this.onTrackFinished(deviceId, detail);
1412
1103
  }
1413
- getOrCreateCamera(deviceId) {
1414
- const existing = this.cameras.get(deviceId);
1415
- if (existing)
1416
- return existing;
1417
- const tracker = new sort_tracker_js_1.SortTracker(this.config.tracker);
1418
- const stateAnalyzer = new state_analyzer_js_1.StateAnalyzer(this.config.stateAnalyzer);
1419
- const zoneEvaluator = new zone_evaluator_js_1.ZoneEvaluator();
1420
- const eventEmitter = new event_emitter_js_1.DetectionEventEmitter(this.config.eventFilter);
1421
- const trackStore = new track_store_js_1.TrackStore();
1422
- const liveStateManager = new live_state_manager_js_1.LiveStateManager();
1423
- const heatmapAggregator = new heatmap_aggregator_js_1.HeatmapAggregator(this.config.defaultFrameWidth, this.config.defaultFrameHeight, this.config.heatmapGridSize);
1424
- const analyticsProvider = new analytics_provider_js_1.AnalyticsProvider(liveStateManager, trackStore);
1425
- const snapshotManager = new snapshot_manager_js_1.SnapshotManager({
1426
- ...snapshot_manager_js_1.DEFAULT_SNAPSHOT_CONFIG,
1427
- ...this.config.snapshot
1428
- });
1429
- const cameraState = {
1430
- tracker,
1431
- stateAnalyzer,
1432
- zoneEvaluator,
1433
- eventEmitter,
1434
- trackStore,
1435
- liveStateManager,
1436
- heatmapAggregator,
1437
- analyticsProvider,
1438
- snapshotManager,
1439
- zones: [],
1440
- lastLiveState: null
1441
- };
1442
- this.cameras.set(deviceId, cameraState);
1443
- return cameraState;
1104
+ }
1105
+ const pipelineMs = pipelineResult?.totalMs ?? 0;
1106
+ const pipelineStatus = [];
1107
+ const liveState = camera.liveStateManager.buildState(
1108
+ deviceId,
1109
+ tracked,
1110
+ states,
1111
+ camera.zoneEvaluator.getTrackZones(),
1112
+ pipelineStatus,
1113
+ pipelineMs,
1114
+ events[events.length - 1],
1115
+ timestamp
1116
+ );
1117
+ camera.lastLiveState = liveState;
1118
+ camera.analyticsProvider.updateLiveState(liveState);
1119
+ const cameraListeners = this.listeners.get(deviceId);
1120
+ if (cameraListeners) {
1121
+ for (const cb of cameraListeners) {
1122
+ cb(liveState);
1444
1123
  }
1445
- extractDetections(pipelineResult) {
1446
- const detections = [];
1447
- for (const stepResult of pipelineResult.results) {
1448
- if (stepResult.slot === "detector") {
1449
- const output = stepResult.output;
1450
- if (output.detections) {
1451
- detections.push(...output.detections);
1452
- }
1453
- }
1124
+ }
1125
+ return events;
1126
+ }
1127
+ setCameraPipeline(deviceId, _config) {
1128
+ this.getOrCreateCamera(deviceId);
1129
+ }
1130
+ setCameraZones(deviceId, zones) {
1131
+ const camera = this.getOrCreateCamera(deviceId);
1132
+ camera.zones = [...zones];
1133
+ camera.liveStateManager.setZones(zones);
1134
+ }
1135
+ setKnownFaces(faces) {
1136
+ this.knownFaces = [...faces];
1137
+ }
1138
+ setKnownPlates(plates) {
1139
+ this.knownPlates = [...plates];
1140
+ }
1141
+ onLiveStateChange(deviceId, callback) {
1142
+ if (!this.listeners.has(deviceId)) {
1143
+ this.listeners.set(deviceId, /* @__PURE__ */ new Set());
1144
+ }
1145
+ this.listeners.get(deviceId).add(callback);
1146
+ return () => {
1147
+ this.listeners.get(deviceId)?.delete(callback);
1148
+ };
1149
+ }
1150
+ // ICameraAnalyticsProvider delegates to per-camera AnalyticsProvider
1151
+ getLiveState(deviceId) {
1152
+ return this.cameras.get(deviceId)?.lastLiveState ?? null;
1153
+ }
1154
+ getTracks(deviceId, filter) {
1155
+ const camera = this.cameras.get(deviceId);
1156
+ if (!camera) return [];
1157
+ return camera.analyticsProvider.getTracks(deviceId, filter);
1158
+ }
1159
+ getZoneState(deviceId, zoneId) {
1160
+ const camera = this.cameras.get(deviceId);
1161
+ if (!camera) return null;
1162
+ return camera.analyticsProvider.getZoneState(deviceId, zoneId);
1163
+ }
1164
+ async getZoneHistory(deviceId, zoneId, options) {
1165
+ const camera = this.cameras.get(deviceId);
1166
+ if (!camera) return [];
1167
+ return camera.analyticsProvider.getZoneHistory(deviceId, zoneId, options);
1168
+ }
1169
+ async getHeatmap(deviceId, options) {
1170
+ const camera = this.cameras.get(deviceId);
1171
+ if (!camera) {
1172
+ const gridSize = options.resolution;
1173
+ return {
1174
+ width: this.config.defaultFrameWidth,
1175
+ height: this.config.defaultFrameHeight,
1176
+ gridSize,
1177
+ cells: new Float32Array(gridSize * gridSize),
1178
+ maxCount: 0
1179
+ };
1180
+ }
1181
+ return camera.heatmapAggregator.getHeatmap();
1182
+ }
1183
+ getTrackDetail(deviceId, trackId) {
1184
+ const camera = this.cameras.get(deviceId);
1185
+ if (!camera) return null;
1186
+ return camera.trackStore.getTrackDetail(trackId);
1187
+ }
1188
+ getCameraStatus(deviceId) {
1189
+ const camera = this.cameras.get(deviceId);
1190
+ if (!camera) return [];
1191
+ return camera.analyticsProvider.getCameraStatus(deviceId);
1192
+ }
1193
+ getOrCreateCamera(deviceId) {
1194
+ const existing = this.cameras.get(deviceId);
1195
+ if (existing) return existing;
1196
+ const tracker = new SortTracker(this.config.tracker);
1197
+ const stateAnalyzer = new StateAnalyzer(this.config.stateAnalyzer);
1198
+ const zoneEvaluator = new ZoneEvaluator();
1199
+ const eventEmitter = new DetectionEventEmitter(this.config.eventFilter);
1200
+ const trackStore = new TrackStore();
1201
+ const liveStateManager = new LiveStateManager();
1202
+ const heatmapAggregator = new HeatmapAggregator(
1203
+ this.config.defaultFrameWidth,
1204
+ this.config.defaultFrameHeight,
1205
+ this.config.heatmapGridSize
1206
+ );
1207
+ const analyticsProvider = new AnalyticsProvider(liveStateManager, trackStore);
1208
+ const snapshotManager = new SnapshotManager({
1209
+ ...DEFAULT_SNAPSHOT_CONFIG,
1210
+ ...this.config.snapshot
1211
+ });
1212
+ const cameraState = {
1213
+ tracker,
1214
+ stateAnalyzer,
1215
+ zoneEvaluator,
1216
+ eventEmitter,
1217
+ trackStore,
1218
+ liveStateManager,
1219
+ heatmapAggregator,
1220
+ analyticsProvider,
1221
+ snapshotManager,
1222
+ zones: [],
1223
+ lastLiveState: null
1224
+ };
1225
+ this.cameras.set(deviceId, cameraState);
1226
+ return cameraState;
1227
+ }
1228
+ extractDetections(pipelineResult) {
1229
+ const detections = [];
1230
+ for (const stepResult of pipelineResult.results) {
1231
+ if (stepResult.slot === "detector") {
1232
+ const output = stepResult.output;
1233
+ if (output.detections) {
1234
+ detections.push(...output.detections);
1454
1235
  }
1455
- return detections;
1456
1236
  }
1457
- matchClassifications(pipelineResult, trackIds) {
1458
- const classifierResults = [];
1459
- for (const stepResult of pipelineResult.results) {
1460
- if (stepResult.slot === "classifier") {
1461
- const output = stepResult.output;
1462
- if (output.classifications) {
1463
- classifierResults.push([...output.classifications]);
1464
- }
1465
- }
1237
+ }
1238
+ return detections;
1239
+ }
1240
+ matchClassifications(pipelineResult, trackIds) {
1241
+ const classifierResults = [];
1242
+ for (const stepResult of pipelineResult.results) {
1243
+ if (stepResult.slot === "classifier") {
1244
+ const output = stepResult.output;
1245
+ if (output.classifications) {
1246
+ classifierResults.push([...output.classifications]);
1466
1247
  }
1467
- return trackIds.slice(0, classifierResults.length).map((trackId, i) => ({
1468
- trackId,
1469
- classifications: classifierResults[i] ?? []
1470
- }));
1471
1248
  }
1472
- };
1473
- exports.AnalysisPipeline = AnalysisPipeline2;
1249
+ }
1250
+ return trackIds.slice(0, classifierResults.length).map((trackId, i) => ({
1251
+ trackId,
1252
+ classifications: classifierResults[i] ?? []
1253
+ }));
1474
1254
  }
1475
- });
1476
-
1477
- // src/index.ts
1478
- var src_exports = {};
1479
- __export(src_exports, {
1480
- AnalysisPipeline: () => import_analysis_pipeline.AnalysisPipeline,
1481
- AnalyticsProvider: () => import_analytics_provider.AnalyticsProvider,
1482
- DEFAULT_EVENT_FILTER_CONFIG: () => import_event_filters.DEFAULT_EVENT_FILTER_CONFIG,
1483
- DEFAULT_SNAPSHOT_CONFIG: () => import_snapshot_manager.DEFAULT_SNAPSHOT_CONFIG,
1484
- DEFAULT_STATE_ANALYZER_CONFIG: () => import_state_analyzer.DEFAULT_STATE_ANALYZER_CONFIG,
1485
- DEFAULT_TRACKER_CONFIG: () => import_sort_tracker.DEFAULT_TRACKER_CONFIG,
1486
- DetectionEventEmitter: () => import_event_emitter.DetectionEventEmitter,
1487
- EventFilter: () => import_event_filters.EventFilter,
1488
- HeatmapAggregator: () => import_heatmap_aggregator.HeatmapAggregator,
1489
- LiveStateManager: () => import_live_state_manager.LiveStateManager,
1490
- SnapshotManager: () => import_snapshot_manager.SnapshotManager,
1491
- SortTracker: () => import_sort_tracker.SortTracker,
1492
- StateAnalyzer: () => import_state_analyzer.StateAnalyzer,
1493
- TrackStore: () => import_track_store.TrackStore,
1494
- ZoneEvaluator: () => import_zone_evaluator.ZoneEvaluator,
1495
- greedyAssignment: () => import_hungarian.greedyAssignment
1496
- });
1497
- __reExport(src_exports, __toESM(require_geometry()));
1498
- var import_sort_tracker = __toESM(require_sort_tracker());
1499
- var import_hungarian = __toESM(require_hungarian());
1500
- var import_state_analyzer = __toESM(require_state_analyzer());
1501
- var import_zone_evaluator = __toESM(require_zone_evaluator());
1502
- var import_event_filters = __toESM(require_event_filters());
1503
- var import_event_emitter = __toESM(require_event_emitter());
1504
- var import_track_store = __toESM(require_track_store());
1505
- var import_live_state_manager = __toESM(require_live_state_manager());
1506
- var import_heatmap_aggregator = __toESM(require_heatmap_aggregator());
1507
- var import_analytics_provider = __toESM(require_analytics_provider());
1508
- var import_snapshot_manager = __toESM(require_snapshot_manager());
1509
- var import_analysis_pipeline = __toESM(require_analysis_pipeline());
1510
- var export_AnalysisPipeline = import_analysis_pipeline.AnalysisPipeline;
1511
- var export_AnalyticsProvider = import_analytics_provider.AnalyticsProvider;
1512
- var export_DEFAULT_EVENT_FILTER_CONFIG = import_event_filters.DEFAULT_EVENT_FILTER_CONFIG;
1513
- var export_DEFAULT_SNAPSHOT_CONFIG = import_snapshot_manager.DEFAULT_SNAPSHOT_CONFIG;
1514
- var export_DEFAULT_STATE_ANALYZER_CONFIG = import_state_analyzer.DEFAULT_STATE_ANALYZER_CONFIG;
1515
- var export_DEFAULT_TRACKER_CONFIG = import_sort_tracker.DEFAULT_TRACKER_CONFIG;
1516
- var export_DetectionEventEmitter = import_event_emitter.DetectionEventEmitter;
1517
- var export_EventFilter = import_event_filters.EventFilter;
1518
- var export_HeatmapAggregator = import_heatmap_aggregator.HeatmapAggregator;
1519
- var export_LiveStateManager = import_live_state_manager.LiveStateManager;
1520
- var export_SnapshotManager = import_snapshot_manager.SnapshotManager;
1521
- var export_SortTracker = import_sort_tracker.SortTracker;
1522
- var export_StateAnalyzer = import_state_analyzer.StateAnalyzer;
1523
- var export_TrackStore = import_track_store.TrackStore;
1524
- var export_ZoneEvaluator = import_zone_evaluator.ZoneEvaluator;
1525
- var export_greedyAssignment = import_hungarian.greedyAssignment;
1255
+ };
1526
1256
  export {
1527
- export_AnalysisPipeline as AnalysisPipeline,
1528
- export_AnalyticsProvider as AnalyticsProvider,
1529
- export_DEFAULT_EVENT_FILTER_CONFIG as DEFAULT_EVENT_FILTER_CONFIG,
1530
- export_DEFAULT_SNAPSHOT_CONFIG as DEFAULT_SNAPSHOT_CONFIG,
1531
- export_DEFAULT_STATE_ANALYZER_CONFIG as DEFAULT_STATE_ANALYZER_CONFIG,
1532
- export_DEFAULT_TRACKER_CONFIG as DEFAULT_TRACKER_CONFIG,
1533
- export_DetectionEventEmitter as DetectionEventEmitter,
1534
- export_EventFilter as EventFilter,
1535
- export_HeatmapAggregator as HeatmapAggregator,
1536
- export_LiveStateManager as LiveStateManager,
1537
- export_SnapshotManager as SnapshotManager,
1538
- export_SortTracker as SortTracker,
1539
- export_StateAnalyzer as StateAnalyzer,
1540
- export_TrackStore as TrackStore,
1541
- export_ZoneEvaluator as ZoneEvaluator,
1542
- export_greedyAssignment as greedyAssignment
1257
+ AnalysisPipeline,
1258
+ AnalyticsProvider,
1259
+ DEFAULT_EVENT_FILTER_CONFIG,
1260
+ DEFAULT_SNAPSHOT_CONFIG,
1261
+ DEFAULT_STATE_ANALYZER_CONFIG,
1262
+ DEFAULT_TRACKER_CONFIG,
1263
+ DetectionEventEmitter,
1264
+ EventFilter,
1265
+ HeatmapAggregator,
1266
+ LiveStateManager,
1267
+ SnapshotManager,
1268
+ SortTracker,
1269
+ StateAnalyzer,
1270
+ TrackStore,
1271
+ ZoneEvaluator,
1272
+ bboxCentroid,
1273
+ greedyAssignment,
1274
+ lineIntersection,
1275
+ normalizeToPixel,
1276
+ pointInPolygon,
1277
+ tripwireCrossing
1543
1278
  };
1544
1279
  //# sourceMappingURL=index.mjs.map