@camstack/addon-pipeline 1.0.7 → 1.1.0

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 (34) hide show
  1. package/dist/audio-analyzer/index.js +7 -8
  2. package/dist/audio-analyzer/index.mjs +3 -4
  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 +862 -696
  8. package/dist/detection-pipeline/index.mjs +850 -684
  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/{model-download-service-C7AjBsX9-rXY-VFDk.js → model-download-service-RxAOiYvX-C8rTRJy_.js} +36 -6
  12. package/dist/{model-download-service-C7AjBsX9-B0ekM6dF.mjs → model-download-service-RxAOiYvX-CMAvhgO7.mjs} +36 -6
  13. package/dist/motion-wasm/index.js +1 -1
  14. package/dist/motion-wasm/index.mjs +1 -1
  15. package/dist/pipeline-runner/index.js +1 -1
  16. package/dist/pipeline-runner/index.mjs +1 -1
  17. package/dist/recorder/index.js +6 -6
  18. package/dist/recorder/index.mjs +4 -4
  19. package/dist/stream-broker/_stub.js +1 -1
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Tbqpu0v3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DrohyZ5L.mjs} +3 -3
  21. 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
  22. package/dist/stream-broker/{hostInit-tIev5Gd9.mjs → hostInit-zLZbYJcg.mjs} +3 -3
  23. package/dist/stream-broker/index.js +26 -26
  24. package/dist/stream-broker/index.mjs +20 -20
  25. package/dist/stream-broker/remoteEntry.js +1 -1
  26. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C0kKwNX_.js → MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js} +1 -1
  27. package/embed-dist/assets/MotionZonesSettings-NcxxQN8r-CQzEnQoq.js +1 -0
  28. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C2SRtNe6.js → PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js} +1 -1
  29. package/embed-dist/assets/{index-B2LRyXWh.js → index-CSuLwWK-.js} +3 -3
  30. package/embed-dist/index.html +1 -1
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +65 -6
  33. 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
  34. package/embed-dist/assets/MotionZonesSettings-C1EEbk2V-CYtJc892.js +0 -1
@@ -2,17 +2,261 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_model_download_service_C7AjBsX9 = require("../model-download-service-C7AjBsX9-rXY-VFDk.js");
6
- const require_dist = require("../dist-G45MVm6i.js");
5
+ const require_model_download_service_RxAOiYvX = require("../model-download-service-RxAOiYvX-C8rTRJy_.js");
6
+ const require_dist = require("../dist-BLcTVvol.js");
7
7
  let node_fs = require("node:fs");
8
- node_fs = require_model_download_service_C7AjBsX9.__toESM(node_fs);
8
+ node_fs = require_model_download_service_RxAOiYvX.__toESM(node_fs);
9
9
  let node_path = require("node:path");
10
- node_path = require_model_download_service_C7AjBsX9.__toESM(node_path);
10
+ node_path = require_model_download_service_RxAOiYvX.__toESM(node_path);
11
11
  let node_os = require("node:os");
12
- node_os = require_model_download_service_C7AjBsX9.__toESM(node_os);
12
+ node_os = require_model_download_service_RxAOiYvX.__toESM(node_os);
13
13
  let node_child_process = require("node:child_process");
14
14
  let sharp = require("sharp");
15
- sharp = require_model_download_service_C7AjBsX9.__toESM(sharp);
15
+ sharp = require_model_download_service_RxAOiYvX.__toESM(sharp);
16
+ //#region src/detection-pipeline/engine-store-keys.ts
17
+ /**
18
+ * Per-node scoping for the detection-pipeline engine cascade.
19
+ *
20
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
21
+ * settings-store cap is hub-resident; every node's detection instance reads and
22
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
23
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
24
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
25
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
26
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
27
+ * another node that has no NPU.
28
+ *
29
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
30
+ * Everything else in the store stays shared. A legacy un-scoped value (written
31
+ * before this change, or by an older build) is read as a migration fallback and
32
+ * re-persisted under the node-scoped key on the next write.
33
+ */
34
+ var ENGINE_CASCADE_KEYS = [
35
+ "engineBackend",
36
+ "engineDevice",
37
+ "probedBestEngine"
38
+ ];
39
+ function isEngineCascadeKey(key) {
40
+ return ENGINE_CASCADE_KEYS.includes(key);
41
+ }
42
+ /**
43
+ * Normalise a raw kernel node id to the bare node id used for scoping.
44
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
45
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
46
+ */
47
+ function normalizeEngineNodeId(rawNodeId) {
48
+ const raw = rawNodeId ?? "hub";
49
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
50
+ }
51
+ /** The node-scoped store key for an engine cascade field. */
52
+ function nodeEngineKey(base, nodeId) {
53
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
54
+ }
55
+ /**
56
+ * Read an engine cascade value for a node: the node-scoped key if present,
57
+ * otherwise the legacy un-scoped value (migration), otherwise undefined.
58
+ */
59
+ function readNodeEngineValue(store, base, nodeId) {
60
+ const scoped = store[nodeEngineKey(base, nodeId)];
61
+ return scoped !== void 0 ? scoped : store[base];
62
+ }
63
+ /**
64
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
65
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
66
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
67
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
68
+ * node's form).
69
+ */
70
+ function projectNodeEngine(store, nodeId) {
71
+ const out = {};
72
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
73
+ for (const key of Object.keys(store)) {
74
+ const atIdx = key.indexOf("@");
75
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
76
+ scopedForAnyNode.add(key);
77
+ continue;
78
+ }
79
+ out[key] = store[key];
80
+ }
81
+ for (const base of ENGINE_CASCADE_KEYS) {
82
+ const value = readNodeEngineValue(store, base, nodeId);
83
+ if (value !== void 0) out[base] = value;
84
+ }
85
+ return out;
86
+ }
87
+ //#endregion
88
+ //#region src/detection-pipeline/runtimes.ts
89
+ var KNOWN_PLATFORMS = [
90
+ "darwin",
91
+ "linux",
92
+ "win32"
93
+ ];
94
+ var KNOWN_ARCHES = ["arm64", "x64"];
95
+ var KNOWN_GPU_TYPES = [
96
+ "nvidia",
97
+ "amd",
98
+ "intel",
99
+ "apple"
100
+ ];
101
+ var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
102
+ function toKnownPlatform(p) {
103
+ return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
104
+ }
105
+ function toKnownArch(a) {
106
+ return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
107
+ }
108
+ function gpuInfoFrom(hw) {
109
+ if (!hw.gpu) return null;
110
+ const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
111
+ if (!type) return null;
112
+ return {
113
+ type,
114
+ name: ""
115
+ };
116
+ }
117
+ function npuInfoFrom(hw) {
118
+ if (!hw.npu) return null;
119
+ const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
120
+ if (!type) return null;
121
+ return { type };
122
+ }
123
+ function envToHardwareInfo(env) {
124
+ if (!env.hardware) return null;
125
+ return {
126
+ platform: toKnownPlatform(env.platform),
127
+ arch: toKnownArch(env.arch),
128
+ cpuModel: "",
129
+ cpuCores: 0,
130
+ totalRAM_MB: 0,
131
+ availableRAM_MB: 0,
132
+ gpu: gpuInfoFrom(env.hardware),
133
+ npu: npuInfoFrom(env.hardware)
134
+ };
135
+ }
136
+ function probedToHardwareInfo(hw) {
137
+ if (!hw) return null;
138
+ return {
139
+ platform: toKnownPlatform(process.platform),
140
+ arch: toKnownArch(process.arch),
141
+ cpuModel: "",
142
+ cpuCores: 0,
143
+ totalRAM_MB: 0,
144
+ availableRAM_MB: 0,
145
+ gpu: gpuInfoFrom(hw),
146
+ npu: npuInfoFrom(hw)
147
+ };
148
+ }
149
+ var RUNTIME_DETAIL = {
150
+ onnx: {
151
+ label: "ONNX Runtime",
152
+ pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
153
+ tuning: {
154
+ concurrency: 4,
155
+ batchMode: "list",
156
+ maxBatchSize: 8,
157
+ intraOpThreads: 0
158
+ }
159
+ },
160
+ openvino: {
161
+ label: "OpenVINO",
162
+ pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
163
+ tuning: {
164
+ concurrency: 1,
165
+ batchMode: "none",
166
+ numStreams: 0
167
+ }
168
+ },
169
+ coreml: {
170
+ label: "CoreML",
171
+ pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
172
+ tuning: {
173
+ concurrency: 1,
174
+ batchMode: "none",
175
+ windowMs: 8,
176
+ maxBatchSize: 8,
177
+ numWorkers: 1
178
+ }
179
+ }
180
+ };
181
+ /**
182
+ * Returns the list of supported runtime IDs for the given hardware env.
183
+ * Delegates to `@camstack/types` `supportedRuntimes`.
184
+ */
185
+ function supportedRuntimes(env) {
186
+ return require_dist.supportedRuntimes(envToHardwareInfo(env));
187
+ }
188
+ /**
189
+ * Returns the device options for a given runtime and probed hardware.
190
+ * Delegates to `@camstack/types` `runtimeDevices`.
191
+ */
192
+ function runtimeDevices(id, hardware) {
193
+ return require_dist.runtimeDevices(id, probedToHardwareInfo(hardware));
194
+ }
195
+ /**
196
+ * Returns the default device string for a given runtime.
197
+ * Delegates to `@camstack/types` `defaultDeviceFor`.
198
+ */
199
+ function defaultDeviceFor(id) {
200
+ return require_dist.defaultDeviceFor(id);
201
+ }
202
+ /** Model format for each inference runtime supported by the detection pipeline. */
203
+ var RUNTIME_FORMAT = {
204
+ onnx: "onnx",
205
+ openvino: "openvino",
206
+ coreml: "coreml"
207
+ };
208
+ /**
209
+ * Returns the model format required for a given runtime.
210
+ * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
211
+ * matching the engine-provisioner's own ModelFormat type.
212
+ */
213
+ function modelFormatFor(id) {
214
+ return RUNTIME_FORMAT[id];
215
+ }
216
+ function pythonRequirementsFor(id) {
217
+ return RUNTIME_DETAIL[id].pythonRequirements;
218
+ }
219
+ function tuningFor(id) {
220
+ return RUNTIME_DETAIL[id].tuning;
221
+ }
222
+ function runtimeLabel(id) {
223
+ return RUNTIME_DETAIL[id].label;
224
+ }
225
+ /**
226
+ * Proactive-install hint kept for back-compat (re-exported from index.ts).
227
+ * Intel on Linux warrants installing openvino; coremltools covers macOS;
228
+ * openvino has no AMD/NVIDIA backend.
229
+ */
230
+ function shouldInstallOpenvino(env) {
231
+ if (env.platform === "darwin") return false;
232
+ return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
233
+ }
234
+ //#endregion
235
+ //#region src/detection-pipeline/auto-pick.ts
236
+ var PREFERENCE = [
237
+ "coreml",
238
+ "openvino",
239
+ "onnx"
240
+ ];
241
+ /**
242
+ * Pure function — picks the best supported runtime for the given hardware env.
243
+ *
244
+ * Logic:
245
+ * 1. If `bestBackendHint` is in the supported set, use it.
246
+ * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
247
+ * 3. Floor to `'onnx'` (always supported).
248
+ *
249
+ * Device is always `defaultDeviceFor(chosen)`.
250
+ */
251
+ function pickBestRuntime(env, bestBackendHint) {
252
+ const supported = supportedRuntimes(env);
253
+ const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
254
+ return {
255
+ runtimeId: chosen,
256
+ device: defaultDeviceFor(chosen)
257
+ };
258
+ }
259
+ //#endregion
16
260
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
17
261
  var MSG_COMMAND = 0;
