@camstack/addon-pipeline 1.1.28 → 1.1.29

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 (39) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/audio-codec-ffmpeg/index.js +1 -5
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -5
  5. package/dist/decoder-ffmpeg/index.js +102 -352
  6. package/dist/decoder-ffmpeg/index.mjs +82 -332
  7. package/dist/decoder-nodeav/index.js +6 -4
  8. package/dist/decoder-nodeav/index.mjs +6 -4
  9. package/dist/detection-pipeline/index.js +204 -180
  10. package/dist/detection-pipeline/index.mjs +204 -180
  11. package/dist/{dist-Biq62zt4.js → dist-Cwc0TUQr.js} +7 -42
  12. package/dist/{dist-C6_wgXqF.mjs → dist-DjuGmyG9.mjs} +7 -42
  13. package/dist/ffmpeg-args-C5GPp8Cw.mjs +323 -0
  14. package/dist/ffmpeg-args-D6h1edXK.js +418 -0
  15. package/dist/frame-dropper-AjheBGMG.mjs +22 -0
  16. package/dist/frame-dropper-DKLM6pMz.js +27 -0
  17. package/dist/{frame-handle-plane-BIoY6nRV.mjs → frame-handle-plane-Bkxz-TTD.mjs} +1 -1
  18. package/dist/{frame-handle-plane-D6BzyEgy.js → frame-handle-plane-DQNCTrpC.js} +1 -1
  19. package/dist/{frame-ring-sink-9J0wCdLF.js → frame-ring-sink-8LLV-cvH.js} +1 -1
  20. package/dist/{frame-ring-sink-Cs9vby6v.mjs → frame-ring-sink-ClEWjiRU.mjs} +1 -1
  21. package/dist/motion-wasm/index.js +1 -1
  22. package/dist/motion-wasm/index.mjs +1 -1
  23. package/dist/pipeline-runner/index.js +2 -2
  24. package/dist/pipeline-runner/index.mjs +2 -2
  25. package/dist/recorder/index.js +1 -1
  26. package/dist/recorder/index.mjs +1 -1
  27. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DoReAb4y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs} +3 -3
  28. package/dist/stream-broker/{hostInit-C0SuwQhL.mjs → hostInit-B_b3PIZB.mjs} +3 -3
  29. package/dist/stream-broker/index.js +168 -121
  30. package/dist/stream-broker/index.mjs +159 -112
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2yi8ah.js → MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BSCcPdPf.js → MotionZonesSettings-NcxxQN8r-DTeuDGCU.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-ztxvxSjS.js → PrivacyMaskSettings-APgPLF7p-CirtxO3e.js} +1 -1
  35. package/embed-dist/assets/{index-CeZV1B-2.js → index-CpUy8OIE.js} +9 -9
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/dist/frame-dropper-7RTo_YyG.js +0 -68
  39. package/dist/frame-dropper-CwkBTPGV.mjs +0 -51
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as sleep, B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, K as createEvent, M as pipelineExecutorCapability, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, X as parseJsonUnknown, a as COCO_TO_MACRO, i as COCO_80_LABELS, j as nodePin, m as YAMNET_TO_MACRO, q as hydrateSchema, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as detectionPipelineCapability } from "../dist-C6_wgXqF.mjs";
2
+ import { $ as sleep, B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, K as createEvent, M as pipelineExecutorCapability, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, X as parseJsonUnknown, a as COCO_TO_MACRO, i as COCO_80_LABELS, m as YAMNET_TO_MACRO, q as hydrateSchema, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as detectionPipelineCapability } from "../dist-DjuGmyG9.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
@@ -8,87 +8,6 @@ import * as path$1 from "node:path";
8
8
  import * as os from "node:os";
9
9
  import { spawn } from "node:child_process";
10
10
  import sharp from "sharp";
