@camstack/addon-pipeline 1.2.28 → 1.2.31

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 (30) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +518 -203
  4. package/dist/detection-pipeline/index.mjs +517 -202
  5. package/dist/{dist-DUHlZcWd.js → dist-BvFLZyhW.js} +105 -3
  6. package/dist/{dist-CsKaXmP8.mjs → dist-Du7_SNiR.mjs} +105 -3
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +149 -8
  10. package/dist/pipeline-runner/index.mjs +148 -8
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{step-definitions-C8A78dBi.mjs → step-definitions-BkY9_IM6.mjs} +13 -2
  14. package/dist/{step-definitions-Bo1hJQQw.js → step-definitions-gC-Ovglc.js} +13 -2
  15. package/dist/stream-broker/_stub.js +2 -2
  16. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-tHahN-lK.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BUXeX-vb.mjs} +3 -3
  17. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ByHi7ZAe.mjs +26 -0
  18. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CscpvIRR.mjs +26 -0
  19. package/dist/stream-broker/{hostInit-CIwilJUZ.mjs → hostInit-DJ3h-b1X.mjs} +3 -3
  20. package/dist/stream-broker/index.js +3 -3
  21. package/dist/stream-broker/index.mjs +3 -3
  22. package/dist/stream-broker/remoteEntry.js +1 -1
  23. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B7VdNJlO.js → MaskShapeCanvas-DI4BY7W2-B0DyP46h.js} +1 -1
  24. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-YyB4uLdH.js → MotionZonesSettings-NcxxQN8r-CzGSy_EO.js} +1 -1
  25. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DfqixEad.js → PrivacyMaskSettings-APgPLF7p-CEgMcOOW.js} +1 -1
  26. package/embed-dist/assets/{index-zVLLVdIS.js → index-CHXOWKHF.js} +14 -14
  27. package/embed-dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1zJmtUe.mjs +0 -26
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CSWUWZ8c.mjs +0 -26
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-BLcNejAE.mjs
1
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -269,6 +269,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
269
269
  */
270
270
  EventCategory["DeviceStateChanged"] = "device.state-changed";
