@camstack/addon-post-analysis 1.2.216 → 1.2.218

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.
@@ -21091,7 +21091,17 @@ var EventDensityBucketSchema = object({
21091
21091
  bucketStart: number(),
21092
21092
  motion: number().int(),
21093
21093
  object: number().int(),
21094
- audio: number().int()
21094
+ audio: number().int(),
21095
+ /**
21096
+ * Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
21097
+ * as opposed to `audio`, which is their COUNT (D431).
21098
+ *
21099
+ * ABSENT when the bucket holds no audio event. Absence means "nothing was
21100
+ * measured", which is not the same claim as silence, and a lane that cannot
21101
+ * tell them apart paints a quiet hour at full scale — which is exactly what
21102
+ * the count-on-a-level-meter did.
21103
+ */
21104
+ audioDbfs: number().optional()
21095
21105
  });
21096
21106
  /**
21097
21107
  * One camera's row in a `getEventDensityBatch` answer.
@@ -31838,7 +31848,23 @@ var RecordingRangeSchema = object({
31838
31848
  });
31839
31849
  var RecordingAvailabilitySchema = object({
31840
31850
  deviceId: number(),
31841
- ranges: array(RecordingRangeSchema)
31851
+ ranges: array(RecordingRangeSchema),
31852
+ /**
31853
+ * Every profile this camera has footage in — not only the one `ranges`
31854
+ * describes (D433).
31855
+ *
31856
+ * `ranges` answers for ONE profile by design: the timeline is a single bar,
31857
+ * and enumerating all of them triples the directory reads for a bar that
31858
+ * would look identical. But the answer used to say nothing about that, so a
31859
+ * caller asking "what can I export?" read the single preferred profile as
31860
+ * the only one that exists — on this deployment every camera records `high`
31861
+ * AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
31862
+ * UI could not see it.
31863
+ *
31864
+ * Cheap: it is the same shallow directory read the profile CHOICE already
31865
+ * makes. Ask for a specific profile's ranges with the `profile` input.
31866
+ */
31867
+ profilesWithFootage: array(string())
31842
31868
  });
