@camstack/addon-decoder-nodeav 1.2.26 → 1.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +184 -34
  2. package/dist/index.mjs +184 -34
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5805,6 +5805,13 @@ var BaseAddon = class {
5805
5805
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5806
5806
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5807
5807
  _registeredCapNames = [];
5808
+ /**
5809
+ * True only after `readAddonStore` actually answered. Constructor
5810
+ * defaults look like stored config when the store is down — a forked
5811
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5812
+ * mode, 2026-08-25) is not "the operator chose this".
5813
+ */
5814
+ settingsStoreReady = false;
5808
5815
  /** Default config values. Provided via constructor. */
5809
5816
  defaults;
5810
5817
  constructor(defaults) {
@@ -6205,7 +6212,9 @@ var BaseAddon = class {
6205
6212
  ];
6206
6213
  let lastErr;
6207
6214
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6208
- return await settings.readAddonStore() ?? {};
6215
+ const stored = await settings.readAddonStore() ?? {};
6216
+ this.settingsStoreReady = true;
6217
+ return stored;
6209
6218
  } catch (err) {
6210
6219
  lastErr = err;
6211
6220
  const msg = err instanceof Error ? err.message : String(err);
@@ -6213,6 +6222,7 @@ var BaseAddon = class {
6213
6222
  if (attempt === delaysMs.length) break;
6214
6223
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6215
6224
  }
6225
+ this.settingsStoreReady = false;
6216
6226
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6217
6227
  return {};
6218
6228
  }
@@ -8112,6 +8122,12 @@ var ModelVariantGroupSchema = object({
8112
8122
  */
8113
8123
  resolution: number().int().positive().optional()
8114
8124
  });
8125
+ var ModelProviderIdSchema = _enum([
8126
+ "camstack",
8127
+ "frigate",
8128
+ "scrypted",
8129
+ "custom"
8130
+ ]);
8115
8131
  var ModelCatalogEntrySchema = object({
8116
8132
  id: string(),
8117
8133
  name: string(),
@@ -8209,6 +8225,12 @@ var ModelCatalogEntrySchema = object({
8209
8225
  */
8210
8226
  group: ModelVariantGroupSchema.optional(),
8211
8227
  /**
8228
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8229
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8230
+ * persisted before this field existed (`inferModelProvider` fills those).
8231
+ */
8232
+ provider: ModelProviderIdSchema.optional(),
8233
+ /**
8212
8234
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8213
8235
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8214
8236
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11866,6 +11888,27 @@ var LinkedDeviceSchema = object({
11866
11888
  features: array(string()),
11867
11889
  producesTrackedEvents: boolean().optional()
11868
11890
  });
11891
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11892
+ * The batch answer needs the tag; the single-device answer already has it
11893
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11894
+ var LinkedDevicesForDeviceSchema = object({
11895
+ deviceId: number(),
11896
+ mode: LinkedDevicesModeSchema,
11897
+ devices: array(LinkedDeviceSchema)
11898
+ });
11899
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11900
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11901
+ * object literal is exactly how the three drift apart. */
11902
+ var DeviceBindingsForDeviceSchema = object({
11903
+ deviceId: number(),
11904
+ entries: array(object({
11905
+ capName: string(),
11906
+ kind: _enum(["native", "wrapped"]),
11907
+ providerAddonId: string(),
11908
+ providerNodeId: string(),
11909
+ nativeAddonId: string()
11910
+ }))
11911
+ });
11869
11912
  var SavedDeviceRowSchema = object({
11870
11913
  /** Numeric id reserved at allocateDeviceId time. */
11871
11914
  id: number(),
@@ -12091,11 +12134,25 @@ method(object({
12091
12134
  projection: _enum(["full", "slim"]).optional(),
12092
12135
  /** Return only camera devices. Filtering server-side instead of
12093
12136
  * shipping 293 rows to find 12. */
12094
- isCamera: boolean().optional()
12137
+ isCamera: boolean().optional(),
12138
+ /**
12139
+ * Return only these device ids. For the caller that already KNOWS the
12140
+ * handful it wants and needs a field the id-bearing answer does not
12141
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12142
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12143
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12144
+ * refetches on the reconcile interval, on a phone.
12145
+ *
12146
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12147
+ * keys rather than rejecting them (verified against the live hub
12148
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12149
+ * it answers today and the caller filters as it already does.
12150
+ */
12151
+ deviceIds: array(number()).optional()
12095
12152
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12096
12153
  mode: LinkedDevicesModeSchema,
12097
12154
  devices: array(LinkedDeviceSchema)
12098
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12155
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12099
12156
  deviceId: number(),
12100
12157
  values: record(string(), unknown())
12101
12158
  }), object({ success: literal(true) }), {
@@ -12122,25 +12179,7 @@ method(object({
12122
12179
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12123
12180
  kind: "mutation",
12124
12181
  auth: "admin"
12125
- }), method(object({ deviceId: number() }), object({
12126
- deviceId: number(),
12127
- entries: array(object({
12128
- capName: string(),
12129
- kind: _enum(["native", "wrapped"]),
12130
- providerAddonId: string(),
12131
- providerNodeId: string(),
12132
- nativeAddonId: string()
12133
- }))
12134
- })), method(object({}), array(object({
12135
- deviceId: number(),
12136
- entries: array(object({
12137
- capName: string(),
12138
- kind: _enum(["native", "wrapped"]),
12139
- providerAddonId: string(),
12140
- providerNodeId: string(),
12141
- nativeAddonId: string()
12142
- }))
12143
- }))), method(object({
12182
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12144
12183
  deviceId: number(),
12145
12184
  capName: string(),
12146
12185
  wrapperAddonId: string(),
@@ -14512,12 +14551,15 @@ var NcOccupancyConditionSchema = object({
14512
14551
  * there is no second switch that can disagree with the first and every rule
14513
14552
  * authored before the decision migrates for free (`audioModeOf`):
14514
14553
  *
14515
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14516
- * classifier labels with one of them. No window, no percentage:
14517
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14518
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14519
- * the analyzer's (`classificationMinScore`, per device) a label only
14520
- * reaches this condition if the classifier was already confident enough.
14554
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14555
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14556
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14557
+ * frames is the wrong question for a classifier that labels 1–3 frames
14558
+ * per episode. The count window is the brake that drops a single-frame
14559
+ * false positive; the rule's own `throttle` cooldown is the other. The
14560
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14561
+ * per device) — a label only reaches this condition if the classifier was
14562
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14521
14563
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14522
14564
  * the condition: at least `hitPercent`% of the samples over
14523
14565
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14544,14 +14586,22 @@ var NcOccupancyConditionSchema = object({
14544
14586
  * an operator who typed `dog` mean the same thing.
14545
14587
  */
14546
14588
  var NcAudioConditionSchema = object({
14547
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14589
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14548
14590
  labels: array(string().min(1)).min(1).optional(),
14549
14591
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14550
14592
  dbThreshold: number().min(-96).max(0).optional(),
14551
14593
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14552
14594
  hitPercent: number().int().min(1).max(100).default(60),
14553
14595
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14554
- samplingSeconds: number().int().min(1).max(300).default(10)
14596
+ samplingSeconds: number().int().min(1).max(300).default(10),
14597
+ /**
14598
+ * LABEL MODE: how many labelled frames must land inside
14599
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14600
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14601
+ */
14602
+ confirmHits: number().int().min(1).max(20).optional(),
14603
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14604
+ confirmWindowSec: number().int().min(1).max(60).optional()
14555
14605
  });
14556
14606
  /**
14557
14607
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16925,6 +16975,46 @@ var RecentTracksPageSchema = object({
16925
16975
  /** Cursor for the next page, or null when this page is the last. */
16926
16976
  nextCursor: string().nullable()
16927
16977
  });
16978
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16979
+ var LIST_GROUPS_MAX_LIMIT = 100;
16980
+ var AnalyticsGroupRecordSchema = object({
16981
+ id: string(),
16982
+ deviceId: number().int(),
16983
+ openedAt: number().int(),
16984
+ closedAt: number().int(),
16985
+ timestamp: number().int(),
16986
+ memberCount: number().int(),
16987
+ memberTrackIds: array(string()).readonly(),
16988
+ className: string(),
16989
+ classes: array(string()).readonly(),
16990
+ /** Relative event-media path, or null when the group has no picture yet. */
16991
+ mediaUrl: string().nullable(),
16992
+ singleton: boolean()
16993
+ });
16994
+ var AnalyticsGroupMemberSchema = object({
16995
+ trackId: string(),
16996
+ deviceId: number().int(),
16997
+ className: string(),
16998
+ firstSeen: number().int(),
16999
+ lastSeen: number().int(),
17000
+ mediaUrl: string().nullable()
17001
+ });
17002
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17003
+ var ListGroupsQueryInput = object({
17004
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17005
+ deviceIds: array(number()),
17006
+ /** Window lower bound on `closedAt` (inclusive). */
17007
+ since: number().optional(),
17008
+ /** Window upper bound on `openedAt` (inclusive). */
17009
+ until: number().optional(),
17010
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17011
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17012
+ cursor: string().optional()
17013
+ });
17014
+ var ListGroupsPageSchema = object({
17015
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17016
+ nextCursor: string().nullable()
17017
+ });
16928
17018
  var KeyEventQueryInput = object({
16929
17019
  deviceId: number(),
16930
17020
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17000,7 +17090,9 @@ var TrackCascadeCountsSchema = object({
17000
17090
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17001
17091
  plates: number().int(),
17002
17092
  /** Per-track CLIP search vectors removed (best-effort). */
17003
- embeddings: number().int()
17093
+ embeddings: number().int(),
17094
+ /** Group membership + group rows removed with their last member (best-effort). */
17095
+ groups: number().int()
17004
17096
  });
17005
17097
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17006
17098
  var DiskReconcileCountsSchema = object({
@@ -17146,7 +17238,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17146
17238
  * stationary registry). Default false: the timeline lists passages,
17147
17239
  * not parking records (operator decision, 2026-08-15). */
17148
17240
  includeStationary: boolean().optional()
17149
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17241
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17242
+ deviceId: number(),
17243
+ groupId: string().min(1)
17244
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17150
17245
  kind: "mutation",
17151
17246
  auth: "admin"
17152
17247
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17456,7 +17551,8 @@ var PipelineModelOptionSchema = object({
17456
17551
  sizeMB: number()
17457
17552
  })),
17458
17553
  group: ModelVariantGroupSchema.optional(),
17459
- legacy: boolean().optional()
17554
+ legacy: boolean().optional(),
17555
+ provider: ModelProviderIdSchema.optional()
17460
17556
  });
17461
17557
  var ConfigFieldBridge = custom();
17462
17558
  var PipelineAddonSchemaSchema = object({
@@ -24040,7 +24136,12 @@ var PlateInfoSchema = object({
24040
24136
  plateBbox: BoundingBoxSchema.optional(),
24041
24137
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24042
24138
  keyFrameMediaKey: string().optional(),
24043
- base64: string().optional()
24139
+ base64: string().optional(),
24140
+ /**
24141
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24142
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24143
+ */
24144
+ cropUrl: string().optional()
24044
24145
  });
24045
24146
  var MediaFileLiteSchema = object({
24046
24147
  key: string(),
@@ -27680,6 +27781,12 @@ Object.freeze({
27680
27781
  addonId: null,
27681
27782
  access: "view"
27682
27783
  },
27784
+ "deviceManager.getBindingsBatch": {
27785
+ capName: "device-manager",
27786
+ capScope: "system",
27787
+ addonId: null,
27788
+ access: "view"
27789
+ },
27683
27790
  "deviceManager.getChildren": {
27684
27791
  capName: "device-manager",
27685
27792
  capScope: "system",
@@ -27740,6 +27847,12 @@ Object.freeze({
27740
27847
  addonId: null,
27741
27848
  access: "view"
27742
27849
  },
27850
+ "deviceManager.getLinkedDevicesBatch": {
27851
+ capName: "device-manager",
27852
+ capScope: "system",
27853
+ addonId: null,
27854
+ access: "view"
27855
+ },
27743
27856
  "deviceManager.getRoleDisplayDefaults": {
27744
27857
  capName: "device-manager",
27745
27858
  capScope: "system",
@@ -29510,6 +29623,12 @@ Object.freeze({
29510
29623
  addonId: null,
29511
29624
  access: "view"
29512
29625
  },
29626
+ "pipelineAnalytics.getGroup": {
29627
+ capName: "pipeline-analytics",
29628
+ capScope: "device",
29629
+ addonId: null,
29630
+ access: "view"
29631
+ },
29513
29632
  "pipelineAnalytics.getKeyEvents": {
29514
29633
  capName: "pipeline-analytics",
29515
29634
  capScope: "device",
@@ -29594,6 +29713,12 @@ Object.freeze({
29594
29713
  addonId: null,
29595
29714
  access: "view"
29596
29715
  },
29716
+ "pipelineAnalytics.listGroups": {
29717
+ capName: "pipeline-analytics",
29718
+ capScope: "device",
29719
+ addonId: null,
29720
+ access: "view"
29721
+ },
29597
29722
  "pipelineAnalytics.listOpsLog": {
29598
29723
  capName: "pipeline-analytics",
29599
29724
  capScope: "device",
@@ -32375,6 +32500,11 @@ Object.freeze({
32375
32500
  form: "single",
32376
32501
  optional: false
32377
32502
  }],
32503
+ "deviceManager.getBindingsBatch": [{
32504
+ name: "deviceIds",
32505
+ form: "array",
32506
+ optional: false
32507
+ }],
32378
32508
  "deviceManager.getChildren": [{
32379
32509
  name: "parentDeviceId",
32380
32510
  form: "single",
@@ -32420,6 +32550,11 @@ Object.freeze({
32420
32550
  form: "single",
32421
32551
  optional: false
32422
32552
  }],
32553
+ "deviceManager.getLinkedDevicesBatch": [{
32554
+ name: "deviceIds",
32555
+ form: "array",
32556
+ optional: false
32557
+ }],
32423
32558
  "deviceManager.getSettingsSchema": [{
32424
32559
  name: "deviceId",
32425
32560
  form: "single",
@@ -32440,6 +32575,11 @@ Object.freeze({
32440
32575
  form: "single",
32441
32576
  optional: false
32442
32577
  }],
32578
+ "deviceManager.listAll": [{
32579
+ name: "deviceIds",
32580
+ form: "array",
32581
+ optional: true
32582
+ }],
32443
32583
  "deviceManager.loadConfig": [{
32444
32584
  name: "deviceId",
32445
32585
  form: "single",
@@ -33013,6 +33153,11 @@ Object.freeze({
33013
33153
  form: "single",
33014
33154
  optional: false
33015
33155
  }],
33156
+ "pipelineAnalytics.getGroup": [{
33157
+ name: "deviceId",
33158
+ form: "single",
33159
+ optional: false
33160
+ }],
33016
33161
  "pipelineAnalytics.getKeyEvents": [{
33017
33162
  name: "deviceId",
33018
33163
  form: "single",
@@ -33068,6 +33213,11 @@ Object.freeze({
33068
33213
  form: "array",
33069
33214
  optional: false
33070
33215
  }],
33216
+ "pipelineAnalytics.listGroups": [{
33217
+ name: "deviceIds",
33218
+ form: "array",
33219
+ optional: false
33220
+ }],
33071
33221
  "pipelineAnalytics.listOpsLog": [{
33072
33222
  name: "deviceId",
33073
33223
  form: "single",
package/dist/index.mjs CHANGED
@@ -5801,6 +5801,13 @@ var BaseAddon = class {
5801
5801
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5802
5802
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5803
5803
  _registeredCapNames = [];
5804
+ /**
5805
+ * True only after `readAddonStore` actually answered. Constructor
5806
+ * defaults look like stored config when the store is down — a forked
5807
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5808
+ * mode, 2026-08-25) is not "the operator chose this".
5809
+ */
5810
+ settingsStoreReady = false;
5804
5811
  /** Default config values. Provided via constructor. */
5805
5812
  defaults;
5806
5813
  constructor(defaults) {
@@ -6201,7 +6208,9 @@ var BaseAddon = class {
6201
6208
  ];
6202
6209
  let lastErr;
6203
6210
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6204
- return await settings.readAddonStore() ?? {};
6211
+ const stored = await settings.readAddonStore() ?? {};
6212
+ this.settingsStoreReady = true;
6213
+ return stored;
6205
6214
  } catch (err) {
6206
6215
  lastErr = err;
6207
6216
  const msg = err instanceof Error ? err.message : String(err);
@@ -6209,6 +6218,7 @@ var BaseAddon = class {
6209
6218
  if (attempt === delaysMs.length) break;
6210
6219
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6211
6220
  }
6221
+ this.settingsStoreReady = false;
6212
6222
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6213
6223
  return {};
6214
6224
  }
@@ -8108,6 +8118,12 @@ var ModelVariantGroupSchema = object({
8108
8118
  */
8109
8119
  resolution: number().int().positive().optional()
8110
8120
  });
8121
+ var ModelProviderIdSchema = _enum([
8122
+ "camstack",
8123
+ "frigate",
8124
+ "scrypted",
8125
+ "custom"
8126
+ ]);
8111
8127
  var ModelCatalogEntrySchema = object({
8112
8128
  id: string(),
8113
8129
  name: string(),
@@ -8205,6 +8221,12 @@ var ModelCatalogEntrySchema = object({
8205
8221
  */
8206
8222
  group: ModelVariantGroupSchema.optional(),
8207
8223
  /**
8224
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8225
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8226
+ * persisted before this field existed (`inferModelProvider` fills those).
8227
+ */
8228
+ provider: ModelProviderIdSchema.optional(),
8229
+ /**
8208
8230
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8209
8231
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8210
8232
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11862,6 +11884,27 @@ var LinkedDeviceSchema = object({
11862
11884
  features: array(string()),
11863
11885
  producesTrackedEvents: boolean().optional()
11864
11886
  });
11887
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11888
+ * The batch answer needs the tag; the single-device answer already has it
11889
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11890
+ var LinkedDevicesForDeviceSchema = object({
11891
+ deviceId: number(),
11892
+ mode: LinkedDevicesModeSchema,
11893
+ devices: array(LinkedDeviceSchema)
11894
+ });
11895
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11896
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11897
+ * object literal is exactly how the three drift apart. */
11898
+ var DeviceBindingsForDeviceSchema = object({
11899
+ deviceId: number(),
11900
+ entries: array(object({
11901
+ capName: string(),
11902
+ kind: _enum(["native", "wrapped"]),
11903
+ providerAddonId: string(),
11904
+ providerNodeId: string(),
11905
+ nativeAddonId: string()
11906
+ }))
11907
+ });
11865
11908
  var SavedDeviceRowSchema = object({
11866
11909
  /** Numeric id reserved at allocateDeviceId time. */
11867
11910
  id: number(),
@@ -12087,11 +12130,25 @@ method(object({
12087
12130
  projection: _enum(["full", "slim"]).optional(),
12088
12131
  /** Return only camera devices. Filtering server-side instead of
12089
12132
  * shipping 293 rows to find 12. */
12090
- isCamera: boolean().optional()
12133
+ isCamera: boolean().optional(),
12134
+ /**
12135
+ * Return only these device ids. For the caller that already KNOWS the
12136
+ * handful it wants and needs a field the id-bearing answer does not
12137
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12138
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12139
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12140
+ * refetches on the reconcile interval, on a phone.
12141
+ *
12142
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12143
+ * keys rather than rejecting them (verified against the live hub
12144
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12145
+ * it answers today and the caller filters as it already does.
12146
+ */
12147
+ deviceIds: array(number()).optional()
12091
12148
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12092
12149
  mode: LinkedDevicesModeSchema,
12093
12150
  devices: array(LinkedDeviceSchema)
12094
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12151
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12095
12152
  deviceId: number(),
12096
12153
  values: record(string(), unknown())
12097
12154
  }), object({ success: literal(true) }), {
@@ -12118,25 +12175,7 @@ method(object({
12118
12175
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12119
12176
  kind: "mutation",
12120
12177
  auth: "admin"
12121
- }), method(object({ deviceId: number() }), object({
12122
- deviceId: number(),
12123
- entries: array(object({
12124
- capName: string(),
12125
- kind: _enum(["native", "wrapped"]),
12126
- providerAddonId: string(),
12127
- providerNodeId: string(),
12128
- nativeAddonId: string()
12129
- }))
12130
- })), method(object({}), array(object({
12131
- deviceId: number(),
12132
- entries: array(object({
12133
- capName: string(),
12134
- kind: _enum(["native", "wrapped"]),
12135
- providerAddonId: string(),
12136
- providerNodeId: string(),
12137
- nativeAddonId: string()
12138
- }))
12139
- }))), method(object({
12178
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12140
12179
  deviceId: number(),
12141
12180
  capName: string(),
12142
12181
  wrapperAddonId: string(),
@@ -14508,12 +14547,15 @@ var NcOccupancyConditionSchema = object({
14508
14547
  * there is no second switch that can disagree with the first and every rule
14509
14548
  * authored before the decision migrates for free (`audioModeOf`):
14510
14549
  *
14511
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14512
- * classifier labels with one of them. No window, no percentage:
14513
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14514
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14515
- * the analyzer's (`classificationMinScore`, per device) a label only
14516
- * reaches this condition if the classifier was already confident enough.
14550
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14551
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14552
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14553
+ * frames is the wrong question for a classifier that labels 1–3 frames
14554
+ * per episode. The count window is the brake that drops a single-frame
14555
+ * false positive; the rule's own `throttle` cooldown is the other. The
14556
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14557
+ * per device) — a label only reaches this condition if the classifier was
14558
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14517
14559
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14518
14560
  * the condition: at least `hitPercent`% of the samples over
14519
14561
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14540,14 +14582,22 @@ var NcOccupancyConditionSchema = object({
14540
14582
  * an operator who typed `dog` mean the same thing.
14541
14583
  */
14542
14584
  var NcAudioConditionSchema = object({
14543
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14585
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14544
14586
  labels: array(string().min(1)).min(1).optional(),
14545
14587
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14546
14588
  dbThreshold: number().min(-96).max(0).optional(),
14547
14589
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14548
14590
  hitPercent: number().int().min(1).max(100).default(60),
14549
14591
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14550
- samplingSeconds: number().int().min(1).max(300).default(10)
14592
+ samplingSeconds: number().int().min(1).max(300).default(10),
14593
+ /**
14594
+ * LABEL MODE: how many labelled frames must land inside
14595
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14596
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14597
+ */
14598
+ confirmHits: number().int().min(1).max(20).optional(),
14599
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14600
+ confirmWindowSec: number().int().min(1).max(60).optional()
14551
14601
  });
14552
14602
  /**
14553
14603
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16921,6 +16971,46 @@ var RecentTracksPageSchema = object({
16921
16971
  /** Cursor for the next page, or null when this page is the last. */
16922
16972
  nextCursor: string().nullable()
16923
16973
  });
16974
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16975
+ var LIST_GROUPS_MAX_LIMIT = 100;
16976
+ var AnalyticsGroupRecordSchema = object({
16977
+ id: string(),
16978
+ deviceId: number().int(),
16979
+ openedAt: number().int(),
16980
+ closedAt: number().int(),
16981
+ timestamp: number().int(),
16982
+ memberCount: number().int(),
16983
+ memberTrackIds: array(string()).readonly(),
16984
+ className: string(),
16985
+ classes: array(string()).readonly(),
16986
+ /** Relative event-media path, or null when the group has no picture yet. */
16987
+ mediaUrl: string().nullable(),
16988
+ singleton: boolean()
16989
+ });
16990
+ var AnalyticsGroupMemberSchema = object({
16991
+ trackId: string(),
16992
+ deviceId: number().int(),
16993
+ className: string(),
16994
+ firstSeen: number().int(),
16995
+ lastSeen: number().int(),
16996
+ mediaUrl: string().nullable()
16997
+ });
16998
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16999
+ var ListGroupsQueryInput = object({
17000
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17001
+ deviceIds: array(number()),
17002
+ /** Window lower bound on `closedAt` (inclusive). */
17003
+ since: number().optional(),
17004
+ /** Window upper bound on `openedAt` (inclusive). */
17005
+ until: number().optional(),
17006
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17007
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17008
+ cursor: string().optional()
17009
+ });
17010
+ var ListGroupsPageSchema = object({
17011
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17012
+ nextCursor: string().nullable()
17013
+ });
16924
17014
  var KeyEventQueryInput = object({
16925
17015
  deviceId: number(),
16926
17016
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16996,7 +17086,9 @@ var TrackCascadeCountsSchema = object({
16996
17086
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16997
17087
  plates: number().int(),
16998
17088
  /** Per-track CLIP search vectors removed (best-effort). */
16999
- embeddings: number().int()
17089
+ embeddings: number().int(),
17090
+ /** Group membership + group rows removed with their last member (best-effort). */
17091
+ groups: number().int()
17000
17092
  });
17001
17093
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17002
17094
  var DiskReconcileCountsSchema = object({
@@ -17142,7 +17234,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17142
17234
  * stationary registry). Default false: the timeline lists passages,
17143
17235
  * not parking records (operator decision, 2026-08-15). */
17144
17236
  includeStationary: boolean().optional()
17145
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17237
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17238
+ deviceId: number(),
17239
+ groupId: string().min(1)
17240
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17146
17241
  kind: "mutation",
17147
17242
  auth: "admin"
17148
17243
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17452,7 +17547,8 @@ var PipelineModelOptionSchema = object({
17452
17547
  sizeMB: number()
17453
17548
  })),
17454
17549
  group: ModelVariantGroupSchema.optional(),
17455
- legacy: boolean().optional()
17550
+ legacy: boolean().optional(),
17551
+ provider: ModelProviderIdSchema.optional()
17456
17552
  });
17457
17553
  var ConfigFieldBridge = custom();
17458
17554
  var PipelineAddonSchemaSchema = object({
@@ -24036,7 +24132,12 @@ var PlateInfoSchema = object({
24036
24132
  plateBbox: BoundingBoxSchema.optional(),
24037
24133
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24038
24134
  keyFrameMediaKey: string().optional(),
24039
- base64: string().optional()
24135
+ base64: string().optional(),
24136
+ /**
24137
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24138
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24139
+ */
24140
+ cropUrl: string().optional()
24040
24141
  });
24041
24142
  var MediaFileLiteSchema = object({
24042
24143
  key: string(),
@@ -27676,6 +27777,12 @@ Object.freeze({
27676
27777
  addonId: null,
27677
27778
  access: "view"
27678
27779
  },
27780
+ "deviceManager.getBindingsBatch": {
27781
+ capName: "device-manager",
27782
+ capScope: "system",
27783
+ addonId: null,
27784
+ access: "view"
27785
+ },
27679
27786
  "deviceManager.getChildren": {
27680
27787
  capName: "device-manager",
27681
27788
  capScope: "system",
@@ -27736,6 +27843,12 @@ Object.freeze({
27736
27843
  addonId: null,
27737
27844
  access: "view"
27738
27845
  },
27846
+ "deviceManager.getLinkedDevicesBatch": {
27847
+ capName: "device-manager",
27848
+ capScope: "system",
27849
+ addonId: null,
27850
+ access: "view"
27851
+ },
27739
27852
  "deviceManager.getRoleDisplayDefaults": {
27740
27853
  capName: "device-manager",
27741
27854
  capScope: "system",
@@ -29506,6 +29619,12 @@ Object.freeze({
29506
29619
  addonId: null,
29507
29620
  access: "view"
29508
29621
  },
29622
+ "pipelineAnalytics.getGroup": {
29623
+ capName: "pipeline-analytics",
29624
+ capScope: "device",
29625
+ addonId: null,
29626
+ access: "view"
29627
+ },
29509
29628
  "pipelineAnalytics.getKeyEvents": {
29510
29629
  capName: "pipeline-analytics",
29511
29630
  capScope: "device",
@@ -29590,6 +29709,12 @@ Object.freeze({
29590
29709
  addonId: null,
29591
29710
  access: "view"
29592
29711
  },
29712
+ "pipelineAnalytics.listGroups": {
29713
+ capName: "pipeline-analytics",
29714
+ capScope: "device",
29715
+ addonId: null,
29716
+ access: "view"
29717
+ },
29593
29718
  "pipelineAnalytics.listOpsLog": {
29594
29719
  capName: "pipeline-analytics",
29595
29720
  capScope: "device",
@@ -32371,6 +32496,11 @@ Object.freeze({
32371
32496
  form: "single",
32372
32497
  optional: false
32373
32498
  }],
32499
+ "deviceManager.getBindingsBatch": [{
32500
+ name: "deviceIds",
32501
+ form: "array",
32502
+ optional: false
32503
+ }],
32374
32504
  "deviceManager.getChildren": [{
32375
32505
  name: "parentDeviceId",
32376
32506
  form: "single",
@@ -32416,6 +32546,11 @@ Object.freeze({
32416
32546
  form: "single",
32417
32547
  optional: false
32418
32548
  }],
32549
+ "deviceManager.getLinkedDevicesBatch": [{
32550
+ name: "deviceIds",
32551
+ form: "array",
32552
+ optional: false
32553
+ }],
32419
32554
  "deviceManager.getSettingsSchema": [{
32420
32555
  name: "deviceId",
32421
32556
  form: "single",
@@ -32436,6 +32571,11 @@ Object.freeze({
32436
32571
  form: "single",
32437
32572
  optional: false
32438
32573
  }],
32574
+ "deviceManager.listAll": [{
32575
+ name: "deviceIds",
32576
+ form: "array",
32577
+ optional: true
32578
+ }],
32439
32579
  "deviceManager.loadConfig": [{
32440
32580
  name: "deviceId",
32441
32581
  form: "single",
@@ -33009,6 +33149,11 @@ Object.freeze({
33009
33149
  form: "single",
33010
33150
  optional: false
33011
33151
  }],
33152
+ "pipelineAnalytics.getGroup": [{
33153
+ name: "deviceId",
33154
+ form: "single",
33155
+ optional: false
33156
+ }],
33012
33157
  "pipelineAnalytics.getKeyEvents": [{
33013
33158
  name: "deviceId",
33014
33159
  form: "single",
@@ -33064,6 +33209,11 @@ Object.freeze({
33064
33209
  form: "array",
33065
33210
  optional: false
33066
33211
  }],
33212
+ "pipelineAnalytics.listGroups": [{
33213
+ name: "deviceIds",
33214
+ form: "array",
33215
+ optional: false
33216
+ }],
33067
33217
  "pipelineAnalytics.listOpsLog": [{
33068
33218
  name: "deviceId",
33069
33219
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-nodeav",
3
- "version": "1.2.26",
3
+ "version": "1.2.27",
4
4
  "description": "Standalone in-process node-av decoder addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",