@camstack/addon-pipeline 1.0.7 → 1.0.8

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 (51) hide show
  1. package/dist/audio-analyzer/index.js +2 -3
  2. package/dist/audio-analyzer/index.mjs +2 -3
  3. package/dist/audio-codec-nodeav/index.js +2 -2
  4. package/dist/audio-codec-nodeav/index.mjs +2 -2
  5. package/dist/decoder-nodeav/index.js +1 -1
  6. package/dist/decoder-nodeav/index.mjs +1 -1
  7. package/dist/detection-pipeline/index.js +373 -184
  8. package/dist/detection-pipeline/index.mjs +373 -184
  9. package/dist/{dist-CjrjeaDd.mjs → dist-BA6DR_jV.mjs} +128 -5
  10. package/dist/{dist-G45MVm6i.js → dist-BLcTVvol.js} +151 -4
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +1 -1
  14. package/dist/pipeline-runner/index.mjs +1 -1
  15. package/dist/recorder/index.js +3 -3
  16. package/dist/recorder/index.mjs +3 -3
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Tbqpu0v3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BFy9iszl.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COa17XL2.mjs +26 -0
  20. package/dist/stream-broker/{hostInit-tIev5Gd9.mjs → hostInit-zRy9SzlX.mjs} +3 -3
  21. package/dist/stream-broker/index.js +19 -19
  22. package/dist/stream-broker/index.mjs +19 -19
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C0kKwNX_.js → MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js} +1 -1
  25. package/embed-dist/assets/MotionZonesSettings-NcxxQN8r-CQzEnQoq.js +1 -0
  26. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C2SRtNe6.js → PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js} +1 -1
  27. package/embed-dist/assets/{index-B2LRyXWh.js → index-CSuLwWK-.js} +3 -3
  28. package/embed-dist/index.html +1 -1
  29. package/package.json +1 -1
  30. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  31. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  34. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  35. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  36. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  37. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  38. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  39. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  40. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  41. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  42. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  43. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  44. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  45. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  46. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  47. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  48. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  49. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  50. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DCsgcqTa.mjs +0 -26
  51. package/embed-dist/assets/MotionZonesSettings-C1EEbk2V-CYtJc892.js +0 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_model_download_service_C7AjBsX9 = require("../model-download-service-C7AjBsX9-rXY-VFDk.js");
6
- const require_dist = require("../dist-G45MVm6i.js");
6
+ const require_dist = require("../dist-BLcTVvol.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_model_download_service_C7AjBsX9.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -2165,6 +2165,78 @@ var EngineFactory = class {
2165
2165
  }
2166
2166
  };
2167
2167
  //#endregion
2168
+ //#region src/detection-pipeline/engine-store-keys.ts
2169
+ /**
2170
+ * Per-node scoping for the detection-pipeline engine cascade.
2171
+ *
2172
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
2173
+ * settings-store cap is hub-resident; every node's detection instance reads and
2174
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
2175
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
2176
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
2177
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
2178
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
2179
+ * another node that has no NPU.
2180
+ *
2181
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
2182
+ * Everything else in the store stays shared. A legacy un-scoped value (written
2183
+ * before this change, or by an older build) is read as a migration fallback and
2184
+ * re-persisted under the node-scoped key on the next write.
2185
+ */
2186
+ var ENGINE_CASCADE_KEYS = [
2187
+ "engineBackend",
2188
+ "engineDevice",
2189
+ "probedBestEngine"
2190
+ ];
2191
+ function isEngineCascadeKey(key) {
2192
+ return ENGINE_CASCADE_KEYS.includes(key);
2193
+ }
2194
+ /**
2195
+ * Normalise a raw kernel node id to the bare node id used for scoping.
2196
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
2197
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
2198
+ */
2199
+ function normalizeEngineNodeId(rawNodeId) {
2200
+ const raw = rawNodeId ?? "hub";
2201
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
2202
+ }
2203
+ /** The node-scoped store key for an engine cascade field. */
2204
+ function nodeEngineKey(base, nodeId) {
2205
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
2206
+ }
2207
+ /**
2208
+ * Read an engine cascade value for a node: the node-scoped key if present,
2209
+ * otherwise the legacy un-scoped value (migration), otherwise undefined.
2210
+ */
2211
+ function readNodeEngineValue(store, base, nodeId) {
2212
+ const scoped = store[nodeEngineKey(base, nodeId)];
2213
+ return scoped !== void 0 ? scoped : store[base];
2214
+ }
2215
+ /**
2216
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
2217
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
2218
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
2219
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
2220
+ * node's form).
2221
+ */
2222
+ function projectNodeEngine(store, nodeId) {
2223
+ const out = {};
2224
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
2225
+ for (const key of Object.keys(store)) {
2226
+ const atIdx = key.indexOf("@");
2227
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
2228
+ scopedForAnyNode.add(key);
2229
+ continue;
2230
+ }
2231
+ out[key] = store[key];
2232
+ }
2233
+ for (const base of ENGINE_CASCADE_KEYS) {
2234
+ const value = readNodeEngineValue(store, base, nodeId);
2235
+ if (value !== void 0) out[base] = value;
2236
+ }
2237
+ return out;
2238
+ }
2239
+ //#endregion
2168
2240
  //#region src/detection-pipeline/postprocess/dispatch.ts