11
- //#region src/detection-pipeline/engine-store-keys.ts
12
- /**
13
- * Per-node scoping for the detection-pipeline engine cascade.
14
- *
15
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
16
- * settings-store cap is hub-resident; every node's detection instance reads and
17
- * writes the same keys). That is correct for node-agnostic settings (pipeline
18
- * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
19
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
20
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
21
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
22
- * another node that has no NPU.
23
- *
24
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
25
- * Everything else in the store stays shared. There is NO fallback to a legacy
26
- * un-scoped value for ANY node (the hub included) and no in-code migration: a
27
- * bare key (written before scoping, or by an older build) is inert — invisible
28
- * to every node. A node with no scoped key reads `undefined`, which drives its
29
- * OWN platform auto-pick (`autoPickAndPersist` / `reprobeEngine` persist the
30
- * node's own probe under `<key>@<node>`). Serving a bare value to any node
31
- * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
32
- * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
33
- * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
34
- */
35
- var ENGINE_CASCADE_KEYS = [
36
- "engineBackend",
37
- "engineDevice",
38
- "probedBestEngine"
39
- ];
40
- function isEngineCascadeKey(key) {
41
- return ENGINE_CASCADE_KEYS.includes(key);
42
- }
43
- /**
44
- * Normalise a raw kernel node id to the bare node id used for scoping.
45
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
46
- * per-NODE, so strip the addon segment. Falls back to `hub`.
47
- */
48
- function normalizeEngineNodeId(rawNodeId) {
49
- const raw = rawNodeId ?? "hub";
50
- return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
51
- }
52
- /** The node-scoped store key for an engine cascade field. */
53
- function nodeEngineKey(base, nodeId) {
54
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
55
- }
56
- /**
57
- * Read an engine cascade value for a node: the node-scoped key if present,
58
- * otherwise `undefined` — for EVERY node, the hub included. There is no
59
- * fallback to the legacy un-scoped key: a bare value in the store is inert.
60
- * A node with no scoped key gets `undefined` so its own platform-driven
61
- * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
62
- * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
63
- */
64
- function readNodeEngineValue(store, base, nodeId) {
65
- return store[nodeEngineKey(base, nodeId)];
66
- }
67
- /**
68
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
69
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
70
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
71
- * keys for OTHER nodes are dropped from the projection (not relevant to this
72
- * node's form).
73
- */
74
- function projectNodeEngine(store, nodeId) {
75
- const out = {};
76
- const scopedForAnyNode = /* @__PURE__ */ new Set();
77
- for (const key of Object.keys(store)) {
78
- const atIdx = key.indexOf("@");
79
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
80
- scopedForAnyNode.add(key);
81
- continue;
82
- }
83
- out[key] = store[key];
84
- }
85
- for (const base of ENGINE_CASCADE_KEYS) {
86
- const value = readNodeEngineValue(store, base, nodeId);
87
- if (value !== void 0) out[base] = value;
88
- }
89
- return out;
90
- }
91
- //#endregion
92
11
  //#region src/detection-pipeline/runtimes.ts
93
12
  var KNOWN_PLATFORMS = [
94
13
  "darwin",
@@ -260,6 +179,88 @@ function pickBestRuntime(env, bestBackendHint) {
260
179
  };
261
180
  }
262
181
  //#endregion