18
262
  var MSG_INFER_JPEG = 1;
@@ -779,6 +1023,26 @@ var HF_REPO = "camstack/camstack-models";
779
1023
  var HF_SCRYPTED = "scrypted/plugin-models";
780
1024
  var hf = (path) => require_dist.hfModelUrl(HF_REPO, path);
781
1025
  var hfScrypted = (path) => require_dist.hfModelUrl(HF_SCRYPTED, path);
1026
+ /**
1027
+ * Build an OpenVINO format entry (always python runtime).
1028
+ *
1029
+ * OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
1030
+ * weights file with the same basename. We declare the `.bin` in `files` so
1031
+ * the (format-agnostic) downloader fetches it alongside the `.xml` — without
1032
+ * the weights, OpenVINO compile fails with "Empty weights data in bin file".
1033
+ * A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
1034
+ * sibling, so none is added.
1035
+ */
1036
+ var ovFormat = (url, sizeMB) => {
1037
+ const base = url.split("/").pop() ?? "";
1038
+ const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
1039
+ return {
1040
+ url,
1041
+ sizeMB,
1042
+ runtimes: ["python"],
1043
+ ...files ? { files } : {}
1044
+ };
1045
+ };
782
1046
  var MLPACKAGE_FILES = [
783
1047
  "Manifest.json",
784
1048
  "Data/com.apple.CoreML/model.mlmodel",
@@ -807,11 +1071,7 @@ var OBJECT_DETECTION_MODELS = [
807
1071
  files: [...MLPACKAGE_FILES],
808
1072
  runtimes: ["python"]
809
1073
  },
810
- openvino: {
811
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"),
812
- sizeMB: 6,
813
- runtimes: ["python"]
814
- }
1074
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
815
1075
  }
816
1076
  },
817
1077
  {
@@ -836,11 +1096,7 @@ var OBJECT_DETECTION_MODELS = [
836
1096
  files: [...MLPACKAGE_FILES],
837
1097
  runtimes: ["python"]
838
1098
  },
839
- openvino: {
840
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"),
841
- sizeMB: 16,
842
- runtimes: ["python"]
843
- }
1099
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
844
1100
  }
845
1101
  },
846
1102
  {
@@ -865,11 +1121,7 @@ var OBJECT_DETECTION_MODELS = [
865
1121
  files: [...MLPACKAGE_FILES],
866
1122
  runtimes: ["python"]
867
1123
  },
868
- openvino: {
869
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"),
870
- sizeMB: 49,
871
- runtimes: ["python"]
872
- }
1124
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
873
1125
  }
874
1126
  },
875
1127
  {
@@ -894,11 +1146,7 @@ var OBJECT_DETECTION_MODELS = [
894
1146
  files: [...MLPACKAGE_FILES],
895
1147
  runtimes: ["python"]
896
1148
  },
897
- openvino: {
898
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"),
899
- sizeMB: 9,
900
- runtimes: ["python"]
901
- }
1149
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
902
1150
  }
903
1151
  },
904
1152
  {
@@ -923,11 +1171,7 @@ var OBJECT_DETECTION_MODELS = [
923
1171
  files: [...MLPACKAGE_FILES],
924
1172
  runtimes: ["python"]
925
1173
  },
926
- openvino: {
927
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"),
928
- sizeMB: 36,
929
- runtimes: ["python"]
930
- }
1174
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
931
1175
  }
932
1176
  },
933
1177
  {
@@ -952,11 +1196,7 @@ var OBJECT_DETECTION_MODELS = [
952
1196
  files: [...MLPACKAGE_FILES],
953
1197
  runtimes: ["python"]
954
1198
  },
955
- openvino: {
956
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"),
957
- sizeMB: 78,
958
- runtimes: ["python"]
959
- }
1199
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
960
1200
  }
961
1201
  },
962
1202
  {
@@ -981,11 +1221,7 @@ var OBJECT_DETECTION_MODELS = [
981
1221
  files: [...MLPACKAGE_FILES],
982
1222
  runtimes: ["python"]
983
1223
  },
984
- openvino: {
985
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"),
986
- sizeMB: 95,
987
- runtimes: ["python"]
988
- }
1224
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
989
1225
  }
990
1226
  },
991
1227
  {
@@ -1010,11 +1246,7 @@ var OBJECT_DETECTION_MODELS = [
1010
1246
  files: [...MLPACKAGE_FILES],
1011
1247
  runtimes: ["python"]
1012
1248
  },
1013
- openvino: {
1014
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"),
1015
- sizeMB: 213,
1016
- runtimes: ["python"]
1017
- }
1249
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
1018
1250
  }
1019
1251
  },
1020
1252
  {
@@ -1039,11 +1271,7 @@ var OBJECT_DETECTION_MODELS = [
1039
1271
  files: [...MLPACKAGE_FILES],
1040
1272
  runtimes: ["python"]
1041
1273
  },
1042
- openvino: {
1043
- url: hfScrypted("openvino/scrypted_yolov9t_relu/best.xml"),
1044
- sizeMB: 6,
1045
- runtimes: ["python"]
1046
- }
1274
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
1047
1275
  }
1048
1276
  },
1049
1277
  {
@@ -1068,11 +1296,7 @@ var OBJECT_DETECTION_MODELS = [
1068
1296
  files: [...MLPACKAGE_FILES],
1069
1297
  runtimes: ["python"]
1070
1298
  },
1071
- openvino: {
1072
- url: hfScrypted("openvino/scrypted_yolov9s_relu/best.xml"),
1073
- sizeMB: 16,
1074
- runtimes: ["python"]
1075
- }
1299
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
1076
1300
  }
1077
1301
  },
1078
1302
  {
@@ -1097,11 +1321,7 @@ var OBJECT_DETECTION_MODELS = [
1097
1321
  files: [...MLPACKAGE_FILES],
1098
1322
  runtimes: ["python"]
1099
1323
  },
1100
- openvino: {
1101
- url: hfScrypted("openvino/scrypted_yolov9c_relu/best.xml"),
1102
- sizeMB: 49,
1103
- runtimes: ["python"]
1104
- }
1324
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
1105
1325
  }
1106
1326
  },
1107
1327
  {
@@ -1126,11 +1346,7 @@ var OBJECT_DETECTION_MODELS = [
1126
1346
  files: [...MLPACKAGE_FILES],
1127
1347
  runtimes: ["python"]
1128
1348
  },
1129
- openvino: {
1130
- url: hfScrypted("openvino/scrypted_yolov9m_relu/best.xml"),
1131
- sizeMB: 38,
1132
- runtimes: ["python"]
1133
- }
1349
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
1134
1350
  }
1135
1351
  }
1136
1352
  ];
@@ -1159,11 +1375,7 @@ var FACE_DETECTION_MODELS = [{
1159
1375
  files: [...MLPACKAGE_FILES],
1160
1376
  runtimes: ["python"]
1161
1377
  },
1162
- openvino: {
1163
- url: hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"),
1164
- sizeMB: 1.8,
1165
- runtimes: ["python"]
1166
- }
1378
+ openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
1167
1379
  }
1168
1380
  }, {
1169
1381
  id: "scrypted-yolov9t-face",
@@ -1190,11 +1402,7 @@ var FACE_DETECTION_MODELS = [{
1190
1402
  files: [...MLPACKAGE_FILES],
1191
1403
  runtimes: ["python"]
1192
1404
  },
1193
- openvino: {
1194
- url: hfScrypted("openvino/scrypted_yolov9t_relu_face/best.xml"),
1195
- sizeMB: 6,
1196
- runtimes: ["python"]
1197
- }
1405
+ openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
1198
1406
  }
1199
1407
  }];