2169
2241
  var VALID_KINDS = new Set([
2170
2242
  "detections",
@@ -2204,7 +2276,7 @@ function postprocessYolo(output, stepDef) {
2204
2276
  const letterbox = output.letterbox;
2205
2277
  const dets = [];
2206
2278
  for (let i = 0; i < numBoxes; i++) {
2207
- const cx = tensor[0 * numBoxes + i];
2279
+ const cx = tensor[i];
2208
2280
  const cy = tensor[1 * numBoxes + i];
2209
2281
  const w = tensor[2 * numBoxes + i];
2210
2282
  const h = tensor[3 * numBoxes + i];
@@ -2360,7 +2432,7 @@ function postprocessArcface(output, _stepDef) {
2360
2432
  let sumSq = 0;
2361
2433
  for (let i = 0; i < tensor.length; i++) sumSq += tensor[i] * tensor[i];
2362
2434
  const norm = Math.sqrt(sumSq);
2363
- const normalized = new Array(tensor.length);
2435
+ const normalized = Array.from({ length: tensor.length });
2364
2436
  for (let i = 0; i < tensor.length; i++) normalized[i] = norm === 0 ? 0 : tensor[i] / norm;
2365
2437
  return {
2366
2438
  kind: "embedding",
@@ -3535,34 +3607,69 @@ function walkFieldsForDefaults(fields, out) {
3535
3607
  }
3536
3608
  //#endregion
3537
3609
  //#region src/detection-pipeline/runtimes.ts
3538
- var AUTO = {
3539
- value: "auto",
3540
- label: "Auto"
3541
- };
3542
- var CPU = {
3543
- value: "cpu",
3544
- label: "CPU"
3545
- };
3546
- var RUNTIMES = [
3547
- {
3548
- id: "onnx",
3610
+ var KNOWN_PLATFORMS = [
3611
+ "darwin",
3612
+ "linux",
3613
+ "win32"
3614
+ ];
3615
+ var KNOWN_ARCHES = ["arm64", "x64"];
3616
+ var KNOWN_GPU_TYPES = [
3617
+ "nvidia",
3618
+ "amd",
3619
+ "intel",
3620
+ "apple"
3621
+ ];
3622
+ var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
3623
+ function toKnownPlatform(p) {
3624
+ return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
3625
+ }
3626
+ function toKnownArch(a) {
3627
+ return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
3628
+ }
3629
+ function gpuInfoFrom(hw) {
3630
+ if (!hw.gpu) return null;
3631
+ const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
3632
+ if (!type) return null;
3633
+ return {
3634
+ type,
3635
+ name: ""
3636
+ };
3637
+ }
3638
+ function npuInfoFrom(hw) {
3639
+ if (!hw.npu) return null;
3640
+ const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
3641
+ if (!type) return null;
3642
+ return { type };
3643
+ }
3644
+ function envToHardwareInfo(env) {
3645
+ if (!env.hardware) return null;
3646
+ return {
3647
+ platform: toKnownPlatform(env.platform),
3648
+ arch: toKnownArch(env.arch),
3649
+ cpuModel: "",
3650
+ cpuCores: 0,
3651
+ totalRAM_MB: 0,
3652
+ availableRAM_MB: 0,
3653
+ gpu: gpuInfoFrom(env.hardware),
3654
+ npu: npuInfoFrom(env.hardware)
3655
+ };
3656
+ }
3657
+ function probedToHardwareInfo(hw) {
3658
+ if (!hw) return null;
3659
+ return {
3660
+ platform: toKnownPlatform(process.platform),
3661
+ arch: toKnownArch(process.arch),
3662
+ cpuModel: "",
3663
+ cpuCores: 0,
3664
+ totalRAM_MB: 0,
3665
+ availableRAM_MB: 0,
3666
+ gpu: gpuInfoFrom(hw),
3667
+ npu: npuInfoFrom(hw)
3668
+ };
3669
+ }
3670
+ var RUNTIME_DETAIL = {
3671
+ onnx: {
3549
3672
  label: "ONNX Runtime",
3550
- supports: () => true,
3551
- devices: (hw) => {
3552
- if (!hw) return [CPU];
3553
- const out = [CPU];
3554
- if (hw.gpu?.type === "nvidia") out.push({
3555
- value: "cuda",
3556
- label: "CUDA"
3557
- });
3558
- if (hw.npu?.type === "apple-ane") out.push({
3559
- value: "coreml",
3560
- label: "CoreML EP"
3561
- });
3562
- return out;
3563
- },
3564
- defaultDevice: "cpu",
3565
- modelFormat: "onnx",
3566
3673
  pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3567
3674
  tuning: {
3568
3675
  concurrency: 4,
@@ -3571,25 +3678,8 @@ var RUNTIMES = [
3571
3678
  intraOpThreads: 0
3572
3679
  }
3573
3680
  },
3574
- {
3575
- id: "openvino",
3681
+ openvino: {
3576
3682
  label: "OpenVINO",
3577
- supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
3578
- devices: (hw) => {
3579
- if (!hw) return [AUTO];
3580
- const out = [AUTO, CPU];
3581
- if (hw.gpu?.type === "intel") out.push({
3582
- value: "gpu",
3583
- label: "GPU"
3584
- });
3585
- if (hw.npu?.type === "intel-npu") out.push({
3586
- value: "npu",
3587
- label: "NPU"
3588
- });
3589
- return out;
3590
- },
3591
- defaultDevice: "auto",
3592
- modelFormat: "openvino",
3593
3683
  pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3594
3684
  tuning: {
3595
3685
  concurrency: 1,
@@ -3597,29 +3687,8 @@ var RUNTIMES = [
3597
3687
  numStreams: 0
3598
3688
  }
3599
3689
  },
3600
- {
3601
- id: "coreml",
3690
+ coreml: {
3602
3691
  label: "CoreML",
3603
- supports: (env) => env.platform === "darwin",
3604
- devices: (hw) => {
3605
- const all = {
3606
- value: "all",
3607
- label: "All (ANE + GPU + CPU)"
3608
- };
3609
- if (!hw) return [all];
3610
- const out = [all];
3611
- if (hw.npu?.type === "apple-ane") out.push({
3612
- value: "ane",
3613
- label: "Apple Neural Engine"
3614
- });
3615
- out.push({
3616
- value: "gpu",
3617
- label: "GPU"
3618
- }, CPU);
3619
- return out;
3620
- },
3621
- defaultDevice: "all",
3622
- modelFormat: "coreml",
3623
3692
  pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3624
3693
  tuning: {
3625
3694
  concurrency: 1,
@@ -3629,32 +3698,50 @@ var RUNTIMES = [
3629
3698
  numWorkers: 1
3630
3699
  }
3631
3700
  }
3632
- ];
3633
- function def(id) {
3634
- const d = RUNTIMES.find((r) => r.id === id);
3635
- if (!d) throw new Error(`Unknown runtime: ${id}`);
3636
- return d;
3637
- }
3701
+ };
3702
+ /**
3703
+ * Returns the list of supported runtime IDs for the given hardware env.
3704
+ * Delegates to `@camstack/types` `supportedRuntimes`.
3705
+ */
3638
3706
  function supportedRuntimes(env) {
3639
- return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3707
+ return require_dist.supportedRuntimes(envToHardwareInfo(env));
3640
3708
  }
3709
+ /**
3710
+ * Returns the device options for a given runtime and probed hardware.
3711
+ * Delegates to `@camstack/types` `runtimeDevices`.
3712
+ */
3641
3713
  function runtimeDevices(id, hardware) {
3642
- return def(id).devices(hardware);
3714
+ return require_dist.runtimeDevices(id, probedToHardwareInfo(hardware));
3643
3715
  }
3716
+ /**
3717
+ * Returns the default device string for a given runtime.
3718
+ * Delegates to `@camstack/types` `defaultDeviceFor`.
3719
+ */
3644
3720
  function defaultDeviceFor(id) {
3645
- return def(id).defaultDevice;
3646
- }
3647
- function pythonRequirementsFor(id) {
3648
- return def(id).pythonRequirements;
3721
+ return require_dist.defaultDeviceFor(id);
3649
3722
  }
3723
+ /** Model format for each inference runtime supported by the detection pipeline. */
3724
+ var RUNTIME_FORMAT = {
3725
+ onnx: "onnx",
3726
+ openvino: "openvino",
3727
+ coreml: "coreml"
3728
+ };
3729
+ /**
3730
+ * Returns the model format required for a given runtime.
3731
+ * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
3732
+ * matching the engine-provisioner's own ModelFormat type.
3733
+ */
3650
3734
  function modelFormatFor(id) {
3651
- return def(id).modelFormat;
3735
+ return RUNTIME_FORMAT[id];
3736
+ }
3737
+ function pythonRequirementsFor(id) {
3738
+ return RUNTIME_DETAIL[id].pythonRequirements;
3652
3739
  }
3653
3740
  function tuningFor(id) {
3654
- return def(id).tuning;
3741
+ return RUNTIME_DETAIL[id].tuning;
3655
3742
  }
3656
3743
  function runtimeLabel(id) {
3657
- return def(id).label;
3744
+ return RUNTIME_DETAIL[id].label;
3658
3745
  }
3659
3746
  /**
3660
3747
  * Proactive-install hint kept for back-compat (re-exported from index.ts).
@@ -3995,6 +4082,27 @@ function toProbedHardware(hw) {
3995
4082
  gpu: hw.gpu ? { type: hw.gpu.type } : null
3996
4083
  };
3997
4084
  }
4085
+ var ONNX_FLOOR = {
4086
+ runtime: "python",
4087
+ backend: "onnx",
4088
+ format: "onnx",
4089
+ device: "cpu"
4090
+ };
4091
+ /**
4092
+ * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
4093
+ * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
4094
+ * result is identical (onnx / cpu) but is now derived through the shared rules
4095
+ * instead of duplicated inline.
4096
+ */
4097
+ function onnxFloorPick() {
4098
+ const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
4099
+ return {
4100
+ runtime: "python",
4101
+ backend: pick.runtimeId,
4102
+ format: modelFormatFor(pick.runtimeId),
4103
+ device: pick.device
4104
+ };
4105
+ }
3998
4106
  var DetectionPipelineProvider = class DetectionPipelineProvider {
3999
4107
  modelsDir;
4000
4108
  eventBus;
@@ -4084,7 +4192,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4084
4192
  this.readStore = () => settings.readAddonStore();
4085
4193
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4086
4194
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4087
- this.currentEngine = DetectionPipelineProvider.detectBestEngine();
4195
+ this.currentEngine = ONNX_FLOOR;
4088
4196
  this.log.info("Engine selected (default)", { meta: {
4089
4197
  runtime: this.currentEngine.runtime,
4090
4198
  backend: this.currentEngine.backend,
@@ -4150,62 +4258,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4150
4258
  isReady() {
4151
4259
  return this.ready;
4152
4260
  }
4153
- /** Detect the best inference engine for this platform. */
4154
- static detectBestEngine() {
4155
- const platform = process.platform;
4156
- const arch = process.arch;
4157
- if (platform === "darwin" && arch === "arm64") return {
4158
- runtime: "python",
4159
- backend: "coreml",
4160
- format: "coreml",
4161
- device: "all"
4162
- };
4163
- if (platform === "darwin") return {
4164
- runtime: "python",
4165
- backend: "coreml",
4166
- format: "coreml",
4167
- device: "gpu"
4168
- };
4169
- try {
4170
- const { execFileSync } = require("node:child_process");
4171
- execFileSync("nvidia-smi", ["--query-gpu=name", "--format=csv,noheader"], {
4172
- timeout: 3e3,
4173
- stdio: "pipe"
4174
- });
4175
- return {
4176
- runtime: "python",
4177
- backend: "onnx",
4178
- format: "onnx",
4179
- device: "cuda"
4180
- };
4181
- } catch {}
4182
- try {
4183
- const fsmod = require("node:fs");
4184
- const isIntel = require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
4185
- const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
4186
- const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
4187
- if (isIntel && (hasIgpu || hasNpu)) return {
4188
- runtime: "python",
4189
- backend: "openvino",
4190
- format: "openvino",
4191
- device: "auto"
4192
- };
4193
- } catch {}
4194
- return {
4195
- runtime: "python",
4196
- backend: "onnx",
4197
- format: "onnx",
4198
- device: "cpu"
4199
- };
4200
- }
4201
4261
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
4202
4262
  async setApi(addonCtx) {
4203
4263
  this.addonCtx = addonCtx;
4204
- if (this.needsAutoPick) {
4264
+ if (this.needsAutoPick) if (this.addonCtx.useCapability("platform-probe").isReady) {
4205
4265
  await this.autoPickAndPersist();
4206
4266
  this.needsAutoPick = false;
4267
+ this.startProvisioningForCurrentEngine();
4268
+ } else {
4269
+ this.startProvisioningForCurrentEngine();
4270
+ const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4271
+ if (state !== "ready") return;
4272
+ unsubscribe();
4273
+ if (!this.needsAutoPick) return;
4274
+ this.autoPickAndPersist().then(() => {
4275
+ this.needsAutoPick = false;
4276
+ this.startProvisioningForCurrentEngine();
4277
+ });
4278
+ });
4279
+ this.addonCtx.addDisposer(unsubscribe);
4207
4280
  }
4208
- this.startProvisioningForCurrentEngine();
4281
+ else this.startProvisioningForCurrentEngine();
4209
4282
  }
4210
4283
  /**
4211
4284
  * Auto-pick the best supported runtime at first boot (no stored engine).
@@ -4220,7 +4293,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4220
4293
  try {
4221
4294
  const api = this.addonCtx?.api;
4222
4295
  if (api) {
4223
- const caps = await api.platformProbe.getCapabilities.query();
4296
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
4224
4297
  hardware = caps?.hardware ?? null;
4225
4298
  const bs = caps?.bestScore;
4226
4299
  if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
@@ -4234,9 +4307,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4234
4307
  device: pick.device
4235
4308
  };
4236
4309
  this.currentEngine = engine;
4310
+ const apNode = this.localProbeNodeId();
4237
4311
  await this.writeStore({
4238
- engineBackend: pick.runtimeId,
4239
- engineDevice: pick.device
4312
+ [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4313
+ [nodeEngineKey("engineDevice", apNode)]: pick.device
4240
4314
  });
4241
4315
  this.log.info("Auto-picked engine at first boot", { meta: {
4242
4316
  backend: pick.runtimeId,
@@ -4409,11 +4483,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4409
4483
  * callers fall back to the registry's safe minimum. The engine OFFER derives
4410
4484
  * from hardware ONLY — install state (probe `scores`) never gates it.
4411
4485
  */
4486
+ /**
4487
+ * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4488
+ * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4489
+ * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4490
+ * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4491
+ * have) and pin a device the node can't run, breaking provisioning.
4492
+ */
4493
+ localProbeNodeId() {
4494
+ const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
4495
+ return raw.includes("/") ? raw.split("/")[0] : raw;
4496
+ }
4412
4497
  async fetchProbeGatingData() {
4413
4498
  try {
4414
4499
  const api = this.addonCtx?.api;
4415
4500
  if (!api) return { hardware: null };
4416
- return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
4501
+ return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4417
4502
  } catch {
4418
4503
  return { hardware: null };
4419
4504
  }
@@ -5478,7 +5563,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5478
5563
  this.engineFactory = null;
5479
5564
  this.executor = null;
5480
5565
  }
5481
- for (const id of [...this.deviceProxies.keys()]) this.releaseDeviceProxy(id);
5566
+ for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5482
5567
  }
5483
5568
  /**
5484
5569
  * Resolve and cache a {@link DeviceProxy} for the given camera. Pins
@@ -5662,25 +5747,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5662
5747
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
5663
5748
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
5664
5749
  * Returns null when neither source has anything; the caller keeps
5665
- * whatever `detectBestEngine()` picked at construction.
5750
+ * the onnx floor set at construction until autoPickAndPersist() runs.
5666
5751
  */
5667
5752
  async loadEngine() {
5668
5753
  const store = await this.readStore();
5669
5754
  const storedRuntime = store["engineRuntime"];
5670
- const storedBackend = store["engineBackend"];
5755
+ const node = this.localProbeNodeId();
5756
+ const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5671
5757
  if (typeof storedBackend === "string" && storedBackend.length > 0) {
5672
5758
  const backend = storedBackend;
5673
- const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5674
- const detected = DetectionPipelineProvider.detectBestEngine();
5759
+ const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5760
+ const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5761
+ const floor = onnxFloorPick();
5675
5762
  const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5676
5763
  if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
5677
- this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5764
+ this.log.warn("Stored engine backend unavailable on this node — falling back to onnx floor", { meta: {
5678
5765
  stored: migratedBackend,
5679
- fallback: `${detected.backend}`
5766
+ fallback: `${floor.backend}`
5680
5767
  } });
5681
- return detected;
5768
+ return floor;
5682
5769
  }
5683
- const device = storedDevice || detected.device;
5770
+ const device = storedDevice || floor.device;
5684
5771
  return {
5685
5772
  runtime: "python",
5686
5773
  backend: migratedBackend,
@@ -5852,18 +5939,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5852
5939
  const api = this.addonCtx?.api;
5853
5940
  let best;
5854
5941
  if (api) try {
5855
- const caps = await api.platformProbe.getCapabilities.query();
5942
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
5856
5943
  const bs = caps?.bestScore;
5857
5944
  if (bs && bs.runtime === "python") {
5858
5945
  const probeBackend = bs.backend;
5859
5946
  const probeDevice = (() => {
5860
5947
  const hw = caps.hardware;
5861
- if (probeBackend === "coreml") return hw?.npu?.type === "apple-ane" ? "ane" : "all";
5862
- if (probeBackend === "openvino") {
5863
- if (hw?.npu?.type === "intel-npu") return "npu";
5864
- if (hw?.gpu?.type === "intel") return "gpu";
5865
- return "auto";
5866
- }
5948
+ if (probeBackend === "openvino") return defaultDeviceFor("openvino");
5949
+ if (probeBackend === "coreml") return defaultDeviceFor("coreml");
5867
5950
  if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
5868
5951
  return "cpu";
5869
5952
  })();
@@ -5873,16 +5956,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5873
5956
  format: backendToFormat(probeBackend),
5874
5957
  device: probeDevice
5875
5958
  };
5876
- } else best = DetectionPipelineProvider.detectBestEngine();
5959
+ } else best = onnxFloorPick();
5877
5960
  } catch {
5878
- best = DetectionPipelineProvider.detectBestEngine();
5961
+ best = onnxFloorPick();
5879
5962
  }
5880
- else best = DetectionPipelineProvider.detectBestEngine();
5963
+ else best = onnxFloorPick();
5881
5964
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
5965
+ const rpNode = this.localProbeNodeId();
5882
5966
  await this.writeStore({
5883
- probedBestEngine: probedLabel,
5884
- engineBackend: best.backend,
5885
- engineDevice: best.device ?? "cpu"
5967
+ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
5968
+ [nodeEngineKey("engineBackend", rpNode)]: best.backend,
5969
+ [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
5886
5970
  });
5887
5971
  this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
5888
5972
  backend: best.backend,
@@ -6305,7 +6389,8 @@ var DEFAULT_CONFIG = {
6305
6389
  engineRuntime: "python",
6306
6390
  engineBackend: "onnx",
6307
6391
  engineDevice: "cpu",
6308
- probedBestEngine: ""
6392
+ probedBestEngine: "",
6393
+ activeEngine: ""
6309
6394
  };
6310
6395
  /** Derive the model-format from a backend value. Called by the provider. */
6311
6396
  function backendToFormat(backend) {
@@ -6348,6 +6433,15 @@ var POOL_BOUND_KEYS = [
6348
6433
  ];
6349
6434
  var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6350
6435
  provider = null;
6436
+ /** Last non-null probed hardware PER NODE — reused when the probe transiently
6437
+ * returns null so offered backends / device lists don't collapse. Keyed by
6438
+ * node because the hub addon probes other nodes when serving their config. */
6439
+ lastGoodHardwareByNode = /* @__PURE__ */ new Map();
6440
+ /** This node's effective engine selection, cached from the node-scoped store
6441
+ * so the synchronous `resolveBackendTuning` and the reprobe gate don't read
6442
+ * the cluster-shared bare keys. Refreshed at init + on every config change. */
6443
+ nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
6444
+ nodeProbedBestEngine = "";
6351
6445
  engineMetricsTimer = null;
6352
6446
  /** Snapshot-equality cache for engine-metrics emit. Most ticks
6353
6447
  * the engine inventory is unchanged (no model load/unload), so
@@ -6402,6 +6496,14 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6402
6496
  tooltip: "Re-probe engine"
6403
6497
  }]
6404
6498
  }),
6499
+ this.field({
6500
+ type: "text",
6501
+ key: "activeEngine",
6502
+ label: "Active engine",
6503
+ description: "The runtime/device the inference pool ACTUALLY loaded on this node, with its provisioning state (format: provider/device (state)). If this differs from the selected provider below — e.g. \"onnx/cpu (ready)\" while OpenVINO is selected — the chosen accelerator could not load on this host and the engine fell back to the ONNX-CPU baseline.",
6504
+ readonlyField: true,
6505
+ default: ""
6506
+ }),
6405
6507
  this.field({
6406
6508
  type: "select",
6407
6509
  key: "engineBackend",
@@ -6538,14 +6640,18 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6538
6640
  * probe is unreachable). Stored backend / device snap back to the registry
6539
6641
  * floor / default when they fall outside the offered set.
6540
6642
  */
6541
- async getGlobalSettings(overlay) {
6542
- const ctx = this.ctxIfReady;
6543
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
6643
+ async getGlobalSettings(overlay, _cap, nodeId) {
6644
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6645
+ const rawStored = await this.resolveUiSettingsStore();
6646
+ if (rawStored === null) return null;
6647
+ const stored = projectNodeEngine(rawStored, targetNode);
6648
+ const storedBackendRaw = stored["engineBackend"];
6649
+ if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6544
6650
  const merged = overlay ? {
6545
6651
  ...stored,
6546
6652
  ...overlay
6547
6653
  } : stored;
6548
- const env = await this.probeHardwareEnv();
6654
+ const env = await this.probeHardwareEnv(targetNode);
6549
6655
  const hardware = env.hardware;
6550
6656
  const offered = supportedRuntimes(env);
6551
6657
  const runtimeBackends = offered.map((id) => ({
@@ -6553,14 +6659,22 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6553
6659
  label: runtimeLabel(id)
6554
6660
  }));
6555
6661
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6556
- const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6662
+ const backend = (() => {
6663
+ const rid = toRuntimeId(storedBackend);
6664
+ if (rid === "onnx") return "onnx";
6665
+ if (offered.includes(rid)) return rid;
6666
+ return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6667
+ })();
6557
6668
  const deviceOptions = runtimeDevices(backend, hardware);
6558
6669
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6559
6670
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6671
+ const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6672
+ const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6560
6673
  const raw = {
6561
6674
  ...merged,
6562
6675
  engineBackend: backend,
6563
- engineDevice: device
6676
+ engineDevice: device,
6677
+ activeEngine
6564
6678
  };
6565
6679
  const schema = this.globalSettingsSchema();
6566
6680
  if (!schema) return { sections: [] };
@@ -6591,15 +6705,15 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6591
6705
  if (field.type === "slider" && "key" in field) {
6592
6706
  const tuned = tuning[field.key];
6593
6707
  const sliderField = field;
6594
- let patched = typeof tuned === "number" ? {
6708
+ let patchedField = typeof tuned === "number" ? {
6595
6709
  ...sliderField,
6596
6710
  default: tuned
6597
6711
  } : sliderField;
6598
- if (sliderField.key === "concurrency" && backend === "coreml") patched = {
6599
- ...patched,
6712
+ if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
6713
+ ...patchedField,
6600
6714
  max: 4
6601
6715
  };
6602
- return patched;
6716
+ return patchedField;
6603
6717
  }
6604
6718
  return field;
6605
6719
  })
@@ -6616,23 +6730,41 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6616
6730
  * Protected seam — overridable by tests (canned hardware) and reused by the
6617
6731
  * Phase 2 auto-pick path. NEVER reads install state.
6618
6732
  */
6619
- async probeHardwareEnv() {
6620
- const hardware = await this.resolveProbeHardware();
6733
+ /**
6734
+ * Resolve the persisted UI settings store, or `null` when it can't be read
6735
+ * right now (ctx/settings not wired — mid-restart). A readable-but-empty
6736
+ * store (genuine first boot) returns `{}`, never null. `getGlobalSettings`
6737
+ * uses the null signal to avoid serving fabricated empty-store defaults
6738
+ * during the restart window. Seam kept protected so tests can drive the
6739
+ * unreadable vs empty vs populated cases without faking a full AddonContext.
6740
+ */
6741
+ async resolveUiSettingsStore() {
6742
+ const settings = this.ctxIfReady?.settings;
6743
+ if (!settings) return null;
6744
+ return await settings.readAddonStore() ?? {};
6745
+ }
6746
+ async probeHardwareEnv(nodeId) {
6747
+ const node = nodeId ?? this.localNodeId();
6748
+ const hardware = await this.resolveProbeHardware(node);
6749
+ if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
6750
+ const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
6621
6751
  return {
6622
6752
  platform: process.platform,
6623
6753
  arch: process.arch,
6624
- hardware
6754
+ hardware: effective
6625
6755
  };
6626
6756
  }
6627
6757
  /**
6628
- * Fetch the probed hardware from the platform-probe cap. Returns null when
6629
- * the cap is not reachable (caller falls back to the registry's safe minimum).
6758
+ * Fetch the probed hardware from the platform-probe cap for `nodeId` (default
6759
+ * = self). Returns null when the cap is not reachable (caller falls back to
6760
+ * the registry's safe minimum).
6630
6761
  */
6631
- async resolveProbeHardware() {
6762
+ async resolveProbeHardware(nodeId) {
6632
6763
  try {
6633
6764
  const api = this.ctxIfReady?.api;
6634
6765
  if (!api) return null;
6635
- const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6766
+ const node = nodeId ?? this.localNodeId();
6767
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, require_dist.nodePin(node)))?.hardware;
6636
6768
  if (!hw) return null;
6637
6769
  return {
6638
6770
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -6643,6 +6775,61 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6643
6775
  }
6644
6776
  }
6645
6777
  /**
6778
+ * Bare node id used to scope the engine cascade in the shared store. MUST
6779
+ * match the provider's `localProbeNodeId()` (kernel.localNodeId, default
6780
+ * 'hub') so the UI write path (this) and the provider read/write path
6781
+ * (loadEngine / autoPick / reprobe) target the SAME `<key>@<nodeId>`. The old
6782
+ * `?? this.ctx.id` fallback resolved to the ADDON id ('detection-pipeline')
6783
+ * when kernel.localNodeId was absent, so UI saves landed on a key the
6784
+ * provider never read — silently losing the per-node selection.
6785
+ */
6786
+ localNodeId() {
6787
+ return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
6788
+ }
6789
+ /**
6790
+ * Refresh this node's cached engine selection from the node-scoped store.
6791
+ * `resolveBackendTuning` is synchronous and the reprobe gate runs before the
6792
+ * provider exists, so both read these cached fields instead of the
6793
+ * cluster-shared bare keys (which belong to no single node). Best-effort: a
6794
+ * transiently-unreadable store leaves the last cached values in place.
6795
+ */
6796
+ async refreshNodeEngineFromStore() {
6797
+ const store = await this.resolveUiSettingsStore();
6798
+ if (store === null) return;
6799
+ const node = this.localNodeId();
6800
+ const backend = readNodeEngineValue(store, "engineBackend", node);
6801
+ if (typeof backend === "string" && backend !== "") this.nodeEngineBackend = backend;
6802
+ const probed = readNodeEngineValue(store, "probedBestEngine", node);
6803
+ this.nodeProbedBestEngine = typeof probed === "string" ? probed : "";
6804
+ }
6805
+ /**
6806
+ * Persist a settings patch, mirroring the engine cascade fields to the TARGET
6807
+ * node's scoped keys so each node keeps an INDEPENDENT engine selection in the
6808
+ * cluster-central store. The hub addon serves writes for every node, so it
6809
+ * scopes by the requested `nodeId` (default self).
6810
+ *
6811
+ * When the target IS this node, `super.updateGlobalSettings` drives the normal
6812
+ * apply path (`resolveConfig` / `onConfigChanged` / `requiresRestart` restart),
6813
+ * and the bare engine keys it writes are shadowed by the node-scoped keys on
6814
+ * read. When the target is a SIBLING node, we persist the scoped engine keys +
6815
+ * the non-engine bare keys but DON'T run this node's restart/reprovision — the
6816
+ * owning node applies its own engine selection on its next (re)start.
6817
+ */
6818
+ async updateGlobalSettings(patch, nodeId) {
6819
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6820
+ const patchRecord = patch;
6821
+ const scopedEngine = {};
6822
+ for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
6823
+ if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
6824
+ if (targetNode === this.localNodeId()) {
6825
+ await super.updateGlobalSettings(patch, nodeId);
6826
+ return;
6827
+ }
6828
+ const sharedPatch = {};
6829
+ for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
6830
+ if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
6831
+ }
6832
+ /**
6646
6833
  * Resolve the effective pool tuning for the configured backend.
6647
6834
  *
6648
6835
  * Reads the registry's `tuningFor(backend)` and ignores any persisted
@@ -6656,7 +6843,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6656
6843
  * a reason to disagree.
6657
6844
  */
6658
6845
  resolveBackendTuning() {
6659
- const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6846
+ const t = tuningFor(toRuntimeId(this.nodeEngineBackend ?? DEFAULT_CONFIG.engineBackend));
6660
6847
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6661
6848
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6662
6849
  return {
@@ -6675,6 +6862,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6675
6862
  relativePath: ""
6676
6863
  }).catch(() => "camstack-data/models");
6677
6864
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6865
+ await this.refreshNodeEngineFromStore();
6678
6866
  this.pythonAddonDir = resolveAddonPythonDir();
6679
6867
  const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6680
6868
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
@@ -6696,7 +6884,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6696
6884
  });
6697
6885
  await this.provider.init();
6698
6886
  await this.provider.setApi(this.ctx);
6699
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6887
+ if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6700
6888
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6701
6889
  });
6702
6890
  await this.provider.warmPool();
@@ -6748,8 +6936,8 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6748
6936
  /**
6749
6937
  * Proactively install the OpenVINO Python package when Intel hardware
6750
6938
  * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6751
- * provider is constructed, so the module is available when the platform
6752
- * probe runs its next `import openvino.runtime` check.
6939
+ * provider is constructed, so the module is RUNNABLE when the engine is
6940
+ * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
6753
6941
  *
6754
6942
  * Failure is non-fatal: a warning is logged and the addon continues with
6755
6943
  * the onnx-cpu baseline. The hardware query itself is also best-effort —
@@ -6818,6 +7006,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6818
7006
  * lifecycle (engineFactory rebuild on next runPipeline).
6819
7007
  */
6820
7008
  async onConfigChanged() {
7009
+ await this.refreshNodeEngineFromStore();
6821
7010
  if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6822
7011
  this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6823
7012
  });