182
+ //#region src/detection-pipeline/engine-store-keys.ts
183
+ /**
184
+ * Per-node scoping for the detection-pipeline engine cascade.
185
+ *
186
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
187
+ * settings-store cap is hub-resident; every node's detection instance reads and
188
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
189
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
190
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
191
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
192
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
193
+ * another node that has no NPU.
194
+ *
195
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
196
+ * Everything else in the store stays shared. There is NO fallback to a legacy
197
+ * un-scoped value for ANY node (the hub included) and no in-code migration: a
198
+ * bare key (written before scoping, or by an older build) is inert — invisible
199
+ * to every node. A node with no scoped key reads `undefined`, which drives its
200
+ * OWN live resolve (`resolveAutoEngine` picks the node's runtime from the local
201
+ * kernel each boot; `reprobeEngine` persists an explicit probe under
202
+ * `<key>@<node>`). Serving a bare value to any node
203
+ * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
204
+ * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
205
+ * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
206
+ */
207
+ var ENGINE_CASCADE_KEYS = [
208
+ "engineBackend",
209
+ "engineDevice",
210
+ "probedBestEngine"
211
+ ];
212
+ function isEngineCascadeKey(key) {
213
+ return ENGINE_CASCADE_KEYS.includes(key);
214
+ }
215
+ /**
216
+ * Normalise a raw kernel node id to the bare node id used for scoping.
217
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
218
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
219
+ */
220
+ function normalizeEngineNodeId(rawNodeId) {
221
+ const raw = rawNodeId ?? "hub";
222
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
223
+ }
224
+ /** The node-scoped store key for an engine cascade field. */
225
+ function nodeEngineKey(base, nodeId) {
226
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
227
+ }
228
+ /**
229
+ * Read an engine cascade value for a node: the node-scoped key if present,
230
+ * otherwise `undefined` — for EVERY node, the hub included. There is no
231
+ * fallback to the legacy un-scoped key: a bare value in the store is inert.
232
+ * A node with no scoped key gets `undefined` so its own platform-driven
233
+ * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
234
+ * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
235
+ */
236
+ function readNodeEngineValue(store, base, nodeId) {
237
+ return store[nodeEngineKey(base, nodeId)];
238
+ }
239
+ /**
240
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
241
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
242
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
243
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
244
+ * node's form).
245
+ */
246
+ function projectNodeEngine(store, nodeId) {
247
+ const out = {};
248
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
249
+ for (const key of Object.keys(store)) {
250
+ const atIdx = key.indexOf("@");
251
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
252
+ scopedForAnyNode.add(key);
253
+ continue;
254
+ }
255
+ out[key] = store[key];
256
+ }
257
+ for (const base of ENGINE_CASCADE_KEYS) {
258
+ const value = readNodeEngineValue(store, base, nodeId);
259
+ if (value !== void 0) out[base] = value;
260
+ }
261
+ return out;
262
+ }
263
+ //#endregion
263
264
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
264
265
  /**
265
266
  * SharedInferencePool — TypeScript wrapper for inference_pool.py.
@@ -4572,56 +4573,53 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4572
4573
  error: errMsg(err)
4573
4574
  } });
4574
4575
  }
4575
- await this.autoPickAndPersist();
4576
+ await this.resolveAutoEngine();
4576
4577
  this.needsAutoPick = false;
4577
4578
  this.startProvisioningForCurrentEngine();
4578
4579
  }
4579
4580
  /**
4580
- * Auto-pick the best supported runtime at first boot (no stored engine).
4581
- * Uses the platform-probe cap's hardware + bestScore hint when available;
4582
- * falls back to platform/arch when the probe cap is not yet reachable.
4581
+ * Resolve the `'auto'` engine LIVE from this node's own kernel inference
4582
+ * resolver (`ctx.kernel.inferenceEngine`) the runtime mirror of
4583
+ * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
4584
+ * resolves its OWN accelerators in-process, so a forked agent child never
4585
+ * adopts the hub's runtime from the `platform-probe` singleton.
4583
4586
  *
4584
- * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4585
- * boots load it via `loadEngine()` and skip this path but ONLY when the
4586
- * probe actually answered (real `hardware` or a `bestScore` hint). If the
4587
- * probe query failed (cold-start race, cap momentarily unreachable), the
4588
- * pick floors to onnx purely for lack of information; persisting that would
4589
- * LOCK onnx and skip auto-pick on every future boot even after the
4590
- * accelerator surfaces. In that case we set the in-memory floor for liveness
4591
- * but leave the store untouched so the next boot re-attempts the pick.
4587
+ * NEVER persists. `'auto'` is a live-resolved sentinel: the pick is recomputed
4588
+ * every boot from the local hardware, so it is inherently self-healing the
4589
+ * old `autoPickAndPersist` sticky-onnx bug (a premature onnx pick persisted
4590
+ * during a boot race, then locked forever) cannot recur because the store is
4591
+ * never written here. Only an explicit operator override is persisted, and
4592
+ * that write happens through the settings UI, not this path.
4592
4593
  */