1200
1408
  var FACE_EMBEDDING_MODELS = [{
@@ -1224,11 +1432,7 @@ var FACE_EMBEDDING_MODELS = [{
1224
1432
  files: [...MLPACKAGE_FILES],
1225
1433
  runtimes: ["python"]
1226
1434
  },
1227
- openvino: {
1228
- url: hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"),
1229
- sizeMB: 65,
1230
- runtimes: ["python"]
1231
- }
1435
+ openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
1232
1436
  }
1233
1437
  }, {
1234
1438
  id: "inception-resnet-v1",
@@ -1255,11 +1459,7 @@ var FACE_EMBEDDING_MODELS = [{
1255
1459
  files: [...MLPACKAGE_FILES],
1256
1460
  runtimes: ["python"]
1257
1461
  },
1258
- openvino: {
1259
- url: hfScrypted("openvino/inception_resnet_v1/best.xml"),
1260
- sizeMB: 45,
1261
- runtimes: ["python"]
1262
- }
1462
+ openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
1263
1463
  }
1264
1464
  }];
1265
1465
  var PLATE_DETECTION_MODELS = [{
@@ -1287,11 +1487,7 @@ var PLATE_DETECTION_MODELS = [{
1287
1487
  files: [...MLPACKAGE_FILES],
1288
1488
  runtimes: ["python"]
1289
1489
  },
1290
- openvino: {
1291
- url: hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"),
1292
- sizeMB: 6.1,
1293
- runtimes: ["python"]
1294
- }
1490
+ openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
1295
1491
  }
1296
1492
  }];
1297
1493
  var PLATE_OCR_MODELS = [{
@@ -1319,11 +1515,7 @@ var PLATE_OCR_MODELS = [{
1319
1515
  files: [...MLPACKAGE_FILES],
1320
1516
  runtimes: ["python"]
1321
1517
  },
1322
- openvino: {
1323
- url: hfScrypted("openvino/vgg_english_g2/best.xml"),
1324
- sizeMB: 7.2,
1325
- runtimes: ["python"]
1326
- }
1518
+ openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
1327
1519
  }
1328
1520
  }];
1329
1521
  var ANIMAL_CLASSIFIER_MODELS = [{
@@ -1352,11 +1544,7 @@ var ANIMAL_CLASSIFIER_MODELS = [{
1352
1544
  files: [...MLPACKAGE_FILES],
1353
1545
  runtimes: ["python"]
1354
1546
  },
1355
- openvino: {
1356
- url: hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"),
1357
- sizeMB: 164,
1358
- runtimes: ["python"]
1359
- }
1547
+ openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
1360
1548
  }
1361
1549
  }];
1362
1550
  var BIRD_CLASSIFIER_MODELS = [{
@@ -1385,11 +1573,7 @@ var BIRD_CLASSIFIER_MODELS = [{
1385
1573
  files: [...MLPACKAGE_FILES],
1386
1574
  runtimes: ["python"]
1387
1575
  },
1388
- openvino: {
1389
- url: hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"),
1390
- sizeMB: 47,
1391
- runtimes: ["python"]
1392
- }
1576
+ openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
1393
1577
  },
1394
1578
  extraFiles: [{
1395
1579
  url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
@@ -1423,11 +1607,7 @@ var VEHICLE_CLASSIFIER_MODELS = [{
1423
1607
  files: [...MLPACKAGE_FILES],
1424
1608
  runtimes: ["python"]
1425
1609
  },
1426
- openvino: {
1427
- url: hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"),
1428
- sizeMB: 68,
1429
- runtimes: ["python"]
1430
- }
1610
+ openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
1431
1611
  },
1432
1612
  extraFiles: [{
1433
1613
  url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
@@ -1460,11 +1640,7 @@ var SEGMENTATION_REFINER_MODELS = [{
1460
1640
  files: [...MLPACKAGE_FILES],
1461
1641
  runtimes: ["python"]
1462
1642
  },
1463
- openvino: {
1464
- url: hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"),
1465
- sizeMB: 2.5,
1466
- runtimes: ["python"]
1467
- }
1643
+ openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
1468
1644
  }
1469
1645
  }];
1470
1646
  var INSTANCE_SEGMENTATION_MODELS = [
@@ -1490,11 +1666,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1490
1666
  files: [...MLPACKAGE_FILES],
1491
1667
  runtimes: ["python"]
1492
1668
  },
1493
- openvino: {
1494
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"),
1495
- sizeMB: 11,
1496
- runtimes: ["python"]
1497
- }
1669
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
1498
1670
  }
1499
1671
  },
1500
1672
  {
@@ -1519,11 +1691,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1519
1691
  files: [...MLPACKAGE_FILES],
1520
1692
  runtimes: ["python"]
1521
1693
  },
1522
- openvino: {
1523
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"),
1524
- sizeMB: 40,
1525
- runtimes: ["python"]
1526
- }
1694
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
1527
1695
  }
1528
1696
  },
1529
1697
  {
@@ -1548,11 +1716,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1548
1716
  files: [...MLPACKAGE_FILES],
1549
1717
  runtimes: ["python"]
1550
1718
  },
1551
- openvino: {
1552
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"),
1553
- sizeMB: 90,
1554
- runtimes: ["python"]
1555
- }
1719
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
1556
1720
  }
1557
1721
  }
1558
1722
  ];