271
271
  /**
272
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
273
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
274
+ *
275
+ * Emitted only on a change, so a steady scene is silent. It exists so a
276
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
277
+ * one live badge with no push signal at all, and it cost a request every
278
+ * four seconds per visible camera.
279
+ *
280
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
281
+ * keeps a slow reconcile rather than trusting it alone.
282
+ */
283
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
284
+ /**
272
285
  * Cap event fired by every device that registers the `battery`
273
286
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
274
287
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -9607,6 +9620,29 @@ var NcOccupancyConditionSchema = object({
9607
9620
  count: number().int().min(0).default(1),
9608
9621
  sustainSeconds: number().int().min(0).max(3600).default(15)
9609
9622
  });
9623
+ /**
9624
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
9625
+ *
9626
+ * The values are not symmetric, and deliberately so — the absent value has to
9627
+ * mean exactly what every rule authored before this condition existed already
9628
+ * does:
9629
+ * - `enter` — entries and every NON-crossing record (movement state,
9630
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
9631
+ * an operator who never asked for exits must not start receiving them.
9632
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
9633
+ * fails closed, because "the car left the drive" is a question about a
9634
+ * boundary, not about a detection.
9635
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
9636
+ *
9637
+ * A rule asking for a direction should normally also scope `zones`, which the
9638
+ * engine evaluates against the crossed zone as well as the current membership
9639
+ * (an exit's membership no longer contains the zone it just left).
9640
+ */
9641
+ var NcCrossingSchema = _enum([
9642
+ "enter",
9643
+ "exit",
9644
+ "any"
9645
+ ]);
9610
9646
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9611
9647
  var NcZoneConditionSchema = object({
9612
9648
  ids: array(string().min(1)).min(1),
@@ -9631,6 +9667,13 @@ var NcConditionsSchema = object({
9631
9667
  /** Veto zones — any hit fails the rule. */
9632
9668
  zonesExclude: array(string().min(1)).optional(),
9633
9669
  /**
9670
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
9671
+ * and a closed track carries none, so a `track-end` rule asking for one
9672
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
9673
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
9674
+ */
9675
+ crossing: NcCrossingSchema.optional(),
9676
+ /**
9634
9677
  * Exact (case-insensitive) match on the record's collapsed `label`
9635
9678
  * (identity name / plate text / subclass).
9636
9679
  */
@@ -9819,11 +9862,31 @@ var NcMediaPolicySchema = object({
9819
9862
  */
9820
9863
  profile: CamProfileSchema.optional()
9821
9864
  });
9865
+ /**
9866
+ * Cooldown GRANULARITY over the subject's class — how much a fired
9867
+ * notification suppresses.
9868
+ * - `shared` (default, and the absent value) — one window for the whole
9869
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
9870
+ * - `per-class` — an independent window per detected class, so cat→dog fires
9871
+ * at once and cat→cat still waits.
9872
+ *
9873
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
9874
+ * must not be swallowed by a bark's window (the precedent this generalizes —
9875
+ * see `cooldownKey` in the rule engine).
9876
+ */
9877
+ var NcThrottleGranularitySchema = _enum(["shared", "per-class"]);
9822
9878
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9823
9879
  var NcThrottleSchema = object({
9824
9880
  cooldownSec: number().int().min(0).max(86400).default(60),
9825
9881
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9826
- scope: _enum(["rule", "rule-device"]).default("rule-device")
9882
+ scope: _enum(["rule", "rule-device"]).default("rule-device"),
9883
+ /**
9884
+ * Class granularity of the cooldown key. Optional rather than defaulted:
9885
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
9886
+ * rule authored before this field simply carries none — and the engine
9887
+ * reads absent as `shared`, the pre-existing behaviour.
9888
+ */
9889
+ granularity: NcThrottleGranularitySchema.optional()
9827
9890
  });
9828
9891
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9829
9892
  var NcRuleInputSchema = object({
@@ -9931,6 +9994,7 @@ var NcConditionDescriptorSchema = object({
9931
9994
  "schedule",
9932
9995
  "plateMatcher",
9933
9996
  "packagePhase",
9997
+ "crossingSelect",
9934
9998
  "polygonDraw",
9935
9999
  "occupancy"
9936
10000
  ]),
@@ -17092,7 +17156,10 @@ method(object({
17092
17156
  }), method(object({
17093
17157
  deviceId: number(),
17094
17158
  caps: array(string()).readonly().optional()
17095
- }), record(string(), unknown().nullable()));
17159
+ }), record(string(), unknown().nullable())), method(object({
17160
+ deviceIds: array(number()).readonly(),
17161
+ caps: array(string()).readonly().optional()
17162
+ }), record(string(), record(string(), unknown().nullable())));
17096
17163
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
17097
17164
  deviceId: number(),
17098
17165
  capName: string()
@@ -18500,6 +18567,29 @@ var MotionEventSchema = object({
18500
18567
  * Absent on legacy rows ⇒ treat as `pipeline`.
18501
18568
  */
18502
18569
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
18570
+ /**
18571
+ * The confirmed zone crossing that produced an object event. Present ONLY on
18572
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
18573
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
18574
+ * appearance event carry none, so a rule asking for a direction fails closed
18575
+ * on them.
18576
+ *
18577
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
18578
+ * into its own event, so a frame in which a track enters A while leaving B
18579
+ * produces two events with two directions — never one ambiguous row.
18580
+ *
18581
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
18582
+ * membership the box has NOW, and by definition it no longer contains the zone
18583
+ * that was just left. Without the id here, a zone-scoped rule could never match
18584
+ * the exit it asked for.
18585
+ */
18586
+ var ZoneCrossingSchema = object({
18587
+ direction: _enum(["enter", "exit"]),
18588
+ /** Admin zone id crossed. */
18589
+ zoneId: string(),
18590
+ /** Zone display name at crossing time (falls back to the id). */
18591
+ zoneName: string().optional()
18592
+ });
18503
18593
  var ObjectEventSchema = object({
18504
18594
  ...BaseEventFields,
18505
18595
  kind: literal("object"),
@@ -18526,6 +18616,12 @@ var ObjectEventSchema = object({
18526
18616
  zones: array(string()).readonly().optional(),
18527
18617
  /** Omitted in slim projection. */
18528
18618
  state: TrackStateSchema.optional(),
18619
+ /**
18620
+ * The zone crossing this event IS, when it is one. Absent on every other
18621
+ * event kind (movement state, appearance, package) — see
18622
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
18623
+ */
18624
+ zoneCrossing: ZoneCrossingSchema.optional(),
18529
18625
  /** Detection-frame dimensions in pixels — let consumers normalize the
18530
18626
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
18531
18627
  frameWidth: number().optional(),
@@ -24041,6 +24137,12 @@ Object.freeze({
24041
24137
  addonId: null,
24042
24138
  access: "view"
24043
24139
  },
24140
+ "deviceManager.getDeviceStatusAggregateBatch": {
24141
+ capName: "device-manager",
24142
+ capScope: "system",
24143
+ addonId: null,
24144
+ access: "view"
24145
+ },
24044
24146
  "deviceManager.getLinkedDevices": {
24045
24147
  capName: "device-manager",
24046
24148
  capScope: "system",
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-BLcNejAE.mjs
1
+ //#region ../types/dist/event-category-Bz24uP1U.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -269,6 +269,19 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
269
269
  */
270
270
  EventCategory["DeviceStateChanged"] = "device.state-changed";
271
271
  /**
272
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
273
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
274
+ *
275
+ * Emitted only on a change, so a steady scene is silent. It exists so a
276
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
277
+ * one live badge with no push signal at all, and it cost a request every
278
+ * four seconds per visible camera.
279
+ *
280
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
281
+ * keeps a slow reconcile rather than trusting it alone.
282
+ */
283
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
284
+ /**
272
285
  * Cap event fired by every device that registers the `battery`
273
286
  * capability. Mirrors the cap definition's `onStatusChanged`. Carries
274
287
  * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
@@ -9607,6 +9620,29 @@ var NcOccupancyConditionSchema = object({
9607
9620
  count: number().int().min(0).default(1),
9608
9621
  sustainSeconds: number().int().min(0).max(3600).default(15)
9609
9622
  });
9623
+ /**
9624
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
9625
+ *
9626
+ * The values are not symmetric, and deliberately so — the absent value has to
9627
+ * mean exactly what every rule authored before this condition existed already
9628
+ * does:
9629
+ * - `enter` — entries and every NON-crossing record (movement state,
9630
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
9631
+ * an operator who never asked for exits must not start receiving them.
9632
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
9633
+ * fails closed, because "the car left the drive" is a question about a
9634
+ * boundary, not about a detection.
9635
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
9636
+ *
9637
+ * A rule asking for a direction should normally also scope `zones`, which the
9638
+ * engine evaluates against the crossed zone as well as the current membership
9639
+ * (an exit's membership no longer contains the zone it just left).
9640
+ */
9641
+ var NcCrossingSchema = _enum([
9642
+ "enter",
9643
+ "exit",
9644
+ "any"
9645
+ ]);
9610
9646
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9611
9647
  var NcZoneConditionSchema = object({
9612
9648
  ids: array(string().min(1)).min(1),
@@ -9631,6 +9667,13 @@ var NcConditionsSchema = object({
9631
9667
  /** Veto zones — any hit fails the rule. */
9632
9668
  zonesExclude: array(string().min(1)).optional(),
9633
9669
  /**
9670
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
9671
+ * and a closed track carries none, so a `track-end` rule asking for one
9672
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
9673
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
9674
+ */
9675
+ crossing: NcCrossingSchema.optional(),
9676
+ /**
9634
9677
  * Exact (case-insensitive) match on the record's collapsed `label`
9635
9678
  * (identity name / plate text / subclass).
9636
9679
  */
@@ -9819,11 +9862,31 @@ var NcMediaPolicySchema = object({
9819
9862
  */
9820
9863
  profile: CamProfileSchema.optional()
9821
9864
  });
9865
+ /**
9866
+ * Cooldown GRANULARITY over the subject's class — how much a fired
9867
+ * notification suppresses.
9868
+ * - `shared` (default, and the absent value) — one window for the whole
9869
+ * rule/scope: a cat silences the next dog for `cooldownSec`.
9870
+ * - `per-class` — an independent window per detected class, so cat→dog fires
9871
+ * at once and cat→cat still waits.
9872
+ *
9873
+ * AUDIO subjects are ALWAYS per-class regardless of this setting: a scream
9874
+ * must not be swallowed by a bark's window (the precedent this generalizes —
9875
+ * see `cooldownKey` in the rule engine).
9876
+ */
9877
+ var NcThrottleGranularitySchema = _enum(["shared", "per-class"]);
9822
9878
  /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9823
9879
  var NcThrottleSchema = object({
9824
9880
  cooldownSec: number().int().min(0).max(86400).default(60),
9825
9881
  /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9826
- scope: _enum(["rule", "rule-device"]).default("rule-device")
9882
+ scope: _enum(["rule", "rule-device"]).default("rule-device"),
9883
+ /**
9884
+ * Class granularity of the cooldown key. Optional rather than defaulted:
9885
+ * a Zod default does NOT run on the addon→addon cap path, so a persisted
9886
+ * rule authored before this field simply carries none — and the engine
9887
+ * reads absent as `shared`, the pre-existing behaviour.
9888
+ */
9889
+ granularity: NcThrottleGranularitySchema.optional()
9827
9890
  });
9828
9891
  /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9829
9892
  var NcRuleInputSchema = object({
@@ -9931,6 +9994,7 @@ var NcConditionDescriptorSchema = object({
9931
9994
  "schedule",
9932
9995
  "plateMatcher",
9933
9996
  "packagePhase",
9997
+ "crossingSelect",
9934
9998
  "polygonDraw",
9935
9999
  "occupancy"
9936
10000
  ]),
@@ -17092,7 +17156,10 @@ method(object({
17092
17156
  }), method(object({
17093
17157
  deviceId: number(),
17094
17158
  caps: array(string()).readonly().optional()
17095
- }), record(string(), unknown().nullable()));
17159
+ }), record(string(), unknown().nullable())), method(object({
17160
+ deviceIds: array(number()).readonly(),
17161
+ caps: array(string()).readonly().optional()
17162
+ }), record(string(), record(string(), unknown().nullable())));
17096
17163
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
17097
17164
  deviceId: number(),
17098
17165
  capName: string()
@@ -18500,6 +18567,29 @@ var MotionEventSchema = object({
18500
18567
  * Absent on legacy rows ⇒ treat as `pipeline`.
18501
18568
  */
18502
18569
  var DetectionSourceSchema = _enum(["pipeline", "onboard"]);
18570
+ /**
18571
+ * The confirmed zone crossing that produced an object event. Present ONLY on
18572
+ * an event emitted BY a crossing (`zone.enter` / `zone.exit`); a movement-state
18573
+ * event (`object.entering` / `leaving` / `stationary` / `loitering`) and an
18574
+ * appearance event carry none, so a rule asking for a direction fails closed
18575
+ * on them.
18576
+ *
18577
+ * Exactly ONE crossing per event: the emitter turns each confirmed crossing
18578
+ * into its own event, so a frame in which a track enters A while leaving B
18579
+ * produces two events with two directions — never one ambiguous row.
18580
+ *
18581
+ * `zoneId` is load-bearing for an EXIT: the event's `zones` list is the
18582
+ * membership the box has NOW, and by definition it no longer contains the zone
18583
+ * that was just left. Without the id here, a zone-scoped rule could never match
18584
+ * the exit it asked for.
18585
+ */
18586
+ var ZoneCrossingSchema = object({
18587
+ direction: _enum(["enter", "exit"]),
18588
+ /** Admin zone id crossed. */
18589
+ zoneId: string(),
18590
+ /** Zone display name at crossing time (falls back to the id). */
18591
+ zoneName: string().optional()
18592
+ });
18503
18593
  var ObjectEventSchema = object({
18504
18594
  ...BaseEventFields,
18505
18595
  kind: literal("object"),
@@ -18526,6 +18616,12 @@ var ObjectEventSchema = object({
18526
18616
  zones: array(string()).readonly().optional(),
18527
18617
  /** Omitted in slim projection. */
18528
18618
  state: TrackStateSchema.optional(),
18619
+ /**
18620
+ * The zone crossing this event IS, when it is one. Absent on every other
18621
+ * event kind (movement state, appearance, package) — see
18622
+ * {@link ZoneCrossingSchema}. Omitted in slim projection.
18623
+ */
18624
+ zoneCrossing: ZoneCrossingSchema.optional(),
18529
18625
  /** Detection-frame dimensions in pixels — let consumers normalize the
18530
18626
  * pixel-space `bbox` onto a displayed image. Omitted in slim projection. */
18531
18627
  frameWidth: number().optional(),
@@ -24041,6 +24137,12 @@ Object.freeze({
24041
24137
  addonId: null,
24042
24138
  access: "view"
24043
24139
  },
24140
+ "deviceManager.getDeviceStatusAggregateBatch": {
24141
+ capName: "device-manager",
24142
+ capScope: "system",
24143
+ addonId: null,
24144
+ access: "view"
24145
+ },
24044
24146
  "deviceManager.getLinkedDevices": {
24045
24147
  capName: "device-manager",
24046
24148
  capScope: "system",
@@ -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-DUHlZcWd.js");
5
+ const require_dist = require("../dist-BvFLZyhW.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { A as motionDetectionCapability, E as evaluateZoneRules, K as hydrateSchema, V as BaseAddon, W as DeviceType } from "../dist-CsKaXmP8.mjs";
1
+ import { A as motionDetectionCapability, E as evaluateZoneRules, K as hydrateSchema, V as BaseAddon, W as DeviceType } from "../dist-Du7_SNiR.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -3,11 +3,13 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-DUHlZcWd.js");
6
+ const require_dist = require("../dist-BvFLZyhW.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
10
- const require_step_definitions = require("../step-definitions-Bo1hJQQw.js");
10
+ const require_step_definitions = require("../step-definitions-gC-Ovglc.js");
11
+ let node_os = require("node:os");
12
+ node_os = require_chunk.__toESM(node_os);
11
13
  let node_child_process = require("node:child_process");
12
14
  let node_url = require("node:url");
13
15
  let sharp = require("sharp");
@@ -383,6 +385,51 @@ var Semaphore = class {
383
385
  if (next) next();
384
386
  }
385
387
  };
388
+ var MotionWithoutInferenceDetector = class {
389
+ streak = /* @__PURE__ */ new Map();
390
+ reported = /* @__PURE__ */ new Set();
391
+ minIntervals;
392
+ constructor(minIntervals = 3) {
393
+ this.minIntervals = Math.max(1, minIntervals);
394
+ }
395
+ /**
396
+ * Feed one interval for one camera. Returns what, if anything, to say.
397
+ *
398
+ * A camera with NO motion is not evidence either way — it is simply quiet,
399
+ * and its streak is left untouched rather than reset, so an outage that
400
+ * spans a lull is still reported as one continuous stall.
401
+ */
402
+ observe(deviceId, summary) {
403
+ if (summary.inferenceFrames > 0) {
404
+ const wasReported = this.reported.delete(deviceId);
405
+ const intervals = this.streak.get(deviceId) ?? 0;
406
+ this.streak.delete(deviceId);
407
+ return wasReported ? {
408
+ kind: "recovered",
409
+ intervals
410
+ } : { kind: "quiet" };
411
+ }
412
+ if (summary.motionFrames <= 0) return { kind: "quiet" };
413
+ const next = (this.streak.get(deviceId) ?? 0) + 1;
414
+ this.streak.set(deviceId, next);
415
+ if (next < this.minIntervals) return { kind: "quiet" };
416
+ if (this.reported.has(deviceId)) return { kind: "quiet" };
417
+ this.reported.add(deviceId);
418
+ return {
419
+ kind: "stalled",
420
+ intervals: next
421
+ };
422
+ }
423
+ /** Drop a camera's state — it was detached, so its streak means nothing. */
424
+ forget(deviceId) {
425
+ this.streak.delete(deviceId);
426
+ this.reported.delete(deviceId);
427
+ }
428
+ /** Cameras currently in a reported stall — the board's "motion, no inference". */
429
+ stalledDevices() {
430
+ return [...this.reported];
431
+ }
432
+ };
386
433
  //#endregion
387
434
  //#region src/pipeline-runner/timing-sampler.ts
388
435
  var REPORT_INTERVAL_MS = 1e4;
@@ -395,6 +442,8 @@ var REPORT_INTERVAL_MS = 1e4;
395
442
  var PipelineTimingSampler = class {
396
443
  detSamples = /* @__PURE__ */ new Map();
397
444
  motSamples = /* @__PURE__ */ new Map();
445
+ /** See {@link MotionWithoutInferenceDetector} — the two-incident signature. */
446
+ motionStall = new MotionWithoutInferenceDetector();
398
447
  audioSamples = /* @__PURE__ */ new Map();
399
448
  /** Per-device cumulative dropped-frames readers (see {@link DroppedFramesSource}). */
400
449
  droppedSources = /* @__PURE__ */ new Map();
@@ -526,6 +575,28 @@ var PipelineTimingSampler = class {
526
575
  }
527
576
  });
528
577
  }
578
+ const devicesSeen = new Set([...this.detSamples.keys(), ...this.motSamples.keys()]);
579
+ for (const deviceId of devicesSeen) {
580
+ const verdict = this.motionStall.observe(deviceId, {
581
+ motionFrames: this.motSamples.get(deviceId)?.length ?? 0,
582
+ inferenceFrames: this.detSamples.get(deviceId)?.length ?? 0
583
+ });
584
+ if (verdict.kind === "stalled") this.log.warn("motion is flowing but NOTHING is being inferred", {
585
+ tags: { deviceId },
586
+ meta: {
587
+ intervals: verdict.intervals,
588
+ forSec: verdict.intervals * REPORT_INTERVAL_MS / 1e3,
589
+ motionFrames: this.motSamples.get(deviceId)?.length ?? 0
590
+ }
591
+ });
592
+ else if (verdict.kind === "recovered") this.log.info("inference resumed after a motion-without-inference stall", {
593
+ tags: { deviceId },
594
+ meta: {
595
+ intervals: verdict.intervals,
596
+ lastedSec: verdict.intervals * REPORT_INTERVAL_MS / 1e3
597
+ }
598
+ });
599
+ }
529
600
  this.detSamples.clear();
530
601
  for (const [deviceId, mot] of this.motSamples) {
531
602
  if (mot.length === 0) continue;
@@ -658,6 +729,9 @@ function toFrameInput$1(frame) {
658
729
  * without spinning up Moleculer or the addon framework.
659
730
  */
660
731
  var PipelineRunner = class {
732
+ /** Abandoned inference calls that have not yet settled. A BURST reads as
733
+ * one rising number instead of N unrelated lines. */
734
+ inflightTimeouts = 0;
661
735
  config;
662
736
  cameras = /* @__PURE__ */ new Map();
663
737
  semaphore;
@@ -1144,16 +1218,33 @@ var PipelineRunner = class {
1144
1218
  timer.unref?.();
1145
1219
  });
1146
1220
  try {
1147
- const outcome = await Promise.race([this.config.processFrame(deviceId, frameInput, executorHandle), expiry]);
1221
+ const inflight = this.config.processFrame(deviceId, frameInput, executorHandle);
1222
+ const outcome = await Promise.race([inflight, expiry]);
1148
1223
  if (outcome === TIMED_OUT) {
1149
1224
  release();
1225
+ this.inflightTimeouts += 1;
1150
1226
  this.logger?.warn("inference timed out — permit released, result discarded", {
1151
1227
  tags: { deviceId },
1152
1228
  meta: {
1153
1229
  timeoutMs,
1154
- availablePermits: this.semaphore.available
1230
+ availablePermits: this.semaphore.available,
1231
+ outstandingTimeouts: this.inflightTimeouts
1155
1232
  }
1156
1233
  });
1234
+ const deadlineAt = Date.now();
1235
+ const noteSettled = (fate) => {
1236
+ this.inflightTimeouts = Math.max(0, this.inflightTimeouts - 1);
1237
+ this.logger?.info("inference: abandoned call settled after the deadline", {
1238
+ tags: { deviceId },
1239
+ meta: {
1240
+ lateByMs: Date.now() - deadlineAt,
1241
+ totalMs: timeoutMs + (Date.now() - deadlineAt),
1242
+ fate,
1243
+ outstandingTimeouts: this.inflightTimeouts
1244
+ }
1245
+ });
1246
+ };
1247
+ inflight.then(() => noteSettled("resolved"), () => noteSettled("rejected"));
1157
1248
  }
1158
1249
  return outcome;
1159
1250
  } finally {
@@ -1251,6 +1342,40 @@ function summarizeDeviceLoad(attachedDeviceKeys) {
1251
1342
  })).toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey));
1252
1343
  }
1253
1344
  //#endregion
1345
+ //#region src/pipeline-runner/hardware-capabilities.ts
1346
+ /**
1347
+ * The `hardware` block of `RunnerLocalLoad` — what this node can accelerate on.
1348
+ *
1349
+ * ## Why this file exists
1350
+ *
1351
+ * `getLocalLoad()` reported a hardcoded `hasGpu: false`. It was not a probe that
1352
+ * failed: when the hardware probe was moved off the runner (Phase 4) the field
1353
+ * was left as a literal, and it has answered "no GPU" ever since — including on
1354
+ * nodes actively running OpenVINO on their Intel iGPU. The admin UI renders a
1355
+ * "GPU" badge straight off it, and the orchestrator's hardware-aware balancing
1356
+ * reads it, so a stale `false` is a wrong answer, not a missing one.
1357
+ *
1358
+ * The probe is cheap and local (`ctx.kernel.inferenceEngine.resolveHardware()`
1359
+ * is deterministic per host, the same resolver the detection addon uses to pick
1360
+ * the node's default engine), and the runner shares a process with the
1361
+ * detection pipeline, so there is no cross-node ambiguity: this is THIS node.
1362
+ */
1363
+ /**
1364
+ * Summarise a local hardware probe into the runner's reported capabilities.
1365
+ *
1366
+ * `hasGpu` is literal — a GPU, not "an accelerator". An NPU or a Coral is not a
1367
+ * GPU and does not set it; they surface per pool through `RunnerLocalLoad.devices`
1368
+ * (`openvino:npu`, `edgetpu:usb`, …), which is the finer-grained answer the
1369
+ * device balancer weights on. A null probe means "could not tell", which reports
1370
+ * as `false` — the same conservative floor the engine cascade takes.
1371
+ */
1372
+ function summarizeRunnerHardware(hardware, cpuCores = node_os.cpus().length) {
1373
+ return {
1374
+ hasGpu: hardware?.gpu != null,
1375
+ cpuCores
1376
+ };
1377
+ }
1378
+ //#endregion
1254
1379
  //#region src/pipeline-runner/native-crop-tier.ts
1255
1380
  /**
1256
1381
  * Pure tier decision for `getNativeCrop`. A native HIT always resolves to the
@@ -2990,6 +3115,8 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
2990
3115
  * {@link MOTION_FRESHNESS_WINDOW_MS}.
2991
3116
  */
2992
3117
  lastMotionFrameAt = /* @__PURE__ */ new Map();
3118
+ /** Cached local accelerator probe — see {@link resolveLocalHardware}. */
3119
+ localHardware = null;
2993
3120
  constructor() {
2994
3121
  super({ ...DEFAULT_CONFIG });
2995
3122
  }
@@ -3843,12 +3970,26 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3843
3970
  avgInferenceTimeMs: metrics.avgInferenceTimeMs,
3844
3971
  queueDepthTotal: metrics.queueDepth,
3845
3972
  devices,
3846
- hardware: {
3847
- hasGpu: false,
3848
- inferenceBackend: void 0
3849
- }
3973
+ hardware: summarizeRunnerHardware(await this.resolveLocalHardware())
3850
3974
  };
3851
3975
  }
3976
+ /**
3977
+ * THIS node's accelerators, probed once from the local kernel resolver and
3978
+ * cached (the probe is deterministic per host, and `getLocalLoad` is polled).
3979
+ * A throwing/absent resolver caches nothing and reports "could not tell" so a
3980
+ * later poll retries.
3981
+ */
3982
+ async resolveLocalHardware() {
3983
+ if (this.localHardware) return this.localHardware;
3984
+ try {
3985
+ const hw = await this.ctx.kernel?.inferenceEngine?.resolveHardware();
3986
+ this.localHardware = hw ?? null;
3987
+ return this.localHardware;
3988
+ } catch (err) {
3989
+ this.ctx.logger.warn("local hardware probe failed — reporting no accelerator", { meta: { error: err instanceof Error ? err.message : String(err) } });
3990
+ return null;
3991
+ }
3992
+ }
3852
3993
  async getLocalMetrics() {
3853
3994
  const m = this.runner?.getMetrics() ?? {
3854
3995
  activeCameras: 0,