4593
- async autoPickAndPersist() {
4594
+ async resolveAutoEngine() {
4594
4595
  let hardware = null;
4595
4596
  try {
4596
- const api = this.addonCtx?.api;
4597
- if (api) hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
4597
+ hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
4598
4598
  } catch {}
4599
- const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
4600
- const engine = {
4599
+ const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
4600
+ this.currentEngine = {
4601
4601
  runtime: "python",
4602
4602
  backend: pick.runtimeId,
4603
4603
  format: modelFormatFor(pick.runtimeId),
4604
4604
  device: pick.device
4605
4605
  };
4606
- this.currentEngine = engine;
4607
- if (!(pick.runtimeId !== "onnx" || hardware !== null)) {
4608
- this.log.info("Auto-pick: onnx floor pending gpu probe — NOT persisting (next boot retries)", { meta: {
4609
- backend: pick.runtimeId,
4610
- device: pick.device
4611
- } });
4612
- return;
4613
- }
4614
- const apNode = this.localProbeNodeId();
4615
- await this.writeStore({
4616
- [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4617
- [nodeEngineKey("engineDevice", apNode)]: pick.device
4618
- });
4619
- this.log.info("Auto-picked engine (platform-deterministic)", { meta: {
4606
+ this.log.info("Resolved auto engine (local kernel)", { meta: {
4620
4607
  backend: pick.runtimeId,
4621
4608
  device: pick.device,
4622
- hadProbeHardware: hardware !== null
4609
+ hadHardware: hardware !== null
4623
4610
  } });
4624
4611
  }
4612
+ /**
4613
+ * The RUNTIME that `'auto'` actually resolved to on this node (the live
4614
+ * `currentEngine.backend`), narrowed to a {@link RuntimeId}. The addon feeds
4615
+ * this into `resolveBackendTuning` when the stored config is `'auto'`, so the
4616
+ * pool tuning + respawn detection track the RESOLVED backend rather than the
4617
+ * `'auto'` sentinel (which would otherwise floor to onnx tuning).
4618
+ */
4619
+ resolvedEngineBackend() {
4620
+ const backend = this.currentEngine.backend;
4621
+ return backend === "openvino" || backend === "coreml" ? backend : "onnx";
4622
+ }
4625
4623
  /** Map a backend string to a known RuntimeId, flooring to onnx. */
4626
4624
  toRuntimeId(backend) {
4627
4625
  return [
@@ -4695,6 +4693,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4695
4693
  const prev = this.currentEngine;
4696
4694
  const stored = await this.loadEngine();
4697
4695
  if (stored) this.currentEngine = stored;
4696
+ else await this.resolveAutoEngine();
4698
4697
  if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
4699
4698
  this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4700
4699
  from: `${prev.backend}/${prev.device ?? "default"}`,
@@ -4796,17 +4795,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4796
4795
  }, { ...state }));
4797
4796
  }
4798
4797
  /**
4799
- * Fetch the probed HARDWARE for engine + device gating. Returns null when the
4800
- * probe cap is not yet reachable (cold-start / probe addon not installed), so
4801
- * callers fall back to the registry's safe minimum. The engine OFFER derives
4802
- * from hardware ONLY — install state (probe `scores`) never gates it.
4798
+ * Fetch this node's inference HARDWARE (gpu/npu) for engine + device gating,
4799
+ * from the local kernel resolver. Returns null when the resolver is not wired
4800
+ * yet (cold-start), so callers fall back to the registry's safe minimum. The
4801
+ * engine OFFER derives from hardware ONLY — install state never gates it.
4803
4802
  */
4804
4803
  /**
4805
- * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4806
- * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4807
- * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4808
- * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4809
- * have) and pin a device the node can't run, breaking provisioning.
4804
+ * The local Moleculer node id (child-suffix stripped). Used to scope this
4805
+ * node's engine-cascade store keys (`<key>@<node>`). Hardware itself is now
4806
+ * resolved in-process via `ctx.kernel.inferenceEngine` (never the
4807
+ * `platform-probe` singleton, whose no-nodeId query resolved to the HUB and
4808
+ * pinned a device a remote agent couldn't run) so this id is a store-scoping
4809
+ * concern, not a probe-routing one.
4810
4810
  */
4811
4811
  localProbeNodeId() {
4812
4812
  const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
@@ -4814,9 +4814,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4814
4814
  }
4815
4815
  async fetchProbeGatingData() {
4816
4816
  try {
4817
- const api = this.addonCtx?.api;
4818
- if (!api) return { hardware: null };
4819
- return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4817
+ return { hardware: await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null };
4820
4818
  } catch {
4821
4819
  return { hardware: null };
4822
4820
  }
@@ -6202,15 +6200,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6202
6200
  * Phase 2b — resolve the engine from the addon's new schema-backed
6203
6201
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
6204
6202
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
6205
- * Returns null when neither source has anything; the caller keeps
6206
- * the onnx floor set at construction until autoPickAndPersist() runs.
6203
+ *
6204
+ * The `'auto'` sentinel is treated as NO override (returns null) so the
6205
+ * caller re-resolves LIVE via `resolveAutoEngine()` — `'auto'` is the
6206
+ * self-healing default, never a persisted concrete backend. An explicit
6207
+ * concrete backend (onnx/openvino/coreml) is honored as an operator override.
6208
+ * Returns null when neither source has a concrete selection; the caller keeps
6209
+ * the onnx floor set at construction until `resolveAutoEngine()` runs.
6207
6210
  */
6208
6211
  async loadEngine() {
6209
6212
  const store = await this.readStore();
6210
6213
  const storedRuntime = store["engineRuntime"];
6211
6214
  const node = this.localProbeNodeId();
6212
6215
  const storedBackend = readNodeEngineValue(store, "engineBackend", node);
6213
- if (typeof storedBackend === "string" && storedBackend.length > 0) {
6216
+ if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
6214
6217
  const backend = storedBackend;
6215
6218
  const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
6216
6219
  const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
@@ -6364,12 +6367,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6364
6367
  return { success: true };
6365
6368
  }
6366
6369
  async reprobeEngine() {
6367
- const api = this.addonCtx?.api;
6368
6370
  let hardware = null;
6369
- if (api) try {
6370
- hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
6371
+ try {
6372
+ hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6371
6373
  } catch {}
6372
- const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
6374
+ const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
6373
6375
  const best = {
6374
6376
  runtime: "python",
6375
6377
  backend: pick.runtimeId,
@@ -6379,17 +6381,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6379
6381
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
6380
6382
  const rpNode = this.localProbeNodeId();
6381
6383
  if (pick.runtimeId !== "onnx" || hardware !== null) {
6382
- await this.writeStore({
6383
- [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
6384
- [nodeEngineKey("engineBackend", rpNode)]: best.backend,
6385
- [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
6386
- });
6387
- this.log.info("Re-probed engine (platform-deterministic) — wrote back", { meta: {
6384
+ await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
6385
+ this.log.info("Re-probed engine (local kernel) — updated probedBestEngine hint", { meta: {
6388
6386
  backend: best.backend,
6389
6387
  device: best.device ?? null,
6390
6388
  probedBestEngine: probedLabel
6391
6389
  } });
6392
- } else this.log.info("Re-probe: onnx floor pending gpu probe — NOT persisting (next boot retries)", { meta: {
6390
+ } else this.log.info("Re-probe: onnx floor pending gpu probe — hint NOT persisted", { meta: {
6393
6391
  backend: best.backend,
6394
6392
  device: best.device ?? null
6395
6393
  } });
@@ -6806,7 +6804,7 @@ var DEFAULT_CONFIG = {
6806
6804
  intraOpThreads: 0,
6807
6805
  numWorkers: 0,
6808
6806
  engineRuntime: "python",
6809
- engineBackend: "onnx",
6807
+ engineBackend: "auto",
6810
6808
  engineDevice: "cpu",
6811
6809
  probedBestEngine: "",
6812
6810
  activeEngine: ""
@@ -6859,7 +6857,7 @@ var POOL_BOUND_KEYS = [
6859
6857
  "intraOpThreads",
6860
6858
  "numWorkers"
6861
6859
  ];
6862
- var DetectionPipelineAddon = class extends BaseAddon {
6860
+ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
6863
6861
  provider = null;
6864
6862
  /** Last non-null probed hardware PER NODE — reused when the probe transiently
6865
6863
  * returns null so offered backends / device lists don't collapse. Keyed by
@@ -6934,7 +6932,10 @@ var DetectionPipelineAddon = class extends BaseAddon {
6934
6932
  type: "select",
6935
6933
  key: "engineBackend",
6936
6934
  label: "Execution provider",
6937
- options: [...STATIC_BACKEND_OPTIONS],
6935
+ options: [{
6936
+ value: "auto",
6937
+ label: "Auto (best for this node)"
6938
+ }, ...STATIC_BACKEND_OPTIONS],
6938
6939
  default: DEFAULT_CONFIG.engineBackend,
6939
6940
  immediate: true,
6940
6941
  perNode: true
@@ -7075,12 +7076,17 @@ var DetectionPipelineAddon = class extends BaseAddon {
7075
7076
  const env = await this.probeHardwareEnv(targetNode);
7076
7077
  const hardware = env.hardware;
7077
7078
  const offered = supportedRuntimes(env);
7078
- const runtimeBackends = offered.map((id) => ({
7079
+ const runtimeBackends = [{
7080
+ value: "auto",
7081
+ label: "Auto (best for this node)"
7082
+ }, ...offered.map((id) => ({
7079
7083
  value: id,
7080
7084
  label: runtimeLabel(id)
7081
- }));
7085
+ }))];
7082
7086
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
7087
+ const isAuto = storedBackend === "auto";
7083
7088
  const backend = (() => {
7089
+ if (isAuto) return pickBestRuntime(env, null).runtimeId;
7084
7090
  const rid = toRuntimeId(storedBackend);
7085
7091
  if (rid === "onnx") return "onnx";
7086
7092
  if (offered.includes(rid)) return rid;
@@ -7091,9 +7097,10 @@ var DetectionPipelineAddon = class extends BaseAddon {
7091
7097
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
7092
7098
  const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
7093
7099
  const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
7100
+ const displayBackend = isAuto ? "auto" : backend;
7094
7101
  const raw = {
7095
7102
  ...merged,
7096
- engineBackend: backend,
7103
+ engineBackend: displayBackend,
7097
7104
  engineDevice: device,
7098
7105
  activeEngine
7099
7106
  };
@@ -7211,7 +7218,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
7211
7218
  const api = this.ctxIfReady?.api;
7212
7219
  if (!api) return null;
7213
7220
  const node = nodeId ?? this.localNodeId();
7214
- const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, nodePin(node)))?.hardware;
7221
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query({ nodeId: node }))?.hardware;
7215
7222
  if (!hw) return null;
7216
7223
  return {
7217
7224
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -7249,7 +7256,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
7249
7256
  * a reason to disagree.
7250
7257
  */
7251
7258
  resolveBackendTuning() {
7252
- const t = tuningFor(toRuntimeId(this.config.engineBackend));
7259
+ const t = tuningFor(this.config.engineBackend === "auto" ? this.provider?.resolvedEngineBackend() ?? "onnx" : toRuntimeId(this.config.engineBackend));
7253
7260
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
7254
7261
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
7255
7262
  return {
@@ -7262,6 +7269,34 @@ var DetectionPipelineAddon = class extends BaseAddon {
7262
7269
  numWorkers: num(t["numWorkers"], 1)
7263
7270
  };
7264
7271
  }
7272
+ /**
7273
+ * Construct a fresh provider with the given effective tuning. Single source of
7274
+ * truth for the provider wiring — shared by `onInitialize`, the post-resolve
7275
+ * tuning reconciliation, and the `onConfigChanged` pool respawn.
7276
+ */
7277
+ buildProvider(modelsDir, tuning) {
7278
+ if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
7279
+ return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7280
+ concurrency: tuning.concurrency,
7281
+ tuning: {
7282
+ batchMode: tuning.batchMode,
7283
+ windowMs: tuning.windowMs,
7284
+ maxBatchSize: tuning.maxBatchSize,
7285
+ numStreams: tuning.numStreams,
7286
+ intraOpThreads: tuning.intraOpThreads
7287
+ },
7288
+ numWorkers: tuning.numWorkers,
7289
+ pythonPath: this.pythonPath,
7290
+ pythonAddonDir: this.pythonAddonDir
7291
+ });
7292
+ }
7293
+ /**
7294
+ * Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
7295
+ * post-resolve / config-change respawn is actually needed).
7296
+ */
7297
+ static poolTuningEqual(a, b) {
7298
+ return a.concurrency === b.concurrency && a.batchMode === b.batchMode && a.windowMs === b.windowMs && a.maxBatchSize === b.maxBatchSize && a.numStreams === b.numStreams && a.intraOpThreads === b.intraOpThreads && a.numWorkers === b.numWorkers;
7299
+ }
7265
7300
  async onInitialize() {
7266
7301
  const modelsDir = await this.resolveModelsDir();
7267
7302
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
@@ -7270,22 +7305,20 @@ var DetectionPipelineAddon = class extends BaseAddon {
7270
7305
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
7271
7306
  else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
7272
7307
  await this.proactivelyInstallOpenvino();
7273
- const effectiveTuning = this.resolveBackendTuning();
7274
- this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7275
- concurrency: effectiveTuning.concurrency,
7276
- tuning: {
7277
- batchMode: effectiveTuning.batchMode,
7278
- windowMs: effectiveTuning.windowMs,
7279
- maxBatchSize: effectiveTuning.maxBatchSize,
7280
- numStreams: effectiveTuning.numStreams,
7281
- intraOpThreads: effectiveTuning.intraOpThreads
7282
- },
7283
- numWorkers: effectiveTuning.numWorkers,
7284
- pythonPath: this.pythonPath,
7285
- pythonAddonDir: this.pythonAddonDir
7286
- });
7308
+ const initialTuning = this.resolveBackendTuning();
7309
+ this.provider = this.buildProvider(modelsDir, initialTuning);
7287
7310
  await this.provider.init();
7288
7311
  await this.provider.setApi(this.ctx);
7312
+ const resolvedTuning = this.resolveBackendTuning();
7313
+ if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
7314
+ this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
7315
+ from: initialTuning,
7316
+ to: resolvedTuning
7317
+ } });
7318
+ this.provider = this.buildProvider(modelsDir, resolvedTuning);
7319
+ await this.provider.init();
7320
+ await this.provider.setApi(this.ctx);
7321
+ }
7289
7322
  if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
7290
7323
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
7291
7324
  });
@@ -7342,13 +7375,18 @@ var DetectionPipelineAddon = class extends BaseAddon {
7342
7375
  * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
7343
7376
  *
7344
7377
  * Failure is non-fatal: a warning is logged and the addon continues with
7345
- * the onnx-cpu baseline. The hardware query itself is also best-effort —
7346
- * if the platform-probe cap isn't wired yet the error is caught here.
7378
+ * the onnx-cpu baseline. The hardware resolve itself is also best-effort —
7379
+ * if the kernel resolver isn't wired yet the error is caught here.
7347
7380
  */
7348
7381
  async proactivelyInstallOpenvino() {
7349
7382
  if (!this.pythonAddonDir || !this.pythonPath) return;
7350
7383
  try {
7351
- const hardware = await this.ctx.api.platformProbe.getHardware.query();
7384
+ const hw = await this.ctx.kernel?.inferenceEngine?.resolveHardware();
7385
+ const hardware = {
7386
+ platform: process.platform,
7387
+ gpu: hw?.gpu ?? null,
7388
+ npu: hw?.npu ?? null
7389
+ };
7352
7390
  if (!shouldInstallOpenvino(hardware)) return;
7353
7391
  this.ctx.logger.info("Intel hardware detected — proactively installing OpenVINO Python package", { meta: {
7354
7392
  gpu: hardware.gpu?.type ?? null,
@@ -7427,21 +7465,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
7427
7465
  this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
7428
7466
  }
7429
7467
  const modelsDir = await this.resolveModelsDir();
7430
- if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available during respawn");
7431
- const effectiveTuning = this.resolveBackendTuning();
7432
- this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
7433
- concurrency: effectiveTuning.concurrency,
7434
- tuning: {
7435
- batchMode: effectiveTuning.batchMode,
7436
- windowMs: effectiveTuning.windowMs,
7437
- maxBatchSize: effectiveTuning.maxBatchSize,
7438
- numStreams: effectiveTuning.numStreams,
7439
- intraOpThreads: effectiveTuning.intraOpThreads
7440
- },
7441
- numWorkers: effectiveTuning.numWorkers,
7442
- pythonPath: this.pythonPath,
7443
- pythonAddonDir: this.pythonAddonDir
7444
- });
7468
+ this.provider = this.buildProvider(modelsDir, this.resolveBackendTuning());
7445
7469
  await this.provider.init();
7446
7470
  await this.provider.setApi(this.ctx);
7447
7471
  await this.provider.warmPool();
@@ -16403,7 +16403,10 @@ var AgentLoadSummarySchema = object({
16403
16403
  online: boolean(),
16404
16404
  load: RunnerLocalLoadSchema,
16405
16405
  /** Computed score used by the L2 capacity balancer (lower = less loaded). */
16406
- score: number()
16406
+ score: number(),
16407
+ /** This node's decode hwaccel backend (per-node `probedBestHwaccel`), or null
16408
+ * when not yet probed — for the cluster Pipeline table UI (P0.2). */
16409
+ decodeHwaccel: string().nullable()
16407
16410
  });
16408
16411
  /**
16409
16412
  * Aggregate metrics across the whole detection cluster. Replaces the legacy
@@ -19034,7 +19037,7 @@ var HardwareEncoderIdSchema = _enum([
19034
19037
  "libx264",
19035
19038
  "libx265"
19036
19039
  ]);
19037
- var HardwareEncodersSchema = object({
19040
+ object({
19038
19041
  encoders: array(object({
19039
19042
  encoder: HardwareEncoderIdSchema,
19040
19043
  codec: _enum(["H264", "H265"]),
@@ -19053,15 +19056,7 @@ var HardwareEncodersSchema = object({
19053
19056
  defaultH265: HardwareEncoderIdSchema,
19054
19057
  probedAt: number()
19055
19058
  });
19056
- /**
19057
- * Decode-side companion to {@link HardwareEncodersSchema}: the `-hwaccel`
19058
- * methods the configured ffmpeg binary actually supports (parsed from
19059
- * `ffmpeg -hwaccels`). Used to gate decode-hwaccel attempts so the transcode
19060
- * egress never spends a spawn on a backend this build cannot offer. Per-stream
19061
- * decode fragility (e.g. VideoToolbox HEVC) is still handled by the egress's
19062
- * software fallback — this only filters out wholly-unsupported backends.
19063
- */
19064
- var HardwareDecodeAccelsSchema = object({
19059
+ object({
19065
19060
  methods: array(string()).readonly(),
19066
19061
  probedAt: number()
19067
19062
  });
@@ -19124,13 +19119,7 @@ var ResolvedInferenceConfigSchema = object({
19124
19119
  format: ModelFormatSchema,
19125
19120
  reason: string()
19126
19121
  });
19127
- method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema), method(object({ requirements: array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema), method(object({ prefer: HwAccelBackendInputSchema }), HwAccelResolutionSchema), method(_void(), HardwareEncodersSchema), method(_void(), HardwareEncodersSchema, {
19128
- kind: "mutation",
19129
- auth: "admin"
19130
- }), method(_void(), HardwareDecodeAccelsSchema), method(_void(), HardwareDecodeAccelsSchema, {
19131
- kind: "mutation",
19132
- auth: "admin"
19133
- });
19122
+ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema), method(object({ requirements: array(ModelRequirementSchema).readonly() }), ResolvedInferenceConfigSchema), method(object({ prefer: HwAccelBackendInputSchema }), HwAccelResolutionSchema);
19134
19123
  var PtzPresetSchema = object({
19135
19124
  id: string(),
19136
19125
  name: string()
@@ -22788,30 +22777,6 @@ Object.freeze({
22788
22777
  addonId: null,
22789
22778
  access: "view"
22790
22779
  },
22791
- "platformProbe.getHardwareDecodeAccels": {
22792
- capName: "platform-probe",
22793
- capScope: "system",
22794
- addonId: null,
22795
- access: "view"
22796
- },
22797
- "platformProbe.getHardwareEncoders": {
22798
- capName: "platform-probe",
22799
- capScope: "system",
22800
- addonId: null,
22801
- access: "view"
22802
- },
22803
- "platformProbe.refreshHardwareDecodeAccels": {
22804
- capName: "platform-probe",
22805
- capScope: "system",
22806
- addonId: null,
22807
- access: "create"
22808
- },
22809
- "platformProbe.refreshHardwareEncoders": {
22810
- capName: "platform-probe",
22811
- capScope: "system",
22812
- addonId: null,
22813
- access: "create"
22814
- },
22815
22780
  "platformProbe.resolveHwAccel": {
22816
22781
  capName: "platform-probe",
22817
22782
  capScope: "system",