@@ -1568,16 +1732,12 @@ var AUDIO_CLASSIFIER_MODELS = [{
1568
1732
  preprocessMode: "resize",
1569
1733
  formats: {
1570
1734
  onnx: {
1571
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1735
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1572
1736
  sizeMB: 3.2
1573
1737
  },
1574
- openvino: {
1575
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1576
- sizeMB: 3.2,
1577
- runtimes: ["python"]
1578
- },
1738
+ openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
1579
1739
  coreml: {
1580
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1740
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1581
1741
  sizeMB: 3.2,
1582
1742
  runtimes: ["python"]
1583
1743
  }
@@ -2165,71 +2325,174 @@ var EngineFactory = class {
2165
2325
  }
2166
2326
  };
2167
2327
  //#endregion
2168
- //#region src/detection-pipeline/postprocess/dispatch.ts
2169
- var VALID_KINDS = new Set([
2170
- "detections",
2171
- "classifications",
2172
- "embedding",
2173
- "text",
2174
- "mask"
2175
- ]);
2176
- /**
2177
- * Type guard: validates that a structured payload from the Python pool is a
2178
- * well-formed StepOutput discriminated union.
2179
- *
2180
- * The Python inference_pool.py always sets `output.structured` with a `kind`
2181
- * field. This guard narrows `Record<string, unknown>` to `StepOutput` without
2182
- * resorting to double-cast.
2183
- */
2184
- function isStepOutput(value) {
2185
- return typeof value === "object" && value !== null && typeof value["kind"] === "string" && VALID_KINDS.has(value["kind"]);
2186
- }
2187
- /**
2188
- * Return the structured payload as `StepOutput`, or null if it fails validation.
2189
- * Callers must handle null by proceeding to raw-tensor postprocessing.
2190
- */
2191
- function tryStructured(output) {
2192
- if (!output.structured) return null;
2193
- if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
2194
- return output.structured;
2195
- }
2196
- function postprocessYolo(output, stepDef) {
2197
- const structured = tryStructured(output);
2198
- if (structured) return structured;
2199
- const tensor = output.tensor;
2200
- if (!tensor) throw new Error("YOLO postprocessor: no tensor in engine output");
2201
- const labels = stepDef.labels ?? [];
2202
- const numClasses = labels.length || 80;
2203
- const numBoxes = tensor.length / (4 + numClasses);
2204
- const letterbox = output.letterbox;
2205
- const dets = [];
2206
- for (let i = 0; i < numBoxes; i++) {
2207
- const cx = tensor[0 * numBoxes + i];
2208
- const cy = tensor[1 * numBoxes + i];
2209
- const w = tensor[2 * numBoxes + i];
2210
- const h = tensor[3 * numBoxes + i];
2211
- let bestScore = -Infinity;
2212
- let bestClass = 0;
2213
- for (let j = 0; j < numClasses; j++) {
2214
- const score = tensor[(4 + j) * numBoxes + i];
2215
- if (score > bestScore) {
2216
- bestScore = score;
2217
- bestClass = j;
2218
- }
2219
- }
2220
- if (bestScore <= 0) continue;
2221
- let x1 = cx - w / 2;
2222
- let y1 = cy - h / 2;
2223
- let x2 = cx + w / 2;
2224
- let y2 = cy + h / 2;
2225
- if (letterbox) {
2226
- x1 = (x1 - letterbox.padX) / letterbox.scale;
2227
- y1 = (y1 - letterbox.padY) / letterbox.scale;
2228
- x2 = (x2 - letterbox.padX) / letterbox.scale;
2229
- y2 = (y2 - letterbox.padY) / letterbox.scale;
2230
- }
2231
- const label = labels[bestClass] ?? String(bestClass);
2232
- dets.push({
2328
+ //#region src/detection-pipeline/engine-provisioner.ts
2329
+ /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
2330
+ var BACKOFF_SCHEDULE_MS = [
2331
+ 5e3,
2332
+ 15e3,
2333
+ 3e4,
2334
+ 6e4,
2335
+ 12e4,
2336
+ 3e5
2337
+ ];
2338
+ var IDLE_STATE = {
2339
+ runtimeId: null,
2340
+ device: null,
2341
+ state: "idle"
2342
+ };
2343
+ var EngineProvisioner = class {
2344
+ fx;
2345
+ current = IDLE_STATE;
2346
+ /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
2347
+ generation = 0;
2348
+ cancelTimer = null;
2349
+ retryIndex = 0;
2350
+ constructor(fx) {
2351
+ this.fx = fx;
2352
+ }
2353
+ get state() {
2354
+ return this.current;
2355
+ }
2356
+ isReady() {
2357
+ return this.current.state === "ready";
2358
+ }
2359
+ select(runtimeId, device) {
2360
+ this.generation++;
2361
+ this.retryIndex = 0;
2362
+ this.clearTimer();
2363
+ this.transition({
2364
+ runtimeId,
2365
+ device,
2366
+ state: "installing",
2367
+ progress: 0
2368
+ });
2369
+ this.provision(this.generation, runtimeId, device);
2370
+ }
2371
+ dispose() {
2372
+ this.generation++;
2373
+ this.clearTimer();
2374
+ }
2375
+ clearTimer() {
2376
+ if (this.cancelTimer !== null) {
2377
+ this.cancelTimer();
2378
+ this.cancelTimer = null;
2379
+ }
2380
+ }
2381
+ transition(next) {
2382
+ this.current = next;
2383
+ this.fx.onChange(next);
2384
+ }
2385
+ async provision(gen, runtimeId, device) {
2386
+ try {
2387
+ await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
2388
+ if (gen !== this.generation) return;
2389
+ this.transition({
2390
+ runtimeId,
2391
+ device,
2392
+ state: "verifying",
2393
+ progress: 100
2394
+ });
2395
+ await this.fx.verify(runtimeId, device);
2396
+ if (gen !== this.generation) return;
2397
+ this.retryIndex = 0;
2398
+ this.transition({
2399
+ runtimeId,
2400
+ device,
2401
+ state: "ready"
2402
+ });
2403
+ } catch (err) {
2404
+ if (gen !== this.generation) return;
2405
+ const message = err instanceof Error ? err.message : String(err);
2406
+ const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
2407
+ this.retryIndex++;
2408
+ const nextRetryAt = this.fx.now() + delay;
2409
+ this.transition({
2410
+ runtimeId,
2411
+ device,
2412
+ state: "failed",
2413
+ error: message,
2414
+ nextRetryAt
2415
+ });
2416
+ this.cancelTimer = this.fx.setTimer(delay, () => {
2417
+ if (gen !== this.generation) return;
2418
+ this.cancelTimer = null;
2419
+ this.transition({
2420
+ runtimeId,
2421
+ device,
2422
+ state: "installing",
2423
+ progress: 0
2424
+ });
2425
+ this.provision(gen, runtimeId, device);
2426
+ });
2427
+ }
2428
+ }
2429
+ };
2430
+ //#endregion
2431
+ //#region src/detection-pipeline/postprocess/dispatch.ts
2432
+ var VALID_KINDS = new Set([
2433
+ "detections",
2434
+ "classifications",
2435
+ "embedding",
2436
+ "text",
2437
+ "mask"
2438
+ ]);
2439
+ /**
2440
+ * Type guard: validates that a structured payload from the Python pool is a
2441
+ * well-formed StepOutput discriminated union.
2442
+ *
2443
+ * The Python inference_pool.py always sets `output.structured` with a `kind`
2444
+ * field. This guard narrows `Record<string, unknown>` to `StepOutput` without
2445
+ * resorting to double-cast.
2446
+ */
2447
+ function isStepOutput(value) {
2448
+ return typeof value === "object" && value !== null && typeof value["kind"] === "string" && VALID_KINDS.has(value["kind"]);
2449
+ }
2450
+ /**
2451
+ * Return the structured payload as `StepOutput`, or null if it fails validation.
2452
+ * Callers must handle null by proceeding to raw-tensor postprocessing.
2453
+ */
2454
+ function tryStructured(output) {
2455
+ if (!output.structured) return null;
2456
+ if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
2457
+ return output.structured;
2458
+ }
2459
+ function postprocessYolo(output, stepDef) {
2460
+ const structured = tryStructured(output);
2461
+ if (structured) return structured;
2462
+ const tensor = output.tensor;
2463
+ if (!tensor) throw new Error("YOLO postprocessor: no tensor in engine output");
2464
+ const labels = stepDef.labels ?? [];
2465
+ const numClasses = labels.length || 80;
2466
+ const numBoxes = tensor.length / (4 + numClasses);
2467
+ const letterbox = output.letterbox;
2468
+ const dets = [];
2469
+ for (let i = 0; i < numBoxes; i++) {
2470
+ const cx = tensor[i];
2471
+ const cy = tensor[1 * numBoxes + i];
2472
+ const w = tensor[2 * numBoxes + i];
2473
+ const h = tensor[3 * numBoxes + i];
2474
+ let bestScore = -Infinity;
2475
+ let bestClass = 0;
2476
+ for (let j = 0; j < numClasses; j++) {
2477
+ const score = tensor[(4 + j) * numBoxes + i];
2478
+ if (score > bestScore) {
2479
+ bestScore = score;
2480
+ bestClass = j;
2481
+ }
2482
+ }
2483
+ if (bestScore <= 0) continue;
2484
+ let x1 = cx - w / 2;
2485
+ let y1 = cy - h / 2;
2486
+ let x2 = cx + w / 2;
2487
+ let y2 = cy + h / 2;
2488
+ if (letterbox) {
2489
+ x1 = (x1 - letterbox.padX) / letterbox.scale;
2490
+ y1 = (y1 - letterbox.padY) / letterbox.scale;
2491
+ x2 = (x2 - letterbox.padX) / letterbox.scale;
2492
+ y2 = (y2 - letterbox.padY) / letterbox.scale;
2493
+ }
2494
+ const label = labels[bestClass] ?? String(bestClass);
2495
+ dets.push({
2233
2496
  class: label,
2234
2497
  score: bestScore,
2235
2498
  bbox: [
@@ -2360,7 +2623,7 @@ function postprocessArcface(output, _stepDef) {
2360
2623
  let sumSq = 0;
2361
2624
  for (let i = 0; i < tensor.length; i++) sumSq += tensor[i] * tensor[i];
2362
2625
  const norm = Math.sqrt(sumSq);
2363
- const normalized = new Array(tensor.length);
2626
+ const normalized = Array.from({ length: tensor.length });
2364
2627
  for (let i = 0; i < tensor.length; i++) normalized[i] = norm === 0 ? 0 : tensor[i] / norm;
2365
2628
  return {
2366
2629
  kind: "embedding",
@@ -3396,403 +3659,143 @@ var PipelineExecutor = class {
3396
3659
  return output;
3397
3660
  }
3398
3661
  async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
3399
- for (const child of children) {
3400
- if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
3401
- const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
3402
- if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
3403
- if (isBboxDegenerate(parentDetection.bbox)) continue;
3404
- try {
3405
- const childStart = Date.now();
3406
- const fullFrameJpeg = await fullFrameJpegProvider();
3407
- const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
3408
- let cropJpegBuf;
3409
- let cropW;
3410
- let cropH;
3411
- if (modelEntry?.faceAlignment) {
3412
- const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
3413
- if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
3414
- const lms = parentDetection.landmarks;
3415
- if (lms && lms.length >= 5) {
3416
- const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
3417
- cropJpegBuf = aligned.jpeg;
3418
- cropW = aligned.width;
3419
- cropH = aligned.height;
3420
- } else {
3421
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3422
- cropJpegBuf = crop.jpeg;
3423
- cropW = crop.width;
3424
- cropH = crop.height;
3425
- }
3426
- } else {
3427
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3428
- cropJpegBuf = crop.jpeg;
3429
- cropW = crop.width;
3430
- cropH = crop.height;
3431
- }
3432
- const childOutput = await this.executeStep(child, {
3433
- kind: "jpeg",
3434
- data: cropJpegBuf
3435
- }, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
3436
- const childMs = Date.now() - childStart;
3437
- const detailsBefore = ctx.details.length;
3438
- applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
3439
- if (childOutput.kind === "detections" && child.children.length > 0) {
3440
- const newDetails = ctx.details.slice(detailsBefore);
3441
- for (const detail of newDetails) {
3442
- detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
3443
- if (detail.landmarks) {
3444
- const [px1, py1] = parentDetection.bbox;
3445
- detail.landmarks = detail.landmarks.map((l) => ({
3446
- x: l.x + px1,
3447
- y: l.y + py1
3448
- }));
3449
- }
3450
- await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
3451
- }
3452
- }
3453
- } catch {}
3454
- }
3455
- }
3456
- matchesInputClasses(className, inputClasses) {
3457
- if (inputClasses.length === 0) return true;
3458
- return inputClasses.includes(className);
3459
- }
3460
- /**
3461
- * Apply the object-detection step's macro filter + per-macro
3462
- * minConfidence sliders introduced in the Phase 6 step rework.
3463
- *
3464
- * Expected `settings` shape:
3465
- * - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
3466
- * Empty array = all allowed (legacy behaviour).
3467
- * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
3468
- */
3469
- matchesMacroFilter(macroClass, score, settings) {
3470
- if (!settings) return true;
3471
- const enabled = settings["enabledMacroClasses"];
3472
- if (Array.isArray(enabled) && enabled.length > 0) {
3473
- if (!enabled.includes(macroClass)) return false;
3474
- }
3475
- const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3476
- if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3477
- return true;
3478
- }
3479
- };
3480
- function capitalize(s) {
3481
- if (s.length === 0) return s;
3482
- return s.charAt(0).toUpperCase() + s.slice(1);
3483
- }
3484
- //#endregion
3485
- //#region src/detection-pipeline/pipeline/tree-builder.ts
3486
- /**
3487
- * Build an executable tree from user config.
3488
- *
3489
- * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3490
- * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3491
- * Throws if step not loaded.
3492
- */
3493
- function buildExecutableTree(steps, getEngine) {
3494
- return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3495
- }
3496
- function buildNode(step, getEngine) {
3497
- const definition = getStepDefinition(step.addonId);
3498
- const engine = getEngine(step.addonId);
3499
- const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3500
- const mergedSettings = {
3501
- ...collectSchemaDefaults(step.addonId),
3502
- ...step.settings
3503
- };
3504
- return {
3505
- stepId: step.addonId,
3506
- definition,
3507
- engine,
3508
- modelId: step.modelId,
3509
- inputClasses: definition.inputClasses ?? [],
3510
- enabled: step.enabled,
3511
- children,
3512
- ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3513
- };
3514
- }
3515
- /**
3516
- * Walk the step's declared `getConfigSchema()` and collect every field
3517
- * with a primitive `default` value into a plain record. Group fields
3518
- * are flattened — a nested `{ type: 'group', fields: [...] }` is
3519
- * entered recursively. Fields without a declared default are skipped.
3520
- */
3521
- function collectSchemaDefaults(stepId) {
3522
- const schema = getStep(stepId).getConfigSchema();
3523
- const out = {};
3524
- walkFieldsForDefaults(schema, out);
3525
- return out;
3526
- }
3527
- function walkFieldsForDefaults(fields, out) {
3528
- for (const field of fields) {
3529
- if ("type" in field && field.type === "group" && "fields" in field) {
3530
- walkFieldsForDefaults(field.fields, out);
3531
- continue;
3532
- }
3533
- if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3534
- }
3535
- }
3536
- //#endregion
3537
- //#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",
3549
- 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
- pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3567
- tuning: {
3568
- concurrency: 4,
3569
- batchMode: "list",
3570
- maxBatchSize: 8,
3571
- intraOpThreads: 0
3572
- }
3573
- },
3574
- {
3575
- id: "openvino",
3576
- 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
- pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3594
- tuning: {
3595
- concurrency: 1,
3596
- batchMode: "none",
3597
- numStreams: 0
3598
- }
3599
- },
3600
- {
3601
- id: "coreml",
3602
- 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
- pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3624
- tuning: {
3625
- concurrency: 1,
3626
- batchMode: "none",
3627
- windowMs: 8,
3628
- maxBatchSize: 8,
3629
- numWorkers: 1
3630
- }
3631
- }
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
- }
3638
- function supportedRuntimes(env) {
3639
- return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3640
- }
3641
- function runtimeDevices(id, hardware) {
3642
- return def(id).devices(hardware);
3643
- }
3644
- function defaultDeviceFor(id) {
3645
- return def(id).defaultDevice;
3646
- }
3647
- function pythonRequirementsFor(id) {
3648
- return def(id).pythonRequirements;
3649
- }
3650
- function modelFormatFor(id) {
3651
- return def(id).modelFormat;
3652
- }
3653
- function tuningFor(id) {
3654
- return def(id).tuning;
3655
- }
3656
- function runtimeLabel(id) {
3657
- return def(id).label;
3658
- }
3659
- /**
3660
- * Proactive-install hint kept for back-compat (re-exported from index.ts).
3661
- * Intel on Linux warrants installing openvino; coremltools covers macOS;
3662
- * openvino has no AMD/NVIDIA backend.
3663
- */
3664
- function shouldInstallOpenvino(env) {
3665
- if (env.platform === "darwin") return false;
3666
- return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3667
- }
3668
- //#endregion
3669
- //#region src/detection-pipeline/engine-provisioner.ts
3670
- /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3671
- var BACKOFF_SCHEDULE_MS = [
3672
- 5e3,
3673
- 15e3,
3674
- 3e4,
3675
- 6e4,
3676
- 12e4,
3677
- 3e5
3678
- ];
3679
- var IDLE_STATE = {
3680
- runtimeId: null,
3681
- device: null,
3682
- state: "idle"
3683
- };
3684
- var EngineProvisioner = class {
3685
- fx;
3686
- current = IDLE_STATE;
3687
- /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
3688
- generation = 0;
3689
- cancelTimer = null;
3690
- retryIndex = 0;
3691
- constructor(fx) {
3692
- this.fx = fx;
3693
- }
3694
- get state() {
3695
- return this.current;
3696
- }
3697
- isReady() {
3698
- return this.current.state === "ready";
3699
- }
3700
- select(runtimeId, device) {
3701
- this.generation++;
3702
- this.retryIndex = 0;
3703
- this.clearTimer();
3704
- this.transition({
3705
- runtimeId,
3706
- device,
3707
- state: "installing",
3708
- progress: 0
3709
- });
3710
- this.provision(this.generation, runtimeId, device);
3711
- }
3712
- dispose() {
3713
- this.generation++;
3714
- this.clearTimer();
3715
- }
3716
- clearTimer() {
3717
- if (this.cancelTimer !== null) {
3718
- this.cancelTimer();
3719
- this.cancelTimer = null;
3662
+ for (const child of children) {
3663
+ if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
3664
+ const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
3665
+ if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
3666
+ if (isBboxDegenerate(parentDetection.bbox)) continue;
3667
+ try {
3668
+ const childStart = Date.now();
3669
+ const fullFrameJpeg = await fullFrameJpegProvider();
3670
+ const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
3671
+ let cropJpegBuf;
3672
+ let cropW;
3673
+ let cropH;
3674
+ if (modelEntry?.faceAlignment) {
3675
+ const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
3676
+ if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
3677
+ const lms = parentDetection.landmarks;
3678
+ if (lms && lms.length >= 5) {
3679
+ const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
3680
+ cropJpegBuf = aligned.jpeg;
3681
+ cropW = aligned.width;
3682
+ cropH = aligned.height;
3683
+ } else {
3684
+ const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3685
+ cropJpegBuf = crop.jpeg;
3686
+ cropW = crop.width;
3687
+ cropH = crop.height;
3688
+ }
3689
+ } else {
3690
+ const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3691
+ cropJpegBuf = crop.jpeg;
3692
+ cropW = crop.width;
3693
+ cropH = crop.height;
3694
+ }
3695
+ const childOutput = await this.executeStep(child, {
3696
+ kind: "jpeg",
3697
+ data: cropJpegBuf
3698
+ }, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
3699
+ const childMs = Date.now() - childStart;
3700
+ const detailsBefore = ctx.details.length;
3701
+ applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
3702
+ if (childOutput.kind === "detections" && child.children.length > 0) {
3703
+ const newDetails = ctx.details.slice(detailsBefore);
3704
+ for (const detail of newDetails) {
3705
+ detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
3706
+ if (detail.landmarks) {
3707
+ const [px1, py1] = parentDetection.bbox;
3708
+ detail.landmarks = detail.landmarks.map((l) => ({
3709
+ x: l.x + px1,
3710
+ y: l.y + py1
3711
+ }));
3712
+ }
3713
+ await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
3714
+ }
3715
+ }
3716
+ } catch {}
3720
3717
  }
3721
3718
  }
3722
- transition(next) {
3723
- this.current = next;
3724
- this.fx.onChange(next);
3719
+ matchesInputClasses(className, inputClasses) {
3720
+ if (inputClasses.length === 0) return true;
3721
+ return inputClasses.includes(className);
3725
3722
  }
3726
- async provision(gen, runtimeId, device) {
3727
- try {
3728
- await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
3729
- if (gen !== this.generation) return;
3730
- this.transition({
3731
- runtimeId,
3732
- device,
3733
- state: "verifying",
3734
- progress: 100
3735
- });
3736
- await this.fx.verify(runtimeId, device);
3737
- if (gen !== this.generation) return;
3738
- this.retryIndex = 0;
3739
- this.transition({
3740
- runtimeId,
3741
- device,
3742
- state: "ready"
3743
- });
3744
- } catch (err) {
3745
- if (gen !== this.generation) return;
3746
- const message = err instanceof Error ? err.message : String(err);
3747
- const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
3748
- this.retryIndex++;
3749
- const nextRetryAt = this.fx.now() + delay;
3750
- this.transition({
3751
- runtimeId,
3752
- device,
3753
- state: "failed",
3754
- error: message,
3755
- nextRetryAt
3756
- });
3757
- this.cancelTimer = this.fx.setTimer(delay, () => {
3758
- if (gen !== this.generation) return;
3759
- this.cancelTimer = null;
3760
- this.transition({
3761
- runtimeId,
3762
- device,
3763
- state: "installing",
3764
- progress: 0
3765
- });
3766
- this.provision(gen, runtimeId, device);
3767
- });
3723
+ /**
3724
+ * Apply the object-detection step's macro filter + per-macro
3725
+ * minConfidence sliders introduced in the Phase 6 step rework.
3726
+ *
3727
+ * Expected `settings` shape:
3728
+ * - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
3729
+ * Empty array = all allowed (legacy behaviour).
3730
+ * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
3731
+ */
3732
+ matchesMacroFilter(macroClass, score, settings) {
3733
+ if (!settings) return true;
3734
+ const enabled = settings["enabledMacroClasses"];
3735
+ if (Array.isArray(enabled) && enabled.length > 0) {
3736
+ if (!enabled.includes(macroClass)) return false;
3768
3737
  }
3738
+ const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3739
+ if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3740
+ return true;
3769
3741
  }
3770
3742
  };
3743
+ function capitalize(s) {
3744
+ if (s.length === 0) return s;
3745
+ return s.charAt(0).toUpperCase() + s.slice(1);
3746
+ }
3771
3747
  //#endregion
3772
- //#region src/detection-pipeline/auto-pick.ts
3773
- var PREFERENCE = [
3774
- "coreml",
3775
- "openvino",
3776
- "onnx"
3777
- ];
3748
+ //#region src/detection-pipeline/pipeline/tree-builder.ts
3778
3749
  /**
3779
- * Pure function picks the best supported runtime for the given hardware env.
3780
- *
3781
- * Logic:
3782
- * 1. If `bestBackendHint` is in the supported set, use it.
3783
- * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
3784
- * 3. Floor to `'onnx'` (always supported).
3750
+ * Build an executable tree from user config.
3785
3751
  *
3786
- * Device is always `defaultDeviceFor(chosen)`.
3752
+ * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3753
+ * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3754
+ * Throws if step not loaded.
3787
3755
  */
3788
- function pickBestRuntime(env, bestBackendHint) {
3789
- const supported = supportedRuntimes(env);
3790
- const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
3756
+ function buildExecutableTree(steps, getEngine) {
3757
+ return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3758
+ }
3759
+ function buildNode(step, getEngine) {
3760
+ const definition = getStepDefinition(step.addonId);
3761
+ const engine = getEngine(step.addonId);
3762
+ const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3763
+ const mergedSettings = {
3764
+ ...collectSchemaDefaults(step.addonId),
3765
+ ...step.settings
3766
+ };
3791
3767
  return {
3792
- runtimeId: chosen,
3793
- device: defaultDeviceFor(chosen)
3768
+ stepId: step.addonId,
3769
+ definition,
3770
+ engine,
3771
+ modelId: step.modelId,
3772
+ inputClasses: definition.inputClasses ?? [],
3773
+ enabled: step.enabled,
3774
+ children,
3775
+ ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3794
3776
  };
3795
3777
  }
3778
+ /**
3779
+ * Walk the step's declared `getConfigSchema()` and collect every field
3780
+ * with a primitive `default` value into a plain record. Group fields
3781
+ * are flattened — a nested `{ type: 'group', fields: [...] }` is
3782
+ * entered recursively. Fields without a declared default are skipped.
3783
+ */
3784
+ function collectSchemaDefaults(stepId) {
3785
+ const schema = getStep(stepId).getConfigSchema();
3786
+ const out = {};
3787
+ walkFieldsForDefaults(schema, out);
3788
+ return out;
3789
+ }
3790
+ function walkFieldsForDefaults(fields, out) {
3791
+ for (const field of fields) {
3792
+ if ("type" in field && field.type === "group" && "fields" in field) {
3793
+ walkFieldsForDefaults(field.fields, out);
3794
+ continue;
3795
+ }
3796
+ if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3797
+ }
3798
+ }
3796
3799
  //#endregion
3797
3800
  //#region src/detection-pipeline/provider.ts
3798
3801
  /**
@@ -3995,6 +3998,27 @@ function toProbedHardware(hw) {
3995
3998
  gpu: hw.gpu ? { type: hw.gpu.type } : null
3996
3999
  };
3997
4000
  }
4001
+ var ONNX_FLOOR = {
4002
+ runtime: "python",
4003
+ backend: "onnx",
4004
+ format: "onnx",
4005
+ device: "cpu"
4006
+ };
4007
+ /**
4008
+ * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
4009
+ * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
4010
+ * result is identical (onnx / cpu) but is now derived through the shared rules
4011
+ * instead of duplicated inline.
4012
+ */
4013
+ function onnxFloorPick() {
4014
+ const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
4015
+ return {
4016
+ runtime: "python",
4017
+ backend: pick.runtimeId,
4018
+ format: modelFormatFor(pick.runtimeId),
4019
+ device: pick.device
4020
+ };
4021
+ }
3998
4022
  var DetectionPipelineProvider = class DetectionPipelineProvider {
3999
4023
  modelsDir;
4000
4024
  eventBus;
@@ -4053,6 +4077,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4053
4077
  */
4054
4078
  needsAutoPick = false;
4055
4079
  /**
4080
+ * Unsubscribe handle for the deferred-auto-pick `platform-probe` ready
4081
+ * listener (armed in `setApi` when the probe isn't ready yet). Cleared once
4082
+ * the engine is resolved — either by the listener firing or by the boot
4083
+ * safety-net `ensureBootEngineProvisioned`.
4084
+ */
4085
+ deferredAutoPickUnsub = null;
4086
+ /**
4056
4087
  * Warm cache for benchmark engine-override runs.
4057
4088
  *
4058
4089
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -4084,8 +4115,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4084
4115
  this.readStore = () => settings.readAddonStore();
4085
4116
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4086
4117
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4087
- this.currentEngine = DetectionPipelineProvider.detectBestEngine();
4088
- this.log.info("Engine selected (default)", { meta: {
4118
+ this.currentEngine = ONNX_FLOOR;
4119
+ this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4089
4120
  runtime: this.currentEngine.runtime,
4090
4121
  backend: this.currentEngine.backend,
4091
4122
  format: this.currentEngine.format
@@ -4150,69 +4181,78 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4150
4181
  isReady() {
4151
4182
  return this.ready;
4152
4183
  }
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
4184
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
4202
4185
  async setApi(addonCtx) {
4203
4186
  this.addonCtx = addonCtx;
4204
- if (this.needsAutoPick) {
4187
+ if (this.needsAutoPick) if (this.addonCtx.useCapability("platform-probe").isReady) {
4205
4188
  await this.autoPickAndPersist();
4206
4189
  this.needsAutoPick = false;
4190
+ this.startProvisioningForCurrentEngine();
4191
+ } else {
4192
+ const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4193
+ if (state !== "ready") return;
4194
+ this.cancelDeferredAutoPick();
4195
+ if (!this.needsAutoPick) return;
4196
+ this.autoPickAndPersist().then(() => {
4197
+ this.needsAutoPick = false;
4198
+ this.startProvisioningForCurrentEngine();
4199
+ });
4200
+ });
4201
+ this.deferredAutoPickUnsub = unsubscribe;
4202
+ this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
4207
4203
  }
4204
+ else this.startProvisioningForCurrentEngine();
4205
+ }
4206
+ /** Tear down the deferred-auto-pick probe listener, if still armed. */
4207
+ cancelDeferredAutoPick() {
4208
+ if (this.deferredAutoPickUnsub) {
4209
+ this.deferredAutoPickUnsub();
4210
+ this.deferredAutoPickUnsub = null;
4211
+ }
4212
+ }
4213
+ /**
4214
+ * Boot safety-net: deterministically provision the hardware-probed engine.
4215
+ *
4216
+ * Called from the addon's `onInitialize` right after `reprobeEngine` has
4217
+ * written this node's probe-driven selection (e.g. `engineBackend=openvino`)
4218
+ * to the store. When first-boot auto-pick was DEFERRED (probe not ready at
4219
+ * `setApi`), the engine would otherwise stay `idle` — selected but never
4220
+ * provisioned — because the `onCapabilityStateChange('platform-probe')` ready
4221
+ * edge can be missed when the cap is already 'ready' by the time we subscribe
4222
+ * (the old eager-onnx-floor masked this; removing it surfaced a node that
4223
+ * boots with no engine at all). This loads the now-persisted selection and
4224
+ * starts provisioning it, so the node reliably comes up on its real best
4225
+ * engine (openvino on Intel) with no onnx floor and no missed boot. No-op
4226
+ * when provisioning already started (persisted-engine path / listener fired)
4227
+ * or when nothing has been selected yet.
4228
+ */
4229
+ async ensureBootEngineProvisioned() {
4230
+ if (this.getEngineProvisioning().state !== "idle") return;
4231
+ const stored = await this.loadEngine();
4232
+ if (!stored) return;
4233
+ this.currentEngine = stored;
4234
+ this.needsAutoPick = false;
4235
+ this.cancelDeferredAutoPick();
4236
+ this.log.info("Boot engine provisioning from probed selection", { meta: {
4237
+ runtime: stored.runtime,
4238
+ backend: stored.backend,
4239
+ device: stored.device ?? null
4240
+ } });
4208
4241
  this.startProvisioningForCurrentEngine();
4209
4242
  }
4210
4243
  /**
4211
4244
  * Auto-pick the best supported runtime at first boot (no stored engine).
4212
4245
  * Uses the platform-probe cap's hardware + bestScore hint when available;
4213
4246
  * falls back to platform/arch when the probe cap is not yet reachable.
4247
+ *
4214
4248
  * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4215
- * boots load it via `loadEngine()` and skip this path.
4249
+ * boots load it via `loadEngine()` and skip this path — but ONLY when the
4250
+ * probe actually answered (real `hardware` or a `bestScore` hint). If the
4251
+ * probe query failed (cold-start race, cap momentarily unreachable), the
4252
+ * pick floors to onnx purely for lack of information; persisting that would
4253
+ * LOCK onnx and skip auto-pick on every future boot even after the
4254
+ * accelerator surfaces. In that case we set the in-memory floor for liveness
4255
+ * but leave the store untouched so the next boot re-attempts the pick.
4216
4256
  */
4217
4257
  async autoPickAndPersist() {
4218
4258
  let hardware = null;
@@ -4220,7 +4260,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4220
4260
  try {
4221
4261
  const api = this.addonCtx?.api;
4222
4262
  if (api) {
4223
- const caps = await api.platformProbe.getCapabilities.query();
4263
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
4224
4264
  hardware = caps?.hardware ?? null;
4225
4265
  const bs = caps?.bestScore;
4226
4266
  if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
@@ -4234,9 +4274,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4234
4274
  device: pick.device
4235
4275
  };
4236
4276
  this.currentEngine = engine;
4277
+ if (!(hardware !== null || bestBackendHint !== null)) {
4278
+ this.log.warn("Auto-pick: probe returned no hardware/hint — using onnx floor WITHOUT persisting", { meta: {
4279
+ backend: pick.runtimeId,
4280
+ device: pick.device
4281
+ } });
4282
+ return;
4283
+ }
4284
+ const apNode = this.localProbeNodeId();
4237
4285
  await this.writeStore({
4238
- engineBackend: pick.runtimeId,
4239
- engineDevice: pick.device
4286
+ [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4287
+ [nodeEngineKey("engineDevice", apNode)]: pick.device
4240
4288
  });
4241
4289
  this.log.info("Auto-picked engine at first boot", { meta: {
4242
4290
  backend: pick.runtimeId,
@@ -4353,7 +4401,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4353
4401
  if (!step.enabled) continue;
4354
4402
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4355
4403
  if (!modelEntry) continue;
4356
- if (require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4404
+ if (require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4357
4405
  await this.downloadWithRetry(modelEntry, format, 3);
4358
4406
  }
4359
4407
  }
@@ -4409,11 +4457,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4409
4457
  * callers fall back to the registry's safe minimum. The engine OFFER derives
4410
4458
  * from hardware ONLY — install state (probe `scores`) never gates it.
4411
4459
  */
4460
+ /**
4461
+ * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4462
+ * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4463
+ * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4464
+ * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4465
+ * have) and pin a device the node can't run, breaking provisioning.
4466
+ */
4467
+ localProbeNodeId() {
4468
+ const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
4469
+ return raw.includes("/") ? raw.split("/")[0] : raw;
4470
+ }
4412
4471
  async fetchProbeGatingData() {
4413
4472
  try {
4414
4473
  const api = this.addonCtx?.api;
4415
4474
  if (!api) return { hardware: null };
4416
- return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
4475
+ return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4417
4476
  } catch {
4418
4477
  return { hardware: null };
4419
4478
  }
@@ -4550,7 +4609,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4550
4609
  const formats = {};
4551
4610
  for (const [formatKey, entry] of Object.entries(m.formats)) {
4552
4611
  if (!entry) continue;
4553
- const downloaded = require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, m, formatKey);
4612
+ const downloaded = require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, m, formatKey);
4554
4613
  formats[formatKey] = {
4555
4614
  url: entry.url,
4556
4615
  sizeMB: entry.sizeMB,
@@ -4673,7 +4732,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4673
4732
  const { modelId, format, addonId } = input;
4674
4733
  const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4675
4734
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4676
- if (!require_model_download_service_C7AjBsX9.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4735
+ if (!require_model_download_service_RxAOiYvX.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4677
4736
  this.log.info("Model deleted from disk", { meta: {
4678
4737
  modelId,
4679
4738
  format
@@ -5128,7 +5187,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5128
5187
  const format = this.currentEngine?.format ?? "onnx";
5129
5188
  for (const step of needed) {
5130
5189
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
5131
- if (modelEntry && !require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) {
5190
+ if (modelEntry && !require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, modelEntry, format)) {
5132
5191
  this.log.info("Downloading model for step", { meta: {
5133
5192
  modelId: step.modelId,
5134
5193
  format,
@@ -5153,7 +5212,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5153
5212
  /** Download a model with retry + exponential backoff */
5154
5213
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
5155
5214
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
5156
- await require_model_download_service_C7AjBsX9.ensureModel(this.modelsDir, entry, format, onProgress);
5215
+ await require_model_download_service_RxAOiYvX.ensureModel(this.modelsDir, entry, format, onProgress);
5157
5216
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
5158
5217
  return;
5159
5218
  } catch (err) {
@@ -5478,7 +5537,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5478
5537
  this.engineFactory = null;
5479
5538
  this.executor = null;
5480
5539
  }
5481
- for (const id of [...this.deviceProxies.keys()]) this.releaseDeviceProxy(id);
5540
+ for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5482
5541
  }
5483
5542
  /**
5484
5543
  * Resolve and cache a {@link DeviceProxy} for the given camera. Pins
@@ -5630,11 +5689,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5630
5689
  } });
5631
5690
  continue;
5632
5691
  }
5633
- if (!require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5692
+ if (!require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5634
5693
  modelId: step.modelId,
5635
5694
  format
5636
5695
  } });
5637
- downloads.push(require_model_download_service_C7AjBsX9.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
5696
+ downloads.push(require_model_download_service_RxAOiYvX.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
5638
5697
  }
5639
5698
  await Promise.all(downloads);
5640
5699
  await this.ensureBackendDeps(this.currentEngine);
@@ -5662,25 +5721,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5662
5721
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
5663
5722
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
5664
5723
  * Returns null when neither source has anything; the caller keeps
5665
- * whatever `detectBestEngine()` picked at construction.
5724
+ * the onnx floor set at construction until autoPickAndPersist() runs.
5666
5725
  */
5667
5726
  async loadEngine() {
5668
5727
  const store = await this.readStore();
5669
5728
  const storedRuntime = store["engineRuntime"];
5670
- const storedBackend = store["engineBackend"];
5729
+ const node = this.localProbeNodeId();
5730
+ const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5671
5731
  if (typeof storedBackend === "string" && storedBackend.length > 0) {
5672
5732
  const backend = storedBackend;
5673
- const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5674
- const detected = DetectionPipelineProvider.detectBestEngine();
5733
+ const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5734
+ const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5735
+ const floor = onnxFloorPick();
5675
5736
  const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5676
5737
  if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
5677
- this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5738
+ this.log.warn("Stored engine backend unavailable on this node — falling back to onnx floor", { meta: {
5678
5739
  stored: migratedBackend,
5679
- fallback: `${detected.backend}`
5740
+ fallback: `${floor.backend}`
5680
5741
  } });
5681
- return detected;
5742
+ return floor;
5682
5743
  }
5683
- const device = storedDevice || detected.device;
5744
+ const device = storedDevice || floor.device;
5684
5745
  return {
5685
5746
  runtime: "python",
5686
5747
  backend: migratedBackend,
@@ -5852,18 +5913,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5852
5913
  const api = this.addonCtx?.api;
5853
5914
  let best;
5854
5915
  if (api) try {
5855
- const caps = await api.platformProbe.getCapabilities.query();
5916
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
5856
5917
  const bs = caps?.bestScore;
5857
5918
  if (bs && bs.runtime === "python") {
5858
5919
  const probeBackend = bs.backend;
5859
5920
  const probeDevice = (() => {
5860
5921
  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
- }
5922
+ if (probeBackend === "openvino") return defaultDeviceFor("openvino");
5923
+ if (probeBackend === "coreml") return defaultDeviceFor("coreml");
5867
5924
  if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
5868
5925
  return "cpu";
5869
5926
  })();
@@ -5873,16 +5930,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5873
5930
  format: backendToFormat(probeBackend),
5874
5931
  device: probeDevice
5875
5932
  };
5876
- } else best = DetectionPipelineProvider.detectBestEngine();
5933
+ } else best = onnxFloorPick();
5877
5934
  } catch {
5878
- best = DetectionPipelineProvider.detectBestEngine();
5935
+ best = onnxFloorPick();
5879
5936
  }
5880
- else best = DetectionPipelineProvider.detectBestEngine();
5937
+ else best = onnxFloorPick();
5881
5938
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
5939
+ const rpNode = this.localProbeNodeId();
5882
5940
  await this.writeStore({
5883
- probedBestEngine: probedLabel,
5884
- engineBackend: best.backend,
5885
- engineDevice: best.device ?? "cpu"
5941
+ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
5942
+ [nodeEngineKey("engineBackend", rpNode)]: best.backend,
5943
+ [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
5886
5944
  });
5887
5945
  this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
5888
5946
  backend: best.backend,
@@ -6046,7 +6104,7 @@ function buildSchemaSlots(format, modelsDir) {
6046
6104
  id: m.id,
6047
6105
  name: m.name,
6048
6106
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
6049
- downloaded: require_model_download_service_C7AjBsX9.isModelDownloaded(modelsDir, m, f),
6107
+ downloaded: require_model_download_service_RxAOiYvX.isModelDownloaded(modelsDir, m, f),
6050
6108
  sizeMB: entry.sizeMB
6051
6109
  }]))
6052
6110
  })),
@@ -6305,7 +6363,8 @@ var DEFAULT_CONFIG = {
6305
6363
  engineRuntime: "python",
6306
6364
  engineBackend: "onnx",
6307
6365
  engineDevice: "cpu",
6308
- probedBestEngine: ""
6366
+ probedBestEngine: "",
6367
+ activeEngine: ""
6309
6368
  };
6310
6369
  /** Derive the model-format from a backend value. Called by the provider. */
6311
6370
  function backendToFormat(backend) {
@@ -6348,6 +6407,15 @@ var POOL_BOUND_KEYS = [
6348
6407
  ];
6349
6408
  var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6350
6409
  provider = null;
6410
+ /** Last non-null probed hardware PER NODE — reused when the probe transiently
6411
+ * returns null so offered backends / device lists don't collapse. Keyed by
6412
+ * node because the hub addon probes other nodes when serving their config. */
6413
+ lastGoodHardwareByNode = /* @__PURE__ */ new Map();
6414
+ /** This node's effective engine selection, cached from the node-scoped store
6415
+ * so the synchronous `resolveBackendTuning` and the reprobe gate don't read
6416
+ * the cluster-shared bare keys. Refreshed at init + on every config change. */
6417
+ nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
6418
+ nodeProbedBestEngine = "";
6351
6419
  engineMetricsTimer = null;
6352
6420
  /** Snapshot-equality cache for engine-metrics emit. Most ticks
6353
6421
  * the engine inventory is unchanged (no model load/unload), so
@@ -6402,6 +6470,14 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6402
6470
  tooltip: "Re-probe engine"
6403
6471
  }]
6404
6472
  }),
6473
+ this.field({
6474
+ type: "text",
6475
+ key: "activeEngine",
6476
+ label: "Active engine",
6477
+ 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.",
6478
+ readonlyField: true,
6479
+ default: ""
6480
+ }),
6405
6481
  this.field({
6406
6482
  type: "select",
6407
6483
  key: "engineBackend",
@@ -6538,14 +6614,18 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6538
6614
  * probe is unreachable). Stored backend / device snap back to the registry
6539
6615
  * floor / default when they fall outside the offered set.
6540
6616
  */
6541
- async getGlobalSettings(overlay) {
6542
- const ctx = this.ctxIfReady;
6543
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
6617
+ async getGlobalSettings(overlay, _cap, nodeId) {
6618
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6619
+ const rawStored = await this.resolveUiSettingsStore();
6620
+ if (rawStored === null) return null;
6621
+ const stored = projectNodeEngine(rawStored, targetNode);
6622
+ const storedBackendRaw = stored["engineBackend"];
6623
+ if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6544
6624
  const merged = overlay ? {
6545
6625
  ...stored,
6546
6626
  ...overlay
6547
6627
  } : stored;
6548
- const env = await this.probeHardwareEnv();
6628
+ const env = await this.probeHardwareEnv(targetNode);
6549
6629
  const hardware = env.hardware;
6550
6630
  const offered = supportedRuntimes(env);
6551
6631
  const runtimeBackends = offered.map((id) => ({
@@ -6553,14 +6633,22 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6553
6633
  label: runtimeLabel(id)
6554
6634
  }));
6555
6635
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6556
- const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6636
+ const backend = (() => {
6637
+ const rid = toRuntimeId(storedBackend);
6638
+ if (rid === "onnx") return "onnx";
6639
+ if (offered.includes(rid)) return rid;
6640
+ return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6641
+ })();
6557
6642
  const deviceOptions = runtimeDevices(backend, hardware);
6558
6643
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6559
6644
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6645
+ const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6646
+ const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6560
6647
  const raw = {
6561
6648
  ...merged,
6562
6649
  engineBackend: backend,
6563
- engineDevice: device
6650
+ engineDevice: device,
6651
+ activeEngine
6564
6652
  };
6565
6653
  const schema = this.globalSettingsSchema();
6566
6654
  if (!schema) return { sections: [] };
@@ -6591,15 +6679,15 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6591
6679
  if (field.type === "slider" && "key" in field) {
6592
6680
  const tuned = tuning[field.key];
6593
6681
  const sliderField = field;
6594
- let patched = typeof tuned === "number" ? {
6682
+ let patchedField = typeof tuned === "number" ? {
6595
6683
  ...sliderField,
6596
6684
  default: tuned
6597
6685
  } : sliderField;
6598
- if (sliderField.key === "concurrency" && backend === "coreml") patched = {
6599
- ...patched,
6686
+ if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
6687
+ ...patchedField,
6600
6688
  max: 4
6601
6689
  };
6602
- return patched;
6690
+ return patchedField;
6603
6691
  }
6604
6692
  return field;
6605
6693
  })
@@ -6616,23 +6704,41 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6616
6704
  * Protected seam — overridable by tests (canned hardware) and reused by the
6617
6705
  * Phase 2 auto-pick path. NEVER reads install state.
6618
6706
  */
6619
- async probeHardwareEnv() {
6620
- const hardware = await this.resolveProbeHardware();
6707
+ /**
6708
+ * Resolve the persisted UI settings store, or `null` when it can't be read
6709
+ * right now (ctx/settings not wired — mid-restart). A readable-but-empty
6710
+ * store (genuine first boot) returns `{}`, never null. `getGlobalSettings`
6711
+ * uses the null signal to avoid serving fabricated empty-store defaults
6712
+ * during the restart window. Seam kept protected so tests can drive the
6713
+ * unreadable vs empty vs populated cases without faking a full AddonContext.
6714
+ */
6715
+ async resolveUiSettingsStore() {
6716
+ const settings = this.ctxIfReady?.settings;
6717
+ if (!settings) return null;
6718
+ return await settings.readAddonStore() ?? {};
6719
+ }
6720
+ async probeHardwareEnv(nodeId) {
6721
+ const node = nodeId ?? this.localNodeId();
6722
+ const hardware = await this.resolveProbeHardware(node);
6723
+ if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
6724
+ const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
6621
6725
  return {
6622
6726
  platform: process.platform,
6623
6727
  arch: process.arch,
6624
- hardware
6728
+ hardware: effective
6625
6729
  };
6626
6730
  }
6627
6731
  /**
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).
6732
+ * Fetch the probed hardware from the platform-probe cap for `nodeId` (default
6733
+ * = self). Returns null when the cap is not reachable (caller falls back to
6734
+ * the registry's safe minimum).
6630
6735
  */
6631
- async resolveProbeHardware() {
6736
+ async resolveProbeHardware(nodeId) {
6632
6737
  try {
6633
6738
  const api = this.ctxIfReady?.api;
6634
6739
  if (!api) return null;
6635
- const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6740
+ const node = nodeId ?? this.localNodeId();
6741
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, require_dist.nodePin(node)))?.hardware;
6636
6742
  if (!hw) return null;
6637
6743
  return {
6638
6744
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -6643,6 +6749,61 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6643
6749
  }
6644
6750
  }
6645
6751
  /**
6752
+ * Bare node id used to scope the engine cascade in the shared store. MUST
6753
+ * match the provider's `localProbeNodeId()` (kernel.localNodeId, default
6754
+ * 'hub') so the UI write path (this) and the provider read/write path
6755
+ * (loadEngine / autoPick / reprobe) target the SAME `<key>@<nodeId>`. The old
6756
+ * `?? this.ctx.id` fallback resolved to the ADDON id ('detection-pipeline')
6757
+ * when kernel.localNodeId was absent, so UI saves landed on a key the
6758
+ * provider never read — silently losing the per-node selection.
6759
+ */
6760
+ localNodeId() {
6761
+ return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
6762
+ }
6763
+ /**
6764
+ * Refresh this node's cached engine selection from the node-scoped store.
6765
+ * `resolveBackendTuning` is synchronous and the reprobe gate runs before the
6766
+ * provider exists, so both read these cached fields instead of the
6767
+ * cluster-shared bare keys (which belong to no single node). Best-effort: a
6768
+ * transiently-unreadable store leaves the last cached values in place.
6769
+ */
6770
+ async refreshNodeEngineFromStore() {
6771
+ const store = await this.resolveUiSettingsStore();
6772
+ if (store === null) return;
6773
+ const node = this.localNodeId();
6774
+ const backend = readNodeEngineValue(store, "engineBackend", node);
6775
+ if (typeof backend === "string" && backend !== "") this.nodeEngineBackend = backend;
6776
+ const probed = readNodeEngineValue(store, "probedBestEngine", node);
6777
+ this.nodeProbedBestEngine = typeof probed === "string" ? probed : "";
6778
+ }
6779
+ /**
6780
+ * Persist a settings patch, mirroring the engine cascade fields to the TARGET
6781
+ * node's scoped keys so each node keeps an INDEPENDENT engine selection in the
6782
+ * cluster-central store. The hub addon serves writes for every node, so it
6783
+ * scopes by the requested `nodeId` (default self).
6784
+ *
6785
+ * When the target IS this node, `super.updateGlobalSettings` drives the normal
6786
+ * apply path (`resolveConfig` / `onConfigChanged` / `requiresRestart` restart),
6787
+ * and the bare engine keys it writes are shadowed by the node-scoped keys on
6788
+ * read. When the target is a SIBLING node, we persist the scoped engine keys +
6789
+ * the non-engine bare keys but DON'T run this node's restart/reprovision — the
6790
+ * owning node applies its own engine selection on its next (re)start.
6791
+ */
6792
+ async updateGlobalSettings(patch, nodeId) {
6793
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6794
+ const patchRecord = patch;
6795
+ const scopedEngine = {};
6796
+ for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
6797
+ if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
6798
+ if (targetNode === this.localNodeId()) {
6799
+ await super.updateGlobalSettings(patch, nodeId);
6800
+ return;
6801
+ }
6802
+ const sharedPatch = {};
6803
+ for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
6804
+ if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
6805
+ }
6806
+ /**
6646
6807
  * Resolve the effective pool tuning for the configured backend.
6647
6808
  *
6648
6809
  * Reads the registry's `tuningFor(backend)` and ignores any persisted
@@ -6656,7 +6817,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6656
6817
  * a reason to disagree.
6657
6818
  */
6658
6819
  resolveBackendTuning() {
6659
- const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6820
+ const t = tuningFor(toRuntimeId(this.nodeEngineBackend ?? DEFAULT_CONFIG.engineBackend));
6660
6821
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6661
6822
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6662
6823
  return {
@@ -6675,6 +6836,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6675
6836
  relativePath: ""
6676
6837
  }).catch(() => "camstack-data/models");
6677
6838
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6839
+ await this.refreshNodeEngineFromStore();
6678
6840
  this.pythonAddonDir = resolveAddonPythonDir();
6679
6841
  const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6680
6842
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
@@ -6696,9 +6858,12 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6696
6858
  });
6697
6859
  await this.provider.init();
6698
6860
  await this.provider.setApi(this.ctx);
6699
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6861
+ if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6700
6862
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6701
6863
  });
6864
+ await this.provider.ensureBootEngineProvisioned().catch((err) => {
6865
+ this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6866
+ });
6702
6867
  await this.provider.warmPool();
6703
6868
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
6704
6869
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -6748,8 +6913,8 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6748
6913
  /**
6749
6914
  * Proactively install the OpenVINO Python package when Intel hardware
6750
6915
  * (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.
6916
+ * provider is constructed, so the module is RUNNABLE when the engine is
6917
+ * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
6753
6918
  *
6754
6919
  * Failure is non-fatal: a warning is logged and the addon continues with
6755
6920
  * the onnx-cpu baseline. The hardware query itself is also best-effort —
@@ -6818,6 +6983,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6818
6983
  * lifecycle (engineFactory rebuild on next runPipeline).
6819
6984
  */
6820
6985
  async onConfigChanged() {
6986
+ await this.refreshNodeEngineFromStore();
6821
6987
  if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6822
6988
  this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6823
6989
  });