@camstack/addon-post-analysis 1.2.201 → 1.2.203

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.
@@ -12525,6 +12525,9 @@ var QueryFilterSchema = object({
12525
12525
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
12526
12526
  /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
12527
12527
  whereNot: record(string(), unknown()).optional(),
12528
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
12529
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
12530
+ whereNotIn: record(string(), array(unknown())).optional(),
12528
12531
  orderBy: object({
12529
12532
  field: string(),
12530
12533
  direction: _enum(["asc", "desc"])
@@ -12545,7 +12548,8 @@ var MutationFilterSchema = object({
12545
12548
  where: record(string(), unknown()).optional(),
12546
12549
  whereIn: record(string(), array(unknown())).optional(),
12547
12550
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
12548
- whereNot: record(string(), unknown()).optional()
12551
+ whereNot: record(string(), unknown()).optional(),
12552
+ whereNotIn: record(string(), array(unknown())).optional()
12549
12553
  });
12550
12554
  /**
12551
12555
  * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
@@ -19884,10 +19888,12 @@ var TrackSourceSchema = _enum([
19884
19888
  * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
19885
19889
  * a deliberate action of the retrain page, not a side effect of a checkbox.
19886
19890
  *
19887
- * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
19888
- * the store's filter language has only positive equality and `whereIn` no
19889
- * negation, no IS NULL so a NULL would be unselectable by ANY predicate and
19890
- * would make the entire pre-column history immortal in one deploy.
19891
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` so that
19892
+ * every row is selectable by a positive predicate. (The filter language has
19893
+ * since grown the NULL-safe `whereNot` / `whereNotIn` and an `IS NULL` reading
19894
+ * of `where: { f: null }`, which is how {@link TrackSourceSchema} can be
19895
+ * filtered on a NULLABLE column — see `excludeSources`. It did not when this
19896
+ * column was designed, and a NOT NULL column is still the better shape.)
19891
19897
  */
19892
19898
  var RetrainStatusSchema = _enum([
19893
19899
  "none",
@@ -20623,7 +20629,9 @@ var RecentTracksQueryInput = object({
20623
20629
  * whose class list is unreadable are kept, and the client's rule stays the
20624
20630
  * exact one.
20625
20631
  */
20626
- classes: array(string()).optional()
20632
+ classes: array(string()).optional(),
20633
+ /** See {@link ExcludeSourcesDoc}. */
20634
+ excludeSources: array(TrackSourceSchema).optional()
20627
20635
  });
20628
20636
  /**
20629
20637
  * A Summary (D359): the per-camera session envelope that references tracks.
@@ -21065,7 +21073,9 @@ var pipelineAnalyticsCapability = {
21065
21073
  * selected is an operator who has not narrowed anything, and an empty
21066
21074
  * timeline would read as a camera that saw nothing.
21067
21075
  */
21068
- classes: array(string()).optional()
21076
+ classes: array(string()).optional(),
21077
+ /** See {@link ExcludeSourcesDoc}. */
21078
+ excludeSources: array(TrackSourceSchema).optional()
21069
21079
  }), array(TrackSchema).readonly()),
21070
21080
  /**
21071
21081
  * Batched cluster-wide track listing — ONE call for the events page /
@@ -12556,6 +12556,9 @@ var QueryFilterSchema = object({
12556
12556
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
12557
12557
  /** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
12558
12558
  whereNot: record(string(), unknown()).optional(),
12559
+ /** NULL-safe exclusion of a SET — `whereNot` for more than one value. An
12560
+ * empty list excludes nothing. See `QueryFilter.whereNotIn`. */
12561
+ whereNotIn: record(string(), array(unknown())).optional(),
12559
12562
  orderBy: object({
12560
12563
  field: string(),
12561
12564
  direction: _enum(["asc", "desc"])
@@ -12576,7 +12579,8 @@ var MutationFilterSchema = object({
12576
12579
  where: record(string(), unknown()).optional(),
12577
12580
  whereIn: record(string(), array(unknown())).optional(),
12578
12581
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
12579
- whereNot: record(string(), unknown()).optional()
12582
+ whereNot: record(string(), unknown()).optional(),
12583
+ whereNotIn: record(string(), array(unknown())).optional()
12580
12584
  });
12581
12585
  /**
12582
12586
  * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
@@ -19915,10 +19919,12 @@ var TrackSourceSchema = _enum([
19915
19919
  * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
19916
19920
  * a deliberate action of the retrain page, not a side effect of a checkbox.
19917
19921
  *
19918
- * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
19919
- * the store's filter language has only positive equality and `whereIn` no
19920
- * negation, no IS NULL so a NULL would be unselectable by ANY predicate and
19921
- * would make the entire pre-column history immortal in one deploy.
19922
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` so that
19923
+ * every row is selectable by a positive predicate. (The filter language has
19924
+ * since grown the NULL-safe `whereNot` / `whereNotIn` and an `IS NULL` reading
19925
+ * of `where: { f: null }`, which is how {@link TrackSourceSchema} can be
19926
+ * filtered on a NULLABLE column — see `excludeSources`. It did not when this
19927
+ * column was designed, and a NOT NULL column is still the better shape.)
19922
19928
  */
19923
19929
  var RetrainStatusSchema = _enum([
19924
19930
  "none",
@@ -20654,7 +20660,9 @@ var RecentTracksQueryInput = object({
20654
20660
  * whose class list is unreadable are kept, and the client's rule stays the
20655
20661
  * exact one.
20656
20662
  */
20657
- classes: array(string()).optional()
20663
+ classes: array(string()).optional(),
20664
+ /** See {@link ExcludeSourcesDoc}. */
20665
+ excludeSources: array(TrackSourceSchema).optional()
20658
20666
  });
20659
20667
  /**
20660
20668
  * A Summary (D359): the per-camera session envelope that references tracks.
@@ -21096,7 +21104,9 @@ var pipelineAnalyticsCapability = {
21096
21104
  * selected is an operator who has not narrowed anything, and an empty
21097
21105
  * timeline would read as a camera that saw nothing.
21098
21106
  */
21099
- classes: array(string()).optional()
21107
+ classes: array(string()).optional(),
21108
+ /** See {@link ExcludeSourcesDoc}. */
21109
+ excludeSources: array(TrackSourceSchema).optional()
21100
21110
  }), array(TrackSchema).readonly()),
21101
21111
  /**
21102
21112
  * Batched cluster-wide track listing — ONE call for the events page /
@@ -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-DbgfbjZC.js");
5
+ const require_dist = require("../dist-rqvrMbFv.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 { F as PoolMemoryWatchdog, Pt as BaseAddon, Tt as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, dt as hfModelUrl, vt as parseProcStatus } from "../dist-1xB8qj2h.mjs";
1
+ import { F as PoolMemoryWatchdog, Pt as BaseAddon, Tt as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, dt as hfModelUrl, vt as parseProcStatus } from "../dist-B2bqI0T1.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.84",
6
+ version: "1.2.85",
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.164",
21
+ version: "1.2.165",
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.132",
36
+ version: "1.2.133",
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.164",
39
+ version: "1.2.165",
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.84",
48
+ version: "1.2.85",
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.132",
84
+ version: "1.2.133",
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-DbgfbjZC.js");
5
+ const require_dist = require("../dist-rqvrMbFv.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");
@@ -35739,12 +35739,7 @@ var FrameProcessor = class {
35739
35739
  trackId: e.detection.trackId,
35740
35740
  className: e.detection.class,
35741
35741
  confidence: e.detection.score,
35742
- bbox: td ? { ...td.bbox } : {
35743
- x: 0,
35744
- y: 0,
35745
- w: 0,
35746
- h: 0
35747
- },
35742
+ ...td ? { bbox: { ...td.bbox } } : {},
35748
35743
  zones,
35749
35744
  state,
35750
35745
  ...crossing !== void 0 ? { zoneCrossing: crossing } : {},
@@ -39658,6 +39653,58 @@ function isShortMotionlessTrack(lifeMs, movedPx, className) {
39658
39653
  return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
39659
39654
  }
39660
39655
  //#endregion
39656
+ //#region src/pipeline-analytics/store/track-source-filter.ts
39657
+ /**
39658
+ * The SQL predicate behind `excludeSources` — track provenance as a real query
39659
+ * filter (see `ExcludeSourcesDoc` in the cap for why the query needs one).
39660
+ *
39661
+ * ## The column is nullable, and that decides everything
39662
+ *
39663
+ * `persistCompleted` writes `source` only when the track carries one, and a
39664
+ * pipeline track does not: on the live hub every pipeline row's `source` is
39665
+ * NULL and only the synthetic rows (`audio`, `sensor`) carry a value. So there
39666
+ * are two exact compilations and the choice between them is purely "is
39667
+ * `pipeline` one of the sources being excluded":
39668
+ *
39669
+ * - **`pipeline` kept** → NULL-safe `whereNotIn`. `(source IS NULL OR source
39670
+ * NOT IN (…))` keeps every pipeline row without naming a value they do not
39671
+ * carry, and keeps any source added after this shipped — an exclusion list
39672
+ * can only hide what it names.
39673
+ * - **`pipeline` excluded** → positive `whereIn` over what survives. NULL is
39674
+ * then exactly the set being dropped, and a NULL-safe exclusion would keep
39675
+ * precisely the rows it was asked to remove. (Excluding EVERY known source
39676
+ * is the degenerate case: `whereIn` with an empty list, which the compiler
39677
+ * reads as "no filter", so it is answered here instead — see below.)
39678
+ *
39679
+ * Both are exact. Neither leaves a row's fate to a default, which is the whole
39680
+ * point: the class filter this replaces was a SUPERSET that kept every row it
39681
+ * could not read, and that is what made "Sensor" and "Audio" select the same
39682
+ * 116 rows on the live hub.
39683
+ */
39684
+ /** Every source the build knows. Derived from the schema, never restated — a
39685
+ * source added to the enum must not need a second edit here to be filterable. */
39686
+ var KNOWN_SOURCES = require_dist.TrackSourceSchema.options;
39687
+ /**
39688
+ * Compile `excludeSources` into a store filter fragment.
39689
+ *
39690
+ * Pure. Unknown / duplicate entries are normalised away: the input crosses a
39691
+ * wire from a client whose enum may be older or newer than this one.
39692
+ */
39693
+ function trackSourceFilter(excludeSources) {
39694
+ const excluded = new Set((excludeSources ?? []).filter((s) => KNOWN_SOURCES.includes(s)));
39695
+ if (excluded.size === 0) return { kind: "none" };
39696
+ const kept = KNOWN_SOURCES.filter((s) => !excluded.has(s));
39697
+ if (kept.length === 0) return { kind: "impossible" };
39698
+ if (!excluded.has("pipeline")) return {
39699
+ kind: "filter",
39700
+ filter: { whereNotIn: { source: [...excluded] } }
39701
+ };
39702
+ return {
39703
+ kind: "filter",
39704
+ filter: { whereIn: { source: kept } }
39705
+ };
39706
+ }
39707
+ //#endregion
39661
39708
  //#region src/pipeline-analytics/store/track-store.ts
39662
39709
  /**
39663
39710
  * WHY a track's row was written EARLY — i.e. by {@link TrackStore.closeActive}
@@ -41227,6 +41274,9 @@ var TrackStore = class {
41227
41274
  const timeBetween = params.since !== void 0 || params.until !== void 0 ? { firstSeen: [params.since ?? 0, params.until ?? Date.now()] } : {};
41228
41275
  const stationaryExclusion = params.excludeStationaryPromotions === true ? { whereNot: { closedReason: "stationary-promotion" } } : {};
41229
41276
  const classFilter = params.classes !== void 0 && params.classes.length > 0 ? { whereJsonArrayAny: { classes: [...params.classes] } } : {};
41277
+ const source = trackSourceFilter(params.excludeSources);
41278
+ if (source.kind === "impossible") return [];
41279
+ const sourceFilter = source.kind === "filter" ? source.filter : {};
41230
41280
  if (params.zone === void 0) return (await this.store.query.query({
41231
41281
  collection: TRACKS_COLLECTION,
41232
41282
  filter: {
@@ -41234,6 +41284,7 @@ var TrackStore = class {
41234
41284
  ...Object.keys(timeBetween).length > 0 ? { whereBetween: timeBetween } : {},
41235
41285
  ...stationaryExclusion,
41236
41286
  ...classFilter,
41287
+ ...sourceFilter,
41237
41288
  orderBy: {
41238
41289
  field: "firstSeen",
41239
41290
  direction: "desc"
@@ -41250,6 +41301,7 @@ var TrackStore = class {
41250
41301
  where: { deviceId: params.deviceId },
41251
41302
  ...stationaryExclusion,
41252
41303
  ...classFilter,
41304
+ ...sourceFilter,
41253
41305
  whereBetween: {
41254
41306
  ...timeBetween,
41255
41307
  envMinX: [-1e6, bounds.maxX],
@@ -41271,6 +41323,7 @@ var TrackStore = class {
41271
41323
  where: { deviceId: params.deviceId },
41272
41324
  ...stationaryExclusion,
41273
41325
  ...classFilter,
41326
+ ...sourceFilter,
41274
41327
  ...Object.keys(timeBetween).length > 0 ? { whereBetween: timeBetween } : {},
41275
41328
  orderBy: {
41276
41329
  field: "firstSeen",
@@ -41368,6 +41421,12 @@ var TrackStore = class {
41368
41421
  page: [],
41369
41422
  nextCursor: null
41370
41423
  };
41424
+ const source = trackSourceFilter(params.excludeSources);
41425
+ if (source.kind === "impossible") return {
41426
+ page: [],
41427
+ nextCursor: null
41428
+ };
41429
+ const sourceFilter = source.kind === "filter" ? source.filter : {};
41371
41430
  const requested = Math.min(Math.max(params.limit ?? RECENT_DEFAULT_LIMIT, 1), RECENT_MAX_LIMIT);
41372
41431
  /**
41373
41432
  * The bound is on `deviceCount x limit`, because that — not `limit` — is
@@ -41400,6 +41459,7 @@ var TrackStore = class {
41400
41459
  where: { deviceId },
41401
41460
  ...stationaryExclusion,
41402
41461
  ...classFilter,
41462
+ ...sourceFilter,
41403
41463
  whereBetween: { lastSeen: range },
41404
41464
  orderBy: {
41405
41465
  field: "lastSeen",
@@ -63607,6 +63667,27 @@ var SyntheticTrackMaterializer = class {
63607
63667
  trackId,
63608
63668
  deviceId: input.cameraId,
63609
63669
  className: input.kind,
63670
+ /**
63671
+ * The accumulated-class column, seeded with the one class this row will
63672
+ * ever have. A pipeline track gets `classes: [className]` on its first
63673
+ * observation; a synthetic row was left with the column NULL, and
63674
+ * `whereJsonArrayAny` — the operator `classes` compiles to — is a
63675
+ * deliberate SUPERSET that KEEPS a row whose column it cannot read.
63676
+ *
63677
+ * So every class selection returned every synthetic row and no class
63678
+ * selection ever SELECTED one: measured on the live hub, `classes:
63679
+ * ['person']` came back with 66 contact markers and 29 audio markers
63680
+ * attached, `classes: ['contact']` returned no contact row by its class
63681
+ * (only the 116 rows the escape hatch keeps), and a class that exists
63682
+ * nowhere answered those same 116. That is the "the sensor and control
63683
+ * filters do nothing" report, exactly.
63684
+ *
63685
+ * Writing it here makes a synthetic row selectable BY ITS KIND like any
63686
+ * other. Rows already on disk keep a NULL column and stay in the
63687
+ * superset — which is what `excludeSources` is for: provenance is not a
63688
+ * class, and the source filter is exact for the whole history.
63689
+ */
63690
+ classes: [input.kind],
63610
63691
  source: input.source,
63611
63692
  ...input.producingDeviceName !== void 0 ? { producingDeviceName: input.producingDeviceName } : {},
63612
63693
  ...input.audioLabels !== void 0 && input.audioLabels.length > 0 ? { audioLabels: input.audioLabels } : {},
@@ -71693,6 +71774,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71693
71774
  if (this.eventMediaDispatcher && frameHandle) {
71694
71775
  const mediaEvents = [...result.objectEvents, ...result.appearanceEvents];
71695
71776
  const childCropsByEvent = buildEventChildCrops(mediaEvents, frame.detections);
71777
+ const uncroppable = mediaEvents.filter((e) => e.bbox === void 0 || e.bbox.w <= 0 || e.bbox.h <= 0);
71778
+ if (uncroppable.length > 0) this.ctx.logger.debug("event media: no crop for events with no box in this frame (a departed track has nothing to cut)", {
71779
+ tags: { deviceId },
71780
+ meta: {
71781
+ events: uncroppable.length,
71782
+ states: [...new Set(uncroppable.map((e) => e.state ?? "unknown"))],
71783
+ zeroArea: uncroppable.filter((e) => e.bbox !== void 0).length
71784
+ }
71785
+ });
71696
71786
  const eventTargets = mediaEvents.filter((e) => e.bbox !== void 0 && e.bbox.w > 0 && e.bbox.h > 0).map((e) => {
71697
71787
  const childCrops = childCropsByEvent.get(e.id);
71698
71788
  return {
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-1xB8qj2h.mjs";
1
+ import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-B2bqI0T1.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";
@@ -35714,12 +35714,7 @@ var FrameProcessor = class {
35714
35714
  trackId: e.detection.trackId,
35715
35715
  className: e.detection.class,
35716
35716
  confidence: e.detection.score,
35717
- bbox: td ? { ...td.bbox } : {
35718
- x: 0,
35719
- y: 0,
35720
- w: 0,
35721
- h: 0
35722
- },
35717
+ ...td ? { bbox: { ...td.bbox } } : {},
35723
35718
  zones,
35724
35719
  state,
35725
35720
  ...crossing !== void 0 ? { zoneCrossing: crossing } : {},
@@ -39633,6 +39628,58 @@ function isShortMotionlessTrack(lifeMs, movedPx, className) {
39633
39628
  return lifeMs <= 25e3 && movedPx <= motionlessMaxPxForClass(className);
39634
39629
  }
39635
39630
  //#endregion
39631
+ //#region src/pipeline-analytics/store/track-source-filter.ts
39632
+ /**
39633
+ * The SQL predicate behind `excludeSources` — track provenance as a real query
39634
+ * filter (see `ExcludeSourcesDoc` in the cap for why the query needs one).
39635
+ *
39636
+ * ## The column is nullable, and that decides everything
39637
+ *
39638
+ * `persistCompleted` writes `source` only when the track carries one, and a
39639
+ * pipeline track does not: on the live hub every pipeline row's `source` is
39640
+ * NULL and only the synthetic rows (`audio`, `sensor`) carry a value. So there
39641
+ * are two exact compilations and the choice between them is purely "is
39642
+ * `pipeline` one of the sources being excluded":
39643
+ *
39644
+ * - **`pipeline` kept** → NULL-safe `whereNotIn`. `(source IS NULL OR source
39645
+ * NOT IN (…))` keeps every pipeline row without naming a value they do not
39646
+ * carry, and keeps any source added after this shipped — an exclusion list
39647
+ * can only hide what it names.
39648
+ * - **`pipeline` excluded** → positive `whereIn` over what survives. NULL is
39649
+ * then exactly the set being dropped, and a NULL-safe exclusion would keep
39650
+ * precisely the rows it was asked to remove. (Excluding EVERY known source
39651
+ * is the degenerate case: `whereIn` with an empty list, which the compiler
39652
+ * reads as "no filter", so it is answered here instead — see below.)
39653
+ *
39654
+ * Both are exact. Neither leaves a row's fate to a default, which is the whole
39655
+ * point: the class filter this replaces was a SUPERSET that kept every row it
39656
+ * could not read, and that is what made "Sensor" and "Audio" select the same
39657
+ * 116 rows on the live hub.
39658
+ */
39659
+ /** Every source the build knows. Derived from the schema, never restated — a
39660
+ * source added to the enum must not need a second edit here to be filterable. */
39661
+ var KNOWN_SOURCES = TrackSourceSchema.options;
39662
+ /**
39663
+ * Compile `excludeSources` into a store filter fragment.
39664
+ *
39665
+ * Pure. Unknown / duplicate entries are normalised away: the input crosses a
39666
+ * wire from a client whose enum may be older or newer than this one.
39667
+ */
39668
+ function trackSourceFilter(excludeSources) {
39669
+ const excluded = new Set((excludeSources ?? []).filter((s) => KNOWN_SOURCES.includes(s)));
39670
+ if (excluded.size === 0) return { kind: "none" };
39671
+ const kept = KNOWN_SOURCES.filter((s) => !excluded.has(s));
39672
+ if (kept.length === 0) return { kind: "impossible" };
39673
+ if (!excluded.has("pipeline")) return {
39674
+ kind: "filter",
39675
+ filter: { whereNotIn: { source: [...excluded] } }
39676
+ };
39677
+ return {
39678
+ kind: "filter",
39679
+ filter: { whereIn: { source: kept } }
39680
+ };
39681
+ }
39682
+ //#endregion
39636
39683
  //#region src/pipeline-analytics/store/track-store.ts
39637
39684
  /**
39638
39685
  * WHY a track's row was written EARLY — i.e. by {@link TrackStore.closeActive}
@@ -41202,6 +41249,9 @@ var TrackStore = class {
41202
41249
  const timeBetween = params.since !== void 0 || params.until !== void 0 ? { firstSeen: [params.since ?? 0, params.until ?? Date.now()] } : {};
41203
41250
  const stationaryExclusion = params.excludeStationaryPromotions === true ? { whereNot: { closedReason: "stationary-promotion" } } : {};
41204
41251
  const classFilter = params.classes !== void 0 && params.classes.length > 0 ? { whereJsonArrayAny: { classes: [...params.classes] } } : {};
41252
+ const source = trackSourceFilter(params.excludeSources);
41253
+ if (source.kind === "impossible") return [];
41254
+ const sourceFilter = source.kind === "filter" ? source.filter : {};
41205
41255
  if (params.zone === void 0) return (await this.store.query.query({
41206
41256
  collection: TRACKS_COLLECTION,
41207
41257
  filter: {
@@ -41209,6 +41259,7 @@ var TrackStore = class {
41209
41259
  ...Object.keys(timeBetween).length > 0 ? { whereBetween: timeBetween } : {},
41210
41260
  ...stationaryExclusion,
41211
41261
  ...classFilter,
41262
+ ...sourceFilter,
41212
41263
  orderBy: {
41213
41264
  field: "firstSeen",
41214
41265
  direction: "desc"
@@ -41225,6 +41276,7 @@ var TrackStore = class {
41225
41276
  where: { deviceId: params.deviceId },
41226
41277
  ...stationaryExclusion,
41227
41278
  ...classFilter,
41279
+ ...sourceFilter,
41228
41280
  whereBetween: {
41229
41281
  ...timeBetween,
41230
41282
  envMinX: [-1e6, bounds.maxX],
@@ -41246,6 +41298,7 @@ var TrackStore = class {
41246
41298
  where: { deviceId: params.deviceId },
41247
41299
  ...stationaryExclusion,
41248
41300
  ...classFilter,
41301
+ ...sourceFilter,
41249
41302
  ...Object.keys(timeBetween).length > 0 ? { whereBetween: timeBetween } : {},
41250
41303
  orderBy: {
41251
41304
  field: "firstSeen",
@@ -41343,6 +41396,12 @@ var TrackStore = class {
41343
41396
  page: [],
41344
41397
  nextCursor: null
41345
41398
  };
41399
+ const source = trackSourceFilter(params.excludeSources);
41400
+ if (source.kind === "impossible") return {
41401
+ page: [],
41402
+ nextCursor: null
41403
+ };
41404
+ const sourceFilter = source.kind === "filter" ? source.filter : {};
41346
41405
  const requested = Math.min(Math.max(params.limit ?? RECENT_DEFAULT_LIMIT, 1), RECENT_MAX_LIMIT);
41347
41406
  /**
41348
41407
  * The bound is on `deviceCount x limit`, because that — not `limit` — is
@@ -41375,6 +41434,7 @@ var TrackStore = class {
41375
41434
  where: { deviceId },
41376
41435
  ...stationaryExclusion,
41377
41436
  ...classFilter,
41437
+ ...sourceFilter,
41378
41438
  whereBetween: { lastSeen: range },
41379
41439
  orderBy: {
41380
41440
  field: "lastSeen",
@@ -63533,6 +63593,27 @@ var SyntheticTrackMaterializer = class {
63533
63593
  trackId,
63534
63594
  deviceId: input.cameraId,
63535
63595
  className: input.kind,
63596
+ /**
63597
+ * The accumulated-class column, seeded with the one class this row will
63598
+ * ever have. A pipeline track gets `classes: [className]` on its first
63599
+ * observation; a synthetic row was left with the column NULL, and
63600
+ * `whereJsonArrayAny` — the operator `classes` compiles to — is a
63601
+ * deliberate SUPERSET that KEEPS a row whose column it cannot read.
63602
+ *
63603
+ * So every class selection returned every synthetic row and no class
63604
+ * selection ever SELECTED one: measured on the live hub, `classes:
63605
+ * ['person']` came back with 66 contact markers and 29 audio markers
63606
+ * attached, `classes: ['contact']` returned no contact row by its class
63607
+ * (only the 116 rows the escape hatch keeps), and a class that exists
63608
+ * nowhere answered those same 116. That is the "the sensor and control
63609
+ * filters do nothing" report, exactly.
63610
+ *
63611
+ * Writing it here makes a synthetic row selectable BY ITS KIND like any
63612
+ * other. Rows already on disk keep a NULL column and stay in the
63613
+ * superset — which is what `excludeSources` is for: provenance is not a
63614
+ * class, and the source filter is exact for the whole history.
63615
+ */
63616
+ classes: [input.kind],
63536
63617
  source: input.source,
63537
63618
  ...input.producingDeviceName !== void 0 ? { producingDeviceName: input.producingDeviceName } : {},
63538
63619
  ...input.audioLabels !== void 0 && input.audioLabels.length > 0 ? { audioLabels: input.audioLabels } : {},
@@ -71619,6 +71700,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71619
71700
  if (this.eventMediaDispatcher && frameHandle) {
71620
71701
  const mediaEvents = [...result.objectEvents, ...result.appearanceEvents];
71621
71702
  const childCropsByEvent = buildEventChildCrops(mediaEvents, frame.detections);
71703
+ const uncroppable = mediaEvents.filter((e) => e.bbox === void 0 || e.bbox.w <= 0 || e.bbox.h <= 0);
71704
+ if (uncroppable.length > 0) this.ctx.logger.debug("event media: no crop for events with no box in this frame (a departed track has nothing to cut)", {
71705
+ tags: { deviceId },
71706
+ meta: {
71707
+ events: uncroppable.length,
71708
+ states: [...new Set(uncroppable.map((e) => e.state ?? "unknown"))],
71709
+ zeroArea: uncroppable.filter((e) => e.bbox !== void 0).length
71710
+ }
71711
+ });
71622
71712
  const eventTargets = mediaEvents.filter((e) => e.bbox !== void 0 && e.bbox.w > 0 && e.bbox.h > 0).map((e) => {
71623
71713
  const childCrops = childCropsByEvent.get(e.id);
71624
71714
  return {
@@ -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-DwFi68p5.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BhGMdGiF.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.201",
3
+ "version": "1.2.203",
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",