31843
31869
  var RecordingDaysSchema = object({
31844
31870
  deviceId: number(),
@@ -31864,7 +31890,11 @@ var RecordingDaysSchema = object({
31864
31890
  var RecordingAvailabilityForDeviceSchema = object({
31865
31891
  deviceId: number(),
31866
31892
  read: _enum(["read", "unreadable"]),
31867
- ranges: array(RecordingRangeSchema).readonly()
31893
+ ranges: array(RecordingRangeSchema).readonly(),
31894
+ /** Same field, same meaning, as the singular answer (D433). A row that
31895
+ * dropped it would tell a grid caller the archive holds one profile.
31896
+ * Empty on an `'unreadable'` row: nobody looked. */
31897
+ profilesWithFootage: array(string()).readonly()
31868
31898
  });
31869
31899
  /**
31870
31900
  * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
@@ -32154,7 +32184,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
32154
32184
  method(object({
32155
32185
  deviceId: number(),
32156
32186
  fromMs: number(),
32157
- toMs: number()
32187
+ toMs: number(),
32188
+ /**
32189
+ * Answer for THIS profile instead of the preferred one (D433). Absent
32190
+ * keeps the timeline's behaviour — one bar, one profile, one set of
32191
+ * reads. `profilesWithFootage` on the answer says what may be asked
32192
+ * for.
32193
+ */
32194
+ profile: string().optional()
32158
32195
  }), RecordingAvailabilitySchema, {
32159
32196
  kind: "query",
32160
32197
  auth: "protected"
@@ -21122,7 +21122,17 @@ var EventDensityBucketSchema = object({
21122
21122
  bucketStart: number(),
21123
21123
  motion: number().int(),
21124
21124
  object: number().int(),
21125
- audio: number().int()
21125
+ audio: number().int(),
21126
+ /**
21127
+ * Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
21128
+ * as opposed to `audio`, which is their COUNT (D431).
21129
+ *
21130
+ * ABSENT when the bucket holds no audio event. Absence means "nothing was
21131
+ * measured", which is not the same claim as silence, and a lane that cannot
21132
+ * tell them apart paints a quiet hour at full scale — which is exactly what
21133
+ * the count-on-a-level-meter did.
21134
+ */
21135
+ audioDbfs: number().optional()
21126
21136
  });
21127
21137
  /**
21128
21138
  * One camera's row in a `getEventDensityBatch` answer.
@@ -31869,7 +31879,23 @@ var RecordingRangeSchema = object({
31869
31879
  });
31870
31880
  var RecordingAvailabilitySchema = object({
31871
31881
  deviceId: number(),
31872
- ranges: array(RecordingRangeSchema)
31882
+ ranges: array(RecordingRangeSchema),
31883
+ /**
31884
+ * Every profile this camera has footage in — not only the one `ranges`
31885
+ * describes (D433).
31886
+ *
31887
+ * `ranges` answers for ONE profile by design: the timeline is a single bar,
31888
+ * and enumerating all of them triples the directory reads for a bar that
31889
+ * would look identical. But the answer used to say nothing about that, so a
31890
+ * caller asking "what can I export?" read the single preferred profile as
31891
+ * the only one that exists — on this deployment every camera records `high`
31892
+ * AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
31893
+ * UI could not see it.
31894
+ *
31895
+ * Cheap: it is the same shallow directory read the profile CHOICE already
31896
+ * makes. Ask for a specific profile's ranges with the `profile` input.
31897
+ */
31898
+ profilesWithFootage: array(string())
31873
31899
  });
31874
31900
  var RecordingDaysSchema = object({
31875
31901
  deviceId: number(),
@@ -31895,7 +31921,11 @@ var RecordingDaysSchema = object({
31895
31921
  var RecordingAvailabilityForDeviceSchema = object({
31896
31922
  deviceId: number(),
31897
31923
  read: _enum(["read", "unreadable"]),
31898
- ranges: array(RecordingRangeSchema).readonly()
31924
+ ranges: array(RecordingRangeSchema).readonly(),
31925
+ /** Same field, same meaning, as the singular answer (D433). A row that
31926
+ * dropped it would tell a grid caller the archive holds one profile.
31927
+ * Empty on an `'unreadable'` row: nobody looked. */
31928
+ profilesWithFootage: array(string()).readonly()
31899
31929
  });
31900
31930
  /**
31901
31931
  * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
@@ -32185,7 +32215,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
32185
32215
  method(object({
32186
32216
  deviceId: number(),
32187
32217
  fromMs: number(),
32188
- toMs: number()
32218
+ toMs: number(),
32219
+ /**
32220
+ * Answer for THIS profile instead of the preferred one (D433). Absent
32221
+ * keeps the timeline's behaviour — one bar, one profile, one set of
32222
+ * reads. `profilesWithFootage` on the answer says what may be asked
32223
+ * for.
32224
+ */
32225
+ profile: string().optional()
32189
32226
  }), RecordingAvailabilitySchema, {
32190
32227
  kind: "query",
32191
32228
  auth: "protected"
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B_6UZCut.js");
5
+ const require_dist = require("../dist-CbQHenLE.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs);
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { At as resolvePoolMemoryPolicy, B as PoolMemoryWatchdog, Ct as parseProcStatus, gt as hfModelUrl, ut as embeddingEncoderCapability, zt as BaseAddon } from "../dist-DWZqNymj.mjs";
1
+ import { At as resolvePoolMemoryPolicy, B as PoolMemoryWatchdog, Ct as parseProcStatus, gt as hfModelUrl, ut as embeddingEncoderCapability, zt as BaseAddon } from "../dist-BqkhuC85.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.95",
6
+ version: "1.2.96",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.175",
21
+ version: "1.2.176",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.143",
36
+ version: "1.2.144",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.175",
39
+ version: "1.2.176",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.95",
48
+ version: "1.2.96",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.143",
84
+ version: "1.2.144",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-B_6UZCut.js");
5
+ const require_dist = require("../dist-CbQHenLE.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -20092,11 +20092,19 @@ var TimelapseRenderer = class {
20092
20092
  async render(request) {
20093
20093
  const { deviceId, window } = request;
20094
20094
  const tags = { deviceId };
20095
- const availability = await this.ports.getAvailability({
20095
+ const first = await this.ports.getAvailability({
20096
20096
  deviceId,
20097
20097
  fromMs: window.startMs,
20098
20098
  toMs: window.endMs
20099
20099
  });
20100
+ const cheapest = [...first.profilesWithFootage].sort((a, b) => profileCost(a) - profileCost(b))[0];
20101
+ const describes = first.ranges[0]?.profile;
20102
+ const availability = cheapest !== void 0 && cheapest !== describes ? (await this.ports.getAvailability({
20103
+ deviceId,
20104
+ fromMs: window.startMs,
20105
+ toMs: window.endMs,
20106
+ profile: cheapest
20107
+ })).ranges : first.ranges;
20100
20108
  const chosen = pickProfile(availability, window);
20101
20109
  if (chosen === null || chosen.coverage.coveredMs === 0) {
20102
20110
  this.ports.logger.warn("timelapse skipped: no footage covers the window", {
@@ -36091,6 +36099,17 @@ function mapObjectStateToTrackState(s) {
36091
36099
  default: return "new";
36092
36100
  }
36093
36101
  }
36102
+ /**
36103
+ * How long an identical refusal stays collapsed. A stationary subject is
36104
+ * refused on every analysed frame; at ~2 fps of analysis, 60 s turns a parked
36105
+ * car's 120 lines into one and still reports a subject that comes back later.
36106
+ */
36107
+ var ZONE_REFUSAL_THROTTLE_MS = 6e4;
36108
+ /** Box quantisation for the collapse key: a subject that moves less than this
36109
+ * is the same sighting for diagnostic purposes. */
36110
+ var ZONE_REFUSAL_BOX_CELL_PX = 64;
36111
+ /** Bound on the collapse map; stale keys are swept when it is exceeded. */
36112
+ var ZONE_REFUSAL_KEY_LIMIT = 512;
36094
36113
  var FrameProcessor = class {
36095
36114
  deviceId;
36096
36115
  /** Detection source this processor handles (`pipeline` | `onboard`).
@@ -36151,7 +36170,6 @@ var FrameProcessor = class {
36151
36170
  * filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
36152
36171
  spawnRefusalObserver = null;
36153
36172
  /** Where a collected refusal is forwarded for LOGGING, when anyone asked. */
36154
- spawnRefusalForward = null;
36155
36173
  /**
36156
36174
  * The spawn refusals of the frame being processed.
36157
36175
  *
@@ -36162,8 +36180,15 @@ var FrameProcessor = class {
36162
36180
  * as before?" answered first. See `zone-detection-emits-without-a-track`.
36163
36181
  */
36164
36182
  refusalsThisFrame = [];
36165
- /** Running count of events this path has produced — the volume measure. */
36183
+ /** Running count of refusals-in-zone SEEN never the throttled number. */
36166
36184
  zoneRefusalEventCount = 0;
36185
+ zoneRefusalObserver = null;
36186
+ /** Track ids whose BIRTH zone history has already been folded in — the fold
36187
+ * happens once, on the frame a track first becomes visible. */
36188
+ birthZonesSeeded = /* @__PURE__ */ new Set();
36189
+ birthZoneTrailObserver = null;
36190
+ /** Last report time per collapsed key, for {@link ZONE_REFUSAL_THROTTLE_MS}. */
36191
+ zoneRefusalLastReport = /* @__PURE__ */ new Map();
36167
36192
  zoneRefusalSampler = new SpawnRefusalSampler();
36168
36193
  /** Optional stationary-object gate (parked-object suppression). Null until
36169
36194
  * the addon wires it via {@link setStationaryGate}. */
@@ -36264,6 +36289,18 @@ var FrameProcessor = class {
36264
36289
  this.tracker.setSpawnRefusalObserver(observer);
36265
36290
  this.spawnRefusalObserver = observer;
36266
36291
  }
36292
+ /**
36293
+ * Where a refused-inside-a-zone detection goes. Diagnostics only: this is
36294
+ * deliberately NOT the event stream, so nothing downstream can alert on a
36295
+ * box the confirmation gate threw away (D426).
36296
+ */
36297
+ setZoneRefusalObserver(observer) {
36298
+ this.zoneRefusalObserver = observer;
36299
+ }
36300
+ /** Where "this track was born already carrying a zone" is reported. */
36301
+ setBirthZoneTrailObserver(observer) {
36302
+ this.birthZoneTrailObserver = observer;
36303
+ }
36267
36304
  setBindObserver(observer) {
36268
36305
  this.tracker.setBindObserver(observer);
36269
36306
  }
@@ -36299,40 +36336,73 @@ var FrameProcessor = class {
36299
36336
  * operator's own detection rules already said the box does not count on this
36300
36337
  * camera, and re-admitting it here would overrule a decision they made.
36301
36338
  */
36302
- eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight) {
36339
+ /**
36340
+ * Report every detection the tracker refused that fell INSIDE a watched
36341
+ * zone. Diagnostics only — see the spec
36342
+ * `zone-refusal-is-observed-never-notified` for the ten hours of production
36343
+ * that decided this: 1473 of 1511 such refusals were one stationary parcel
36344
+ * re-offered every frame, and both notifications this path ever produced
36345
+ * were phantoms the confirmation gate had correctly rejected.
36346
+ *
36347
+ * Drains {@link refusalsThisFrame}. `zone-filtered` is skipped: a box the
36348
+ * zone filter already removed was never in a zone.
36349
+ */
36350
+ reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight) {
36303
36351
  const refusals = this.refusalsThisFrame;
36304
36352
  this.refusalsThisFrame = [];
36305
- if (refusals.length === 0 || this.zones.length === 0) return [];
36306
- const out = [];
36353
+ if (refusals.length === 0 || this.zones.length === 0) return;
36354
+ const observer = this.zoneRefusalObserver;
36355
+ if (observer === null) return;
36307
36356
  for (const r of refusals) {
36308
36357
  if (r.reason === "zone-filtered") continue;
36309
36358
  const { memberships } = this.zoneEngine.splitDetectionZones(r.bbox, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36310
36359
  if (memberships.length === 0) continue;
36311
36360
  this.zoneRefusalEventCount += 1;
36312
- out.push({
36313
- id: (0, node_crypto.randomUUID)(),
36314
- kind: "object",
36361
+ if (!this.shouldReportZoneRefusal(r, memberships, timestamp)) continue;
36362
+ observer({
36315
36363
  deviceId: this.deviceId,
36316
36364
  timestamp,
36317
- frameId: frame.frameId,
36318
36365
  className: r.detClass,
36319
36366
  confidence: r.detScore,
36320
36367
  bbox: r.bbox,
36321
- frameWidth,
36322
- frameHeight,
36323
- zones: memberships.map((m) => m.zoneId),
36324
- zoneOverlaps: memberships.map((m) => ({
36368
+ reason: r.reason,
36369
+ zones: memberships.map((m) => ({
36325
36370
  zoneId: m.zoneId,
36326
36371
  ...m.zoneName === void 0 ? {} : { zoneName: m.zoneName },
36327
36372
  overlapPct: Math.round(m.overlap * 1e3) / 10
36328
36373
  })),
36329
- spawnRefused: r.reason
36330
- });
36374
+ seenSoFar: this.zoneRefusalEventCount
36375
+ });
36376
+ }
36377
+ }
36378
+ /**
36379
+ * Collapse identical refusals. A refused box is refused on EVERY analysed
36380
+ * frame, so a subject that does not leave — the parcel at the door, the
36381
+ * parked car — is an unbounded emitter. The key includes a coarse box, so a
36382
+ * subject that actually moves keeps reporting while a stationary one does
36383
+ * not.
36384
+ */
36385
+ shouldReportZoneRefusal(r, memberships, timestamp) {
36386
+ const cell = (v) => Math.round(v / ZONE_REFUSAL_BOX_CELL_PX);
36387
+ const key = [
36388
+ r.detClass,
36389
+ r.reason,
36390
+ memberships.map((m) => m.zoneId).join("+"),
36391
+ cell(r.bbox.x),
36392
+ cell(r.bbox.y),
36393
+ cell(r.bbox.w),
36394
+ cell(r.bbox.h)
36395
+ ].join("|");
36396
+ const last = this.zoneRefusalLastReport.get(key);
36397
+ if (last !== void 0 && timestamp - last < ZONE_REFUSAL_THROTTLE_MS) return false;
36398
+ this.zoneRefusalLastReport.set(key, timestamp);
36399
+ if (this.zoneRefusalLastReport.size > ZONE_REFUSAL_KEY_LIMIT) {
36400
+ for (const [k, t] of this.zoneRefusalLastReport) if (timestamp - t >= ZONE_REFUSAL_THROTTLE_MS) this.zoneRefusalLastReport.delete(k);
36331
36401
  }
36332
- return out;
36402
+ return true;
36333
36403
  }
36334
- /** How many zone-refusal events this processor has emitted — the measure that
36335
- * says whether this path is a trickle or a storm. Read by the addon. */
36404
+ /** How many refusals-in-zone this processor has SEEN — the measure that says
36405
+ * whether this path is a trickle or a storm, before any throttling. */
36336
36406
  zoneRefusalEventsEmitted() {
36337
36407
  return this.zoneRefusalEventCount;
36338
36408
  }
@@ -36566,6 +36636,32 @@ var FrameProcessor = class {
36566
36636
  if (memberships.length > 0) overlapsByTrack.set(td.trackId, memberships);
36567
36637
  if (belowBar.length > 0) rejectedByTrack.set(td.trackId, belowBar);
36568
36638
  }
36639
+ for (const td of trackedDetections) {
36640
+ if (this.birthZonesSeeded.has(td.trackId)) continue;
36641
+ this.birthZonesSeeded.add(td.trackId);
36642
+ const trail = td.path;
36643
+ if (trail.length <= 1) continue;
36644
+ const atBirth = zonesByTrack.get(td.trackId) ?? [];
36645
+ const zones = new Set(atBirth);
36646
+ const bornWith = zones.size;
36647
+ for (let i = 0; i < trail.length - 1; i++) {
36648
+ const box = trail[i];
36649
+ if (box === void 0) continue;
36650
+ const { memberships } = this.zoneEngine.splitDetectionZones(box, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36651
+ for (const m of memberships) zones.add(m.zoneId);
36652
+ }
36653
+ if (zones.size === bornWith) continue;
36654
+ const inherited = [...zones].filter((z) => !atBirth.includes(z));
36655
+ zonesByTrack.set(td.trackId, [...zones]);
36656
+ this.birthZoneTrailObserver?.({
36657
+ deviceId: this.deviceId,
36658
+ trackId: td.trackId,
36659
+ className: td.class,
36660
+ measurementsBeforeBirth: trail.length - 1,
36661
+ zonesAtBirth: [...atBirth],
36662
+ inheritedZones: inherited
36663
+ });
36664
+ }
36569
36665
  this.lastZoneOverlaps = overlapsByTrack;
36570
36666
  this.lastZoneRejections = rejectedByTrack;
36571
36667
  for (const td of trackedDetections) this.lastDetectionByTrack.set(td.trackId, td);
@@ -36638,6 +36734,7 @@ var FrameProcessor = class {
36638
36734
  for (const state of objectStates) {
36639
36735
  if (state.state !== "leaving") continue;
36640
36736
  this.lastZonesByTrack.delete(state.trackId);
36737
+ this.birthZonesSeeded.delete(state.trackId);
36641
36738
  this.lastDetectionByTrack.delete(state.trackId);
36642
36739
  this.reportedRefusalByTrack.delete(state.trackId);
36643
36740
  }
@@ -36737,11 +36834,10 @@ var FrameProcessor = class {
36737
36834
  frameHeight
36738
36835
  };
36739
36836
  };
36740
- const zoneRefusalEvents = this.eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight);
36837
+ this.reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight);
36741
36838
  const withTrackId = rawEvents.filter((e) => e.detection.trackId);
36742
36839
  const objectEvents = withTrackId.filter((e) => e.type !== "object.detected").map((e) => toObjectEvent(e));
36743
36840
  const appearanceEvents = withTrackId.filter((e) => e.type === "object.detected").map((e) => toObjectEvent(e, "entered"));
36744
- objectEvents.push(...zoneRefusalEvents);
36745
36841
  return {
36746
36842
  deviceId: this.deviceId,
36747
36843
  timestamp,
@@ -39566,6 +39662,62 @@ function tieredLabelColumnData(patch) {
39566
39662
  };
39567
39663
  }
39568
39664
  //#endregion
39665
+ //#region src/pipeline-analytics/store/audio-level-density.ts
39666
+ /**
39667
+ * The energy-weighted mean of a set of dBFS values, in dBFS.
39668
+ *
39669
+ * dBFS is logarithmic, so an arithmetic mean of dB numbers is the mean of
39670
+ * nothing physical — it under-weights the loud moment that is the reason to
39671
+ * look at a bar at all. Averaging POWER and converting back is what a meter is
39672
+ * expected to show, and it is the same arithmetic as an RMS over the interval.
39673
+ *
39674
+ * `undefined` for an empty set, and for a set with no finite sample: a bucket
39675
+ * nobody measured must not be reported as silence, and −∞ dBFS is not a number
39676
+ * a bar can paint.
39677
+ */
39678
+ function energyWeightedMeanDbfs(samples) {
39679
+ let power = 0;
39680
+ let n = 0;
39681
+ for (const dbfs of samples) {
39682
+ if (Number.isNaN(dbfs)) continue;
39683
+ if (dbfs === Number.NEGATIVE_INFINITY) {
39684
+ n += 1;
39685
+ continue;
39686
+ }
39687
+ if (!Number.isFinite(dbfs)) continue;
39688
+ power += 10 ** (dbfs / 10);
39689
+ n += 1;
39690
+ }
39691
+ if (n === 0) return void 0;
39692
+ const mean = power / n;
39693
+ if (mean <= 0) return void 0;
39694
+ return 10 * Math.log10(mean);
39695
+ }
39696
+ /**
39697
+ * Energy-weighted mean dBFS per bucket index, for the buckets that HAVE audio.
39698
+ *
39699
+ * A bucket with no events is absent from the map rather than present at zero:
39700
+ * "nothing was heard" and "nothing was measured" are different claims, and the
39701
+ * lane has to be able to draw nothing.
39702
+ */
39703
+ function audioDbfsByBucket(events, since, bucketMs) {
39704
+ const out = /* @__PURE__ */ new Map();
39705
+ if (bucketMs <= 0) return out;
39706
+ const byBucket = /* @__PURE__ */ new Map();
39707
+ for (const ev of events) {
39708
+ const idx = Math.floor((ev.timestamp - since) / bucketMs);
39709
+ if (idx < 0) continue;
39710
+ const bin = byBucket.get(idx);
39711
+ if (bin === void 0) byBucket.set(idx, [ev.dbfs]);
39712
+ else bin.push(ev.dbfs);
39713
+ }
39714
+ for (const [idx, samples] of byBucket) {
39715
+ const mean = energyWeightedMeanDbfs(samples);
39716
+ if (mean !== void 0) out.set(idx, mean);
39717
+ }
39718
+ return out;
39719
+ }
39720
+ //#endregion
39569
39721
  //#region src/pipeline-analytics/store/event-store.ts
39570
39722
  /**
39571
39723
  * @durable class=ledger owner=pipeline-analytics
@@ -40404,10 +40556,11 @@ var EventStore = class {
40404
40556
  });
40405
40557
  return new Map(rows.map((r) => [r.bucket, r.count]));
40406
40558
  };
40407
- const [motionMap, objectMap, audioMap] = await Promise.all([
40559
+ const [motionMap, objectMap, audioMap, audioDbfsMap] = await Promise.all([
40408
40560
  run(MOTION_EVENTS_COLLECTION),
40409
40561
  run(OBJECT_EVENTS_COLLECTION),
40410
- run(AUDIO_EVENTS_COLLECTION)
40562
+ run(AUDIO_EVENTS_COLLECTION),
40563
+ this.audioDbfsPerBucket(q)
40411
40564
  ]);
40412
40565
  const buckets = Math.max(0, Math.ceil((q.until - q.since) / q.bucketMs));
40413
40566
  const out = [];
@@ -40416,16 +40569,46 @@ var EventStore = class {
40416
40569
  const o = objectMap.get(i) ?? 0;
40417
40570
  const a = audioMap.get(i) ?? 0;
40418
40571
  if (m + o + a === 0) continue;
40572
+ const dbfs = audioDbfsMap.get(i);
40419
40573
  out.push({
40420
40574
  bucketStart: q.since + i * q.bucketMs,
40421
40575
  motion: m,
40422
40576
  object: o,
40423
- audio: a
40577
+ audio: a,
40578
+ ...dbfs !== void 0 ? { audioDbfs: dbfs } : {}
40424
40579
  });
40425
40580
  }
40426
40581
  return out;
40427
40582
  }
40428
40583
  /**
40584
+ * Energy-weighted mean dBFS per bucket index, for the buckets that have
40585
+ * audio. Reads the window's audio events once, `slim`, and folds them —
40586
+ * `histogram` can only count, and a level is not a count.
40587
+ *
40588
+ * A read that fails yields an EMPTY map, so the lane draws no level rather
40589
+ * than a wrong one: the density counts beside it are unaffected, and a
40590
+ * missing bar is the honest rendering of "not measured" (D431).
40591
+ */
40592
+ async audioDbfsPerBucket(q) {
40593
+ try {
40594
+ return audioDbfsByBucket((await this.queryAudio({
40595
+ deviceId: q.deviceId,
40596
+ since: q.since,
40597
+ until: q.until,
40598
+ projection: "slim"
40599
+ })).map((e) => ({
40600
+ timestamp: e.timestamp,
40601
+ dbfs: e.dbfs
40602
+ })), q.since, q.bucketMs);
40603
+ } catch (err) {
40604
+ this.logger.warn("density: audio level aggregation failed — the lane will draw no level", {
40605
+ tags: { deviceId: q.deviceId },
40606
+ meta: { error: String(err) }
40607
+ });
40608
+ return /* @__PURE__ */ new Map();
40609
+ }
40610
+ }
40611
+ /**
40429
40612
  * Age-evict the TRACKLESS event kinds — motion and audio.
40430
40613
  *
40431
40614
  * **Object events are not here, and that is the point.** An object event is a
@@ -72762,12 +72945,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72762
72945
  */
72763
72946
  buildTimelapsePorts(api, stores) {
72764
72947
  return {
72765
- getAvailability: async ({ deviceId, fromMs, toMs }) => {
72766
- return (await api.recording.getAvailability.query({
72948
+ getAvailability: async ({ deviceId, fromMs, toMs, profile }) => {
72949
+ const availability = await api.recording.getAvailability.query({
72767
72950
  deviceId,
72768
72951
  fromMs,
72769
- toMs
72770
- })).ranges;
72952
+ toMs,
72953
+ ...profile !== void 0 ? { profile } : {}
72954
+ });
72955
+ return {
72956
+ ranges: availability.ranges,
72957
+ profilesWithFootage: availability.profilesWithFootage
72958
+ };
72771
72959
  },
72772
72960
  listTracks: async ({ deviceId, since, until }) => {
72773
72961
  return (await stores.trackStore.queryHistorical({
@@ -77554,6 +77742,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77554
77742
  }
77555
77743
  });
77556
77744
  });
77745
+ p.setBirthZoneTrailObserver((trail) => {
77746
+ this.ctx.logger.info("track born carrying zones it crossed before it was a track", {
77747
+ tags: { deviceId },
77748
+ meta: {
77749
+ source,
77750
+ ...trail
77751
+ }
77752
+ });
77753
+ });
77754
+ p.setZoneRefusalObserver((observation) => {
77755
+ this.ctx.logger.info("detection refused inside a watched zone — not notified", {
77756
+ tags: { deviceId },
77757
+ meta: {
77758
+ source,
77759
+ ...observation
77760
+ }
77761
+ });
77762
+ });
77557
77763
  p.setSpawnBesideCoastingObserver((record) => {
77558
77764
  this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
77559
77765
  tags: { deviceId },
@@ -1,4 +1,4 @@
1
- import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-DWZqNymj.mjs";
1
+ import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-BqkhuC85.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -20070,11 +20070,19 @@ var TimelapseRenderer = class {
20070
20070
  async render(request) {
20071
20071
  const { deviceId, window } = request;
20072
20072
  const tags = { deviceId };
20073
- const availability = await this.ports.getAvailability({
20073
+ const first = await this.ports.getAvailability({
20074
20074
  deviceId,
20075
20075
  fromMs: window.startMs,
20076
20076
  toMs: window.endMs
20077
20077
  });
20078
+ const cheapest = [...first.profilesWithFootage].sort((a, b) => profileCost(a) - profileCost(b))[0];
20079
+ const describes = first.ranges[0]?.profile;
20080
+ const availability = cheapest !== void 0 && cheapest !== describes ? (await this.ports.getAvailability({
20081
+ deviceId,
20082
+ fromMs: window.startMs,
20083
+ toMs: window.endMs,
20084
+ profile: cheapest
20085
+ })).ranges : first.ranges;
20078
20086
  const chosen = pickProfile(availability, window);
20079
20087
  if (chosen === null || chosen.coverage.coveredMs === 0) {
20080
20088
  this.ports.logger.warn("timelapse skipped: no footage covers the window", {
@@ -36066,6 +36074,17 @@ function mapObjectStateToTrackState(s) {
36066
36074
  default: return "new";
36067
36075
  }
36068
36076
  }
36077
+ /**
36078
+ * How long an identical refusal stays collapsed. A stationary subject is
36079
+ * refused on every analysed frame; at ~2 fps of analysis, 60 s turns a parked
36080
+ * car's 120 lines into one and still reports a subject that comes back later.
36081
+ */
36082
+ var ZONE_REFUSAL_THROTTLE_MS = 6e4;
36083
+ /** Box quantisation for the collapse key: a subject that moves less than this
36084
+ * is the same sighting for diagnostic purposes. */
36085
+ var ZONE_REFUSAL_BOX_CELL_PX = 64;
36086
+ /** Bound on the collapse map; stale keys are swept when it is exceeded. */
36087
+ var ZONE_REFUSAL_KEY_LIMIT = 512;
36069
36088
  var FrameProcessor = class {
36070
36089
  deviceId;
36071
36090
  /** Detection source this processor handles (`pipeline` | `onboard`).
@@ -36126,7 +36145,6 @@ var FrameProcessor = class {
36126
36145
  * filter). Shares the tracker's vocabulary — see {@link SpawnRefusalReason}. */
36127
36146
  spawnRefusalObserver = null;
36128
36147
  /** Where a collected refusal is forwarded for LOGGING, when anyone asked. */
36129
- spawnRefusalForward = null;
36130
36148
  /**
36131
36149
  * The spawn refusals of the frame being processed.
36132
36150
  *
@@ -36137,8 +36155,15 @@ var FrameProcessor = class {
36137
36155
  * as before?" answered first. See `zone-detection-emits-without-a-track`.
36138
36156
  */
36139
36157
  refusalsThisFrame = [];
36140
- /** Running count of events this path has produced — the volume measure. */
36158
+ /** Running count of refusals-in-zone SEEN never the throttled number. */
36141
36159
  zoneRefusalEventCount = 0;
36160
+ zoneRefusalObserver = null;
36161
+ /** Track ids whose BIRTH zone history has already been folded in — the fold
36162
+ * happens once, on the frame a track first becomes visible. */
36163
+ birthZonesSeeded = /* @__PURE__ */ new Set();
36164
+ birthZoneTrailObserver = null;
36165
+ /** Last report time per collapsed key, for {@link ZONE_REFUSAL_THROTTLE_MS}. */
36166
+ zoneRefusalLastReport = /* @__PURE__ */ new Map();
36142
36167
  zoneRefusalSampler = new SpawnRefusalSampler();
36143
36168
  /** Optional stationary-object gate (parked-object suppression). Null until
36144
36169
  * the addon wires it via {@link setStationaryGate}. */
@@ -36239,6 +36264,18 @@ var FrameProcessor = class {
36239
36264
  this.tracker.setSpawnRefusalObserver(observer);
36240
36265
  this.spawnRefusalObserver = observer;
36241
36266
  }
36267
+ /**
36268
+ * Where a refused-inside-a-zone detection goes. Diagnostics only: this is
36269
+ * deliberately NOT the event stream, so nothing downstream can alert on a
36270
+ * box the confirmation gate threw away (D426).
36271
+ */
36272
+ setZoneRefusalObserver(observer) {
36273
+ this.zoneRefusalObserver = observer;
36274
+ }
36275
+ /** Where "this track was born already carrying a zone" is reported. */
36276
+ setBirthZoneTrailObserver(observer) {
36277
+ this.birthZoneTrailObserver = observer;
36278
+ }
36242
36279
  setBindObserver(observer) {
36243
36280
  this.tracker.setBindObserver(observer);
36244
36281
  }
@@ -36274,40 +36311,73 @@ var FrameProcessor = class {
36274
36311
  * operator's own detection rules already said the box does not count on this
36275
36312
  * camera, and re-admitting it here would overrule a decision they made.
36276
36313
  */
36277
- eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight) {
36314
+ /**
36315
+ * Report every detection the tracker refused that fell INSIDE a watched
36316
+ * zone. Diagnostics only — see the spec
36317
+ * `zone-refusal-is-observed-never-notified` for the ten hours of production
36318
+ * that decided this: 1473 of 1511 such refusals were one stationary parcel
36319
+ * re-offered every frame, and both notifications this path ever produced
36320
+ * were phantoms the confirmation gate had correctly rejected.
36321
+ *
36322
+ * Drains {@link refusalsThisFrame}. `zone-filtered` is skipped: a box the
36323
+ * zone filter already removed was never in a zone.
36324
+ */
36325
+ reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight) {
36278
36326
  const refusals = this.refusalsThisFrame;
36279
36327
  this.refusalsThisFrame = [];
36280
- if (refusals.length === 0 || this.zones.length === 0) return [];
36281
- const out = [];
36328
+ if (refusals.length === 0 || this.zones.length === 0) return;
36329
+ const observer = this.zoneRefusalObserver;
36330
+ if (observer === null) return;
36282
36331
  for (const r of refusals) {
36283
36332
  if (r.reason === "zone-filtered") continue;
36284
36333
  const { memberships } = this.zoneEngine.splitDetectionZones(r.bbox, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36285
36334
  if (memberships.length === 0) continue;
36286
36335
  this.zoneRefusalEventCount += 1;
36287
- out.push({
36288
- id: randomUUID(),
36289
- kind: "object",
36336
+ if (!this.shouldReportZoneRefusal(r, memberships, timestamp)) continue;
36337
+ observer({
36290
36338
  deviceId: this.deviceId,
36291
36339
  timestamp,
36292
- frameId: frame.frameId,
36293
36340
  className: r.detClass,
36294
36341
  confidence: r.detScore,
36295
36342
  bbox: r.bbox,
36296
- frameWidth,
36297
- frameHeight,
36298
- zones: memberships.map((m) => m.zoneId),
36299
- zoneOverlaps: memberships.map((m) => ({
36343
+ reason: r.reason,
36344
+ zones: memberships.map((m) => ({
36300
36345
  zoneId: m.zoneId,
36301
36346
  ...m.zoneName === void 0 ? {} : { zoneName: m.zoneName },
36302
36347
  overlapPct: Math.round(m.overlap * 1e3) / 10
36303
36348
  })),
36304
- spawnRefused: r.reason
36305
- });
36349
+ seenSoFar: this.zoneRefusalEventCount
36350
+ });
36351
+ }
36352
+ }
36353
+ /**
36354
+ * Collapse identical refusals. A refused box is refused on EVERY analysed
36355
+ * frame, so a subject that does not leave — the parcel at the door, the
36356
+ * parked car — is an unbounded emitter. The key includes a coarse box, so a
36357
+ * subject that actually moves keeps reporting while a stationary one does
36358
+ * not.
36359
+ */
36360
+ shouldReportZoneRefusal(r, memberships, timestamp) {
36361
+ const cell = (v) => Math.round(v / ZONE_REFUSAL_BOX_CELL_PX);
36362
+ const key = [
36363
+ r.detClass,
36364
+ r.reason,
36365
+ memberships.map((m) => m.zoneId).join("+"),
36366
+ cell(r.bbox.x),
36367
+ cell(r.bbox.y),
36368
+ cell(r.bbox.w),
36369
+ cell(r.bbox.h)
36370
+ ].join("|");
36371
+ const last = this.zoneRefusalLastReport.get(key);
36372
+ if (last !== void 0 && timestamp - last < ZONE_REFUSAL_THROTTLE_MS) return false;
36373
+ this.zoneRefusalLastReport.set(key, timestamp);
36374
+ if (this.zoneRefusalLastReport.size > ZONE_REFUSAL_KEY_LIMIT) {
36375
+ for (const [k, t] of this.zoneRefusalLastReport) if (timestamp - t >= ZONE_REFUSAL_THROTTLE_MS) this.zoneRefusalLastReport.delete(k);
36306
36376
  }
36307
- return out;
36377
+ return true;
36308
36378
  }
36309
- /** How many zone-refusal events this processor has emitted — the measure that
36310
- * says whether this path is a trickle or a storm. Read by the addon. */
36379
+ /** How many refusals-in-zone this processor has SEEN — the measure that says
36380
+ * whether this path is a trickle or a storm, before any throttling. */
36311
36381
  zoneRefusalEventsEmitted() {
36312
36382
  return this.zoneRefusalEventCount;
36313
36383
  }
@@ -36541,6 +36611,32 @@ var FrameProcessor = class {
36541
36611
  if (memberships.length > 0) overlapsByTrack.set(td.trackId, memberships);
36542
36612
  if (belowBar.length > 0) rejectedByTrack.set(td.trackId, belowBar);
36543
36613
  }
36614
+ for (const td of trackedDetections) {
36615
+ if (this.birthZonesSeeded.has(td.trackId)) continue;
36616
+ this.birthZonesSeeded.add(td.trackId);
36617
+ const trail = td.path;
36618
+ if (trail.length <= 1) continue;
36619
+ const atBirth = zonesByTrack.get(td.trackId) ?? [];
36620
+ const zones = new Set(atBirth);
36621
+ const bornWith = zones.size;
36622
+ for (let i = 0; i < trail.length - 1; i++) {
36623
+ const box = trail[i];
36624
+ if (box === void 0) continue;
36625
+ const { memberships } = this.zoneEngine.splitDetectionZones(box, this.zones, frameWidth, frameHeight, void 0, void 0, void 0, this.zoneMembershipMinOverlap);
36626
+ for (const m of memberships) zones.add(m.zoneId);
36627
+ }
36628
+ if (zones.size === bornWith) continue;
36629
+ const inherited = [...zones].filter((z) => !atBirth.includes(z));
36630
+ zonesByTrack.set(td.trackId, [...zones]);
36631
+ this.birthZoneTrailObserver?.({
36632
+ deviceId: this.deviceId,
36633
+ trackId: td.trackId,
36634
+ className: td.class,
36635
+ measurementsBeforeBirth: trail.length - 1,
36636
+ zonesAtBirth: [...atBirth],
36637
+ inheritedZones: inherited
36638
+ });
36639
+ }
36544
36640
  this.lastZoneOverlaps = overlapsByTrack;
36545
36641
  this.lastZoneRejections = rejectedByTrack;
36546
36642
  for (const td of trackedDetections) this.lastDetectionByTrack.set(td.trackId, td);
@@ -36613,6 +36709,7 @@ var FrameProcessor = class {
36613
36709
  for (const state of objectStates) {
36614
36710
  if (state.state !== "leaving") continue;
36615
36711
  this.lastZonesByTrack.delete(state.trackId);
36712
+ this.birthZonesSeeded.delete(state.trackId);
36616
36713
  this.lastDetectionByTrack.delete(state.trackId);
36617
36714
  this.reportedRefusalByTrack.delete(state.trackId);
36618
36715
  }
@@ -36712,11 +36809,10 @@ var FrameProcessor = class {
36712
36809
  frameHeight
36713
36810
  };
36714
36811
  };
36715
- const zoneRefusalEvents = this.eventsForRefusedDetectionsInZones(timestamp, frame, frameWidth, frameHeight);
36812
+ this.reportRefusedDetectionsInZones(timestamp, frameWidth, frameHeight);
36716
36813
  const withTrackId = rawEvents.filter((e) => e.detection.trackId);
36717
36814
  const objectEvents = withTrackId.filter((e) => e.type !== "object.detected").map((e) => toObjectEvent(e));
36718
36815
  const appearanceEvents = withTrackId.filter((e) => e.type === "object.detected").map((e) => toObjectEvent(e, "entered"));
36719
- objectEvents.push(...zoneRefusalEvents);
36720
36816
  return {
36721
36817
  deviceId: this.deviceId,
36722
36818
  timestamp,
@@ -39541,6 +39637,62 @@ function tieredLabelColumnData(patch) {
39541
39637
  };
39542
39638
  }
39543
39639
  //#endregion
39640
+ //#region src/pipeline-analytics/store/audio-level-density.ts
39641
+ /**
39642
+ * The energy-weighted mean of a set of dBFS values, in dBFS.
39643
+ *
39644
+ * dBFS is logarithmic, so an arithmetic mean of dB numbers is the mean of
39645
+ * nothing physical — it under-weights the loud moment that is the reason to
39646
+ * look at a bar at all. Averaging POWER and converting back is what a meter is
39647
+ * expected to show, and it is the same arithmetic as an RMS over the interval.
39648
+ *
39649
+ * `undefined` for an empty set, and for a set with no finite sample: a bucket
39650
+ * nobody measured must not be reported as silence, and −∞ dBFS is not a number
39651
+ * a bar can paint.
39652
+ */
39653
+ function energyWeightedMeanDbfs(samples) {
39654
+ let power = 0;
39655
+ let n = 0;
39656
+ for (const dbfs of samples) {
39657
+ if (Number.isNaN(dbfs)) continue;
39658
+ if (dbfs === Number.NEGATIVE_INFINITY) {
39659
+ n += 1;
39660
+ continue;
39661
+ }
39662
+ if (!Number.isFinite(dbfs)) continue;
39663
+ power += 10 ** (dbfs / 10);
39664
+ n += 1;
39665
+ }
39666
+ if (n === 0) return void 0;
39667
+ const mean = power / n;
39668
+ if (mean <= 0) return void 0;
39669
+ return 10 * Math.log10(mean);
39670
+ }
39671
+ /**
39672
+ * Energy-weighted mean dBFS per bucket index, for the buckets that HAVE audio.
39673
+ *
39674
+ * A bucket with no events is absent from the map rather than present at zero:
39675
+ * "nothing was heard" and "nothing was measured" are different claims, and the
39676
+ * lane has to be able to draw nothing.
39677
+ */
39678
+ function audioDbfsByBucket(events, since, bucketMs) {
39679
+ const out = /* @__PURE__ */ new Map();
39680
+ if (bucketMs <= 0) return out;
39681
+ const byBucket = /* @__PURE__ */ new Map();
39682
+ for (const ev of events) {
39683
+ const idx = Math.floor((ev.timestamp - since) / bucketMs);
39684
+ if (idx < 0) continue;
39685
+ const bin = byBucket.get(idx);
39686
+ if (bin === void 0) byBucket.set(idx, [ev.dbfs]);
39687
+ else bin.push(ev.dbfs);
39688
+ }
39689
+ for (const [idx, samples] of byBucket) {
39690
+ const mean = energyWeightedMeanDbfs(samples);
39691
+ if (mean !== void 0) out.set(idx, mean);
39692
+ }
39693
+ return out;
39694
+ }
39695
+ //#endregion
39544
39696
  //#region src/pipeline-analytics/store/event-store.ts
39545
39697
  /**
39546
39698
  * @durable class=ledger owner=pipeline-analytics
@@ -40379,10 +40531,11 @@ var EventStore = class {
40379
40531
  });
40380
40532
  return new Map(rows.map((r) => [r.bucket, r.count]));
40381
40533
  };
40382
- const [motionMap, objectMap, audioMap] = await Promise.all([
40534
+ const [motionMap, objectMap, audioMap, audioDbfsMap] = await Promise.all([
40383
40535
  run(MOTION_EVENTS_COLLECTION),
40384
40536
  run(OBJECT_EVENTS_COLLECTION),
40385
- run(AUDIO_EVENTS_COLLECTION)
40537
+ run(AUDIO_EVENTS_COLLECTION),
40538
+ this.audioDbfsPerBucket(q)
40386
40539
  ]);
40387
40540
  const buckets = Math.max(0, Math.ceil((q.until - q.since) / q.bucketMs));
40388
40541
  const out = [];
@@ -40391,16 +40544,46 @@ var EventStore = class {
40391
40544
  const o = objectMap.get(i) ?? 0;
40392
40545
  const a = audioMap.get(i) ?? 0;
40393
40546
  if (m + o + a === 0) continue;
40547
+ const dbfs = audioDbfsMap.get(i);
40394
40548
  out.push({
40395
40549
  bucketStart: q.since + i * q.bucketMs,
40396
40550
  motion: m,
40397
40551
  object: o,
40398
- audio: a
40552
+ audio: a,
40553
+ ...dbfs !== void 0 ? { audioDbfs: dbfs } : {}
40399
40554
  });
40400
40555
  }
40401
40556
  return out;
40402
40557
  }
40403
40558
  /**
40559
+ * Energy-weighted mean dBFS per bucket index, for the buckets that have
40560
+ * audio. Reads the window's audio events once, `slim`, and folds them —
40561
+ * `histogram` can only count, and a level is not a count.
40562
+ *
40563
+ * A read that fails yields an EMPTY map, so the lane draws no level rather
40564
+ * than a wrong one: the density counts beside it are unaffected, and a
40565
+ * missing bar is the honest rendering of "not measured" (D431).
40566
+ */
40567
+ async audioDbfsPerBucket(q) {
40568
+ try {
40569
+ return audioDbfsByBucket((await this.queryAudio({
40570
+ deviceId: q.deviceId,
40571
+ since: q.since,
40572
+ until: q.until,
40573
+ projection: "slim"
40574
+ })).map((e) => ({
40575
+ timestamp: e.timestamp,
40576
+ dbfs: e.dbfs
40577
+ })), q.since, q.bucketMs);
40578
+ } catch (err) {
40579
+ this.logger.warn("density: audio level aggregation failed — the lane will draw no level", {
40580
+ tags: { deviceId: q.deviceId },
40581
+ meta: { error: String(err) }
40582
+ });
40583
+ return /* @__PURE__ */ new Map();
40584
+ }
40585
+ }
40586
+ /**
40404
40587
  * Age-evict the TRACKLESS event kinds — motion and audio.
40405
40588
  *
40406
40589
  * **Object events are not here, and that is the point.** An object event is a
@@ -72688,12 +72871,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72688
72871
  */
72689
72872
  buildTimelapsePorts(api, stores) {
72690
72873
  return {
72691
- getAvailability: async ({ deviceId, fromMs, toMs }) => {
72692
- return (await api.recording.getAvailability.query({
72874
+ getAvailability: async ({ deviceId, fromMs, toMs, profile }) => {
72875
+ const availability = await api.recording.getAvailability.query({
72693
72876
  deviceId,
72694
72877
  fromMs,
72695
- toMs
72696
- })).ranges;
72878
+ toMs,
72879
+ ...profile !== void 0 ? { profile } : {}
72880
+ });
72881
+ return {
72882
+ ranges: availability.ranges,
72883
+ profilesWithFootage: availability.profilesWithFootage
72884
+ };
72697
72885
  },
72698
72886
  listTracks: async ({ deviceId, since, until }) => {
72699
72887
  return (await stores.trackStore.queryHistorical({
@@ -77480,6 +77668,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
77480
77668
  }
77481
77669
  });
77482
77670
  });
77671
+ p.setBirthZoneTrailObserver((trail) => {
77672
+ this.ctx.logger.info("track born carrying zones it crossed before it was a track", {
77673
+ tags: { deviceId },
77674
+ meta: {
77675
+ source,
77676
+ ...trail
77677
+ }
77678
+ });
77679
+ });
77680
+ p.setZoneRefusalObserver((observation) => {
77681
+ this.ctx.logger.info("detection refused inside a watched zone — not notified", {
77682
+ tags: { deviceId },
77683
+ meta: {
77684
+ source,
77685
+ ...observation
77686
+ }
77687
+ });
77688
+ });
77483
77689
  p.setSpawnBesideCoastingObserver((record) => {
77484
77690
  this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
77485
77691
  tags: { deviceId },
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BmDXGilB.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-JPlVJb-8.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.216",
3
+ "version": "1.2.218",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",