@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
@@ -1,11 +1,255 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as EventCategory, C as evaluateZoneRules, F as errMsg, I as BaseAddon, S as detectionPipelineCapability, U as createEvent, W as hydrateSchema, X as sleep, a as COCO_TO_MACRO, i as COCO_80_LABELS, k as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as hfModelUrl } from "../dist-CjrjeaDd.mjs";
3
- import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C7AjBsX9-B0ekM6dF.mjs";
2
+ import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-BA6DR_jV.mjs";
3
+ import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-RxAOiYvX-CMAvhgO7.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import * as os from "node:os";
7
7
  import { spawn } from "node:child_process";
8
8
  import sharp from "sharp";
9
+ //#region src/detection-pipeline/engine-store-keys.ts
10
+ /**
11
+ * Per-node scoping for the detection-pipeline engine cascade.
12
+ *
13
+ * The detection addon's settings store is a single CLUSTER-SHARED blob (the
14
+ * settings-store cap is hub-resident; every node's detection instance reads and
15
+ * writes the same keys). That is correct for node-agnostic settings (pipeline
16
+ * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
17
+ * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
18
+ * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
19
+ * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
20
+ * another node that has no NPU.
21
+ *
22
+ * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
23
+ * Everything else in the store stays shared. A legacy un-scoped value (written
24
+ * before this change, or by an older build) is read as a migration fallback and
25
+ * re-persisted under the node-scoped key on the next write.
26
+ */
27
+ var ENGINE_CASCADE_KEYS = [
28
+ "engineBackend",
29
+ "engineDevice",
30
+ "probedBestEngine"
31
+ ];
32
+ function isEngineCascadeKey(key) {
33
+ return ENGINE_CASCADE_KEYS.includes(key);
34
+ }
35
+ /**
36
+ * Normalise a raw kernel node id to the bare node id used for scoping.
37
+ * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
38
+ * per-NODE, so strip the addon segment. Falls back to `hub`.
39
+ */
40
+ function normalizeEngineNodeId(rawNodeId) {
41
+ const raw = rawNodeId ?? "hub";
42
+ return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
43
+ }
44
+ /** The node-scoped store key for an engine cascade field. */
45
+ function nodeEngineKey(base, nodeId) {
46
+ return `${base}@${normalizeEngineNodeId(nodeId)}`;
47
+ }
48
+ /**
49
+ * Read an engine cascade value for a node: the node-scoped key if present,
50
+ * otherwise the legacy un-scoped value (migration), otherwise undefined.
51
+ */
52
+ function readNodeEngineValue(store, base, nodeId) {
53
+ const scoped = store[nodeEngineKey(base, nodeId)];
54
+ return scoped !== void 0 ? scoped : store[base];
55
+ }
56
+ /**
57
+ * Project a raw store onto the plain engine cascade keys for THIS node, so the
58
+ * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
59
+ * the node's own selection. Non-engine keys are left untouched. Node-scoped
60
+ * keys for OTHER nodes are dropped from the projection (not relevant to this
61
+ * node's form).
62
+ */
63
+ function projectNodeEngine(store, nodeId) {
64
+ const out = {};
65
+ const scopedForAnyNode = /* @__PURE__ */ new Set();
66
+ for (const key of Object.keys(store)) {
67
+ const atIdx = key.indexOf("@");
68
+ if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
69
+ scopedForAnyNode.add(key);
70
+ continue;
71
+ }
72
+ out[key] = store[key];
73
+ }
74
+ for (const base of ENGINE_CASCADE_KEYS) {
75
+ const value = readNodeEngineValue(store, base, nodeId);
76
+ if (value !== void 0) out[base] = value;
77
+ }
78
+ return out;
79
+ }
80
+ //#endregion
81
+ //#region src/detection-pipeline/runtimes.ts
82
+ var KNOWN_PLATFORMS = [
83
+ "darwin",
84
+ "linux",
85
+ "win32"
86
+ ];
87
+ var KNOWN_ARCHES = ["arm64", "x64"];
88
+ var KNOWN_GPU_TYPES = [
89
+ "nvidia",
90
+ "amd",
91
+ "intel",
92
+ "apple"
93
+ ];
94
+ var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
95
+ function toKnownPlatform(p) {
96
+ return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
97
+ }
98
+ function toKnownArch(a) {
99
+ return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
100
+ }
101
+ function gpuInfoFrom(hw) {
102
+ if (!hw.gpu) return null;
103
+ const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
104
+ if (!type) return null;
105
+ return {
106
+ type,
107
+ name: ""
108
+ };
109
+ }
110
+ function npuInfoFrom(hw) {
111
+ if (!hw.npu) return null;
112
+ const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
113
+ if (!type) return null;
114
+ return { type };
115
+ }
116
+ function envToHardwareInfo(env) {
117
+ if (!env.hardware) return null;
118
+ return {
119
+ platform: toKnownPlatform(env.platform),
120
+ arch: toKnownArch(env.arch),
121
+ cpuModel: "",
122
+ cpuCores: 0,
123
+ totalRAM_MB: 0,
124
+ availableRAM_MB: 0,
125
+ gpu: gpuInfoFrom(env.hardware),
126
+ npu: npuInfoFrom(env.hardware)
127
+ };
128
+ }
129
+ function probedToHardwareInfo(hw) {
130
+ if (!hw) return null;
131
+ return {
132
+ platform: toKnownPlatform(process.platform),
133
+ arch: toKnownArch(process.arch),
134
+ cpuModel: "",
135
+ cpuCores: 0,
136
+ totalRAM_MB: 0,
137
+ availableRAM_MB: 0,
138
+ gpu: gpuInfoFrom(hw),
139
+ npu: npuInfoFrom(hw)
140
+ };
141
+ }
142
+ var RUNTIME_DETAIL = {
143
+ onnx: {
144
+ label: "ONNX Runtime",
145
+ pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
146
+ tuning: {
147
+ concurrency: 4,
148
+ batchMode: "list",
149
+ maxBatchSize: 8,
150
+ intraOpThreads: 0
151
+ }
152
+ },
153
+ openvino: {
154
+ label: "OpenVINO",
155
+ pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
156
+ tuning: {
157
+ concurrency: 1,
158
+ batchMode: "none",
159
+ numStreams: 0
160
+ }
161
+ },
162
+ coreml: {
163
+ label: "CoreML",
164
+ pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
165
+ tuning: {
166
+ concurrency: 1,
167
+ batchMode: "none",
168
+ windowMs: 8,
169
+ maxBatchSize: 8,
170
+ numWorkers: 1
171
+ }
172
+ }
173
+ };
174
+ /**
175
+ * Returns the list of supported runtime IDs for the given hardware env.
176
+ * Delegates to `@camstack/types` `supportedRuntimes`.
177
+ */
178
+ function supportedRuntimes(env) {
179
+ return supportedRuntimes$1(envToHardwareInfo(env));
180
+ }
181
+ /**
182
+ * Returns the device options for a given runtime and probed hardware.
183
+ * Delegates to `@camstack/types` `runtimeDevices`.
184
+ */
185
+ function runtimeDevices(id, hardware) {
186
+ return runtimeDevices$1(id, probedToHardwareInfo(hardware));
187
+ }
188
+ /**
189
+ * Returns the default device string for a given runtime.
190
+ * Delegates to `@camstack/types` `defaultDeviceFor`.
191
+ */
192
+ function defaultDeviceFor(id) {
193
+ return defaultDeviceFor$1(id);
194
+ }
195
+ /** Model format for each inference runtime supported by the detection pipeline. */
196
+ var RUNTIME_FORMAT = {
197
+ onnx: "onnx",
198
+ openvino: "openvino",
199
+ coreml: "coreml"
200
+ };
201
+ /**
202
+ * Returns the model format required for a given runtime.
203
+ * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
204
+ * matching the engine-provisioner's own ModelFormat type.
205
+ */
206
+ function modelFormatFor(id) {
207
+ return RUNTIME_FORMAT[id];
208
+ }
209
+ function pythonRequirementsFor(id) {
210
+ return RUNTIME_DETAIL[id].pythonRequirements;
211
+ }
212
+ function tuningFor(id) {
213
+ return RUNTIME_DETAIL[id].tuning;
214
+ }
215
+ function runtimeLabel(id) {
216
+ return RUNTIME_DETAIL[id].label;
217
+ }
218
+ /**
219
+ * Proactive-install hint kept for back-compat (re-exported from index.ts).
220
+ * Intel on Linux warrants installing openvino; coremltools covers macOS;
221
+ * openvino has no AMD/NVIDIA backend.
222
+ */
223
+ function shouldInstallOpenvino(env) {
224
+ if (env.platform === "darwin") return false;
225
+ return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
226
+ }
227
+ //#endregion
228
+ //#region src/detection-pipeline/auto-pick.ts
229
+ var PREFERENCE = [
230
+ "coreml",
231
+ "openvino",
232
+ "onnx"
233
+ ];
234
+ /**
235
+ * Pure function — picks the best supported runtime for the given hardware env.
236
+ *
237
+ * Logic:
238
+ * 1. If `bestBackendHint` is in the supported set, use it.
239
+ * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
240
+ * 3. Floor to `'onnx'` (always supported).
241
+ *
242
+ * Device is always `defaultDeviceFor(chosen)`.
243
+ */
244
+ function pickBestRuntime(env, bestBackendHint) {
245
+ const supported = supportedRuntimes(env);
246
+ const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
247
+ return {
248
+ runtimeId: chosen,
249
+ device: defaultDeviceFor(chosen)
250
+ };
251
+ }
252
+ //#endregion
9
253
  //#region src/detection-pipeline/engine/shared-inference-pool.ts
10
254
  var MSG_COMMAND = 0;
11
255
  var MSG_INFER_JPEG = 1;
@@ -772,6 +1016,26 @@ var HF_REPO = "camstack/camstack-models";
772
1016
  var HF_SCRYPTED = "scrypted/plugin-models";
773
1017
  var hf = (path) => hfModelUrl(HF_REPO, path);
774
1018
  var hfScrypted = (path) => hfModelUrl(HF_SCRYPTED, path);
1019
+ /**
1020
+ * Build an OpenVINO format entry (always python runtime).
1021
+ *
1022
+ * OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
1023
+ * weights file with the same basename. We declare the `.bin` in `files` so
1024
+ * the (format-agnostic) downloader fetches it alongside the `.xml` — without
1025
+ * the weights, OpenVINO compile fails with "Empty weights data in bin file".
1026
+ * A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
1027
+ * sibling, so none is added.
1028
+ */
1029
+ var ovFormat = (url, sizeMB) => {
1030
+ const base = url.split("/").pop() ?? "";
1031
+ const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
1032
+ return {
1033
+ url,
1034
+ sizeMB,
1035
+ runtimes: ["python"],
1036
+ ...files ? { files } : {}
1037
+ };
1038
+ };
775
1039
  var MLPACKAGE_FILES = [
776
1040
  "Manifest.json",
777
1041
  "Data/com.apple.CoreML/model.mlmodel",
@@ -800,11 +1064,7 @@ var OBJECT_DETECTION_MODELS = [
800
1064
  files: [...MLPACKAGE_FILES],
801
1065
  runtimes: ["python"]
802
1066
  },
803
- openvino: {
804
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"),
805
- sizeMB: 6,
806
- runtimes: ["python"]
807
- }
1067
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
808
1068
  }
809
1069
  },
810
1070
  {
@@ -829,11 +1089,7 @@ var OBJECT_DETECTION_MODELS = [
829
1089
  files: [...MLPACKAGE_FILES],
830
1090
  runtimes: ["python"]
831
1091
  },
832
- openvino: {
833
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"),
834
- sizeMB: 16,
835
- runtimes: ["python"]
836
- }
1092
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
837
1093
  }
838
1094
  },
839
1095
  {
@@ -858,11 +1114,7 @@ var OBJECT_DETECTION_MODELS = [
858
1114
  files: [...MLPACKAGE_FILES],
859
1115
  runtimes: ["python"]
860
1116
  },
861
- openvino: {
862
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"),
863
- sizeMB: 49,
864
- runtimes: ["python"]
865
- }
1117
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
866
1118
  }
867
1119
  },
868
1120
  {
@@ -887,11 +1139,7 @@ var OBJECT_DETECTION_MODELS = [
887
1139
  files: [...MLPACKAGE_FILES],
888
1140
  runtimes: ["python"]
889
1141
  },
890
- openvino: {
891
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"),
892
- sizeMB: 9,
893
- runtimes: ["python"]
894
- }
1142
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
895
1143
  }
896
1144
  },
897
1145
  {
@@ -916,11 +1164,7 @@ var OBJECT_DETECTION_MODELS = [
916
1164
  files: [...MLPACKAGE_FILES],
917
1165
  runtimes: ["python"]
918
1166
  },
919
- openvino: {
920
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"),
921
- sizeMB: 36,
922
- runtimes: ["python"]
923
- }
1167
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
924
1168
  }
925
1169
  },
926
1170
  {
@@ -945,11 +1189,7 @@ var OBJECT_DETECTION_MODELS = [
945
1189
  files: [...MLPACKAGE_FILES],
946
1190
  runtimes: ["python"]
947
1191
  },
948
- openvino: {
949
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"),
950
- sizeMB: 78,
951
- runtimes: ["python"]
952
- }
1192
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
953
1193
  }
954
1194
  },
955
1195
  {
@@ -974,11 +1214,7 @@ var OBJECT_DETECTION_MODELS = [
974
1214
  files: [...MLPACKAGE_FILES],
975
1215
  runtimes: ["python"]
976
1216
  },
977
- openvino: {
978
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"),
979
- sizeMB: 95,
980
- runtimes: ["python"]
981
- }
1217
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
982
1218
  }
983
1219
  },
984
1220
  {
@@ -1003,11 +1239,7 @@ var OBJECT_DETECTION_MODELS = [
1003
1239
  files: [...MLPACKAGE_FILES],
1004
1240
  runtimes: ["python"]
1005
1241
  },
1006
- openvino: {
1007
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"),
1008
- sizeMB: 213,
1009
- runtimes: ["python"]
1010
- }
1242
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
1011
1243
  }
1012
1244
  },
1013
1245
  {
@@ -1032,11 +1264,7 @@ var OBJECT_DETECTION_MODELS = [
1032
1264
  files: [...MLPACKAGE_FILES],
1033
1265
  runtimes: ["python"]
1034
1266
  },
1035
- openvino: {
1036
- url: hfScrypted("openvino/scrypted_yolov9t_relu/best.xml"),
1037
- sizeMB: 6,
1038
- runtimes: ["python"]
1039
- }
1267
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
1040
1268
  }
1041
1269
  },
1042
1270
  {
@@ -1061,11 +1289,7 @@ var OBJECT_DETECTION_MODELS = [
1061
1289
  files: [...MLPACKAGE_FILES],
1062
1290
  runtimes: ["python"]
1063
1291
  },
1064
- openvino: {
1065
- url: hfScrypted("openvino/scrypted_yolov9s_relu/best.xml"),
1066
- sizeMB: 16,
1067
- runtimes: ["python"]
1068
- }
1292
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
1069
1293
  }
1070
1294
  },
1071
1295
  {
@@ -1090,11 +1314,7 @@ var OBJECT_DETECTION_MODELS = [
1090
1314
  files: [...MLPACKAGE_FILES],
1091
1315
  runtimes: ["python"]
1092
1316
  },
1093
- openvino: {
1094
- url: hfScrypted("openvino/scrypted_yolov9c_relu/best.xml"),
1095
- sizeMB: 49,
1096
- runtimes: ["python"]
1097
- }
1317
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
1098
1318
  }
1099
1319
  },
1100
1320
  {
@@ -1119,11 +1339,7 @@ var OBJECT_DETECTION_MODELS = [
1119
1339
  files: [...MLPACKAGE_FILES],
1120
1340
  runtimes: ["python"]
1121
1341
  },
1122
- openvino: {
1123
- url: hfScrypted("openvino/scrypted_yolov9m_relu/best.xml"),
1124
- sizeMB: 38,
1125
- runtimes: ["python"]
1126
- }
1342
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
1127
1343
  }
1128
1344
  }
1129
1345
  ];
@@ -1152,11 +1368,7 @@ var FACE_DETECTION_MODELS = [{
1152
1368
  files: [...MLPACKAGE_FILES],
1153
1369
  runtimes: ["python"]
1154
1370
  },
1155
- openvino: {
1156
- url: hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"),
1157
- sizeMB: 1.8,
1158
- runtimes: ["python"]
1159
- }
1371
+ openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
1160
1372
  }
1161
1373
  }, {
1162
1374
  id: "scrypted-yolov9t-face",
@@ -1183,11 +1395,7 @@ var FACE_DETECTION_MODELS = [{
1183
1395
  files: [...MLPACKAGE_FILES],
1184
1396
  runtimes: ["python"]
1185
1397
  },
1186
- openvino: {
1187
- url: hfScrypted("openvino/scrypted_yolov9t_relu_face/best.xml"),
1188
- sizeMB: 6,
1189
- runtimes: ["python"]
1190
- }
1398
+ openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
1191
1399
  }
1192
1400
  }];
1193
1401
  var FACE_EMBEDDING_MODELS = [{
@@ -1217,11 +1425,7 @@ var FACE_EMBEDDING_MODELS = [{
1217
1425
  files: [...MLPACKAGE_FILES],
1218
1426
  runtimes: ["python"]
1219
1427
  },
1220
- openvino: {
1221
- url: hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"),
1222
- sizeMB: 65,
1223
- runtimes: ["python"]
1224
- }
1428
+ openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
1225
1429
  }
1226
1430
  }, {
1227
1431
  id: "inception-resnet-v1",
@@ -1248,11 +1452,7 @@ var FACE_EMBEDDING_MODELS = [{
1248
1452
  files: [...MLPACKAGE_FILES],
1249
1453
  runtimes: ["python"]
1250
1454
  },
1251
- openvino: {
1252
- url: hfScrypted("openvino/inception_resnet_v1/best.xml"),
1253
- sizeMB: 45,
1254
- runtimes: ["python"]
1255
- }
1455
+ openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
1256
1456
  }
1257
1457
  }];
1258
1458
  var PLATE_DETECTION_MODELS = [{
@@ -1280,11 +1480,7 @@ var PLATE_DETECTION_MODELS = [{
1280
1480
  files: [...MLPACKAGE_FILES],
1281
1481
  runtimes: ["python"]
1282
1482
  },
1283
- openvino: {
1284
- url: hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"),
1285
- sizeMB: 6.1,
1286
- runtimes: ["python"]
1287
- }
1483
+ openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
1288
1484
  }
1289
1485
  }];
1290
1486
  var PLATE_OCR_MODELS = [{
@@ -1312,11 +1508,7 @@ var PLATE_OCR_MODELS = [{
1312
1508
  files: [...MLPACKAGE_FILES],
1313
1509
  runtimes: ["python"]
1314
1510
  },
1315
- openvino: {
1316
- url: hfScrypted("openvino/vgg_english_g2/best.xml"),
1317
- sizeMB: 7.2,
1318
- runtimes: ["python"]
1319
- }
1511
+ openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
1320
1512
  }
1321
1513
  }];
1322
1514
  var ANIMAL_CLASSIFIER_MODELS = [{
@@ -1345,11 +1537,7 @@ var ANIMAL_CLASSIFIER_MODELS = [{
1345
1537
  files: [...MLPACKAGE_FILES],
1346
1538
  runtimes: ["python"]
1347
1539
  },
1348
- openvino: {
1349
- url: hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"),
1350
- sizeMB: 164,
1351
- runtimes: ["python"]
1352
- }
1540
+ openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
1353
1541
  }
1354
1542
  }];
1355
1543
  var BIRD_CLASSIFIER_MODELS = [{
@@ -1378,11 +1566,7 @@ var BIRD_CLASSIFIER_MODELS = [{
1378
1566
  files: [...MLPACKAGE_FILES],
1379
1567
  runtimes: ["python"]
1380
1568
  },
1381
- openvino: {
1382
- url: hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"),
1383
- sizeMB: 47,
1384
- runtimes: ["python"]
1385
- }
1569
+ openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
1386
1570
  },
1387
1571
  extraFiles: [{
1388
1572
  url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
@@ -1416,11 +1600,7 @@ var VEHICLE_CLASSIFIER_MODELS = [{
1416
1600
  files: [...MLPACKAGE_FILES],
1417
1601
  runtimes: ["python"]
1418
1602
  },
1419
- openvino: {
1420
- url: hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"),
1421
- sizeMB: 68,
1422
- runtimes: ["python"]
1423
- }
1603
+ openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
1424
1604
  },
1425
1605
  extraFiles: [{
1426
1606
  url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
@@ -1453,11 +1633,7 @@ var SEGMENTATION_REFINER_MODELS = [{
1453
1633
  files: [...MLPACKAGE_FILES],
1454
1634
  runtimes: ["python"]
1455
1635
  },
1456
- openvino: {
1457
- url: hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"),
1458
- sizeMB: 2.5,
1459
- runtimes: ["python"]
1460
- }
1636
+ openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
1461
1637
  }
1462
1638
  }];
1463
1639
  var INSTANCE_SEGMENTATION_MODELS = [
@@ -1483,11 +1659,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1483
1659
  files: [...MLPACKAGE_FILES],
1484
1660
  runtimes: ["python"]
1485
1661
  },
1486
- openvino: {
1487
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"),
1488
- sizeMB: 11,
1489
- runtimes: ["python"]
1490
- }
1662
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
1491
1663
  }
1492
1664
  },
1493
1665
  {
@@ -1512,11 +1684,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1512
1684
  files: [...MLPACKAGE_FILES],
1513
1685
  runtimes: ["python"]
1514
1686
  },
1515
- openvino: {
1516
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"),
1517
- sizeMB: 40,
1518
- runtimes: ["python"]
1519
- }
1687
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
1520
1688
  }
1521
1689
  },
1522
1690
  {
@@ -1541,11 +1709,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1541
1709
  files: [...MLPACKAGE_FILES],
1542
1710
  runtimes: ["python"]
1543
1711
  },
1544
- openvino: {
1545
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"),
1546
- sizeMB: 90,
1547
- runtimes: ["python"]
1548
- }
1712
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
1549
1713
  }
1550
1714
  }
1551
1715
  ];
@@ -1561,16 +1725,12 @@ var AUDIO_CLASSIFIER_MODELS = [{
1561
1725
  preprocessMode: "resize",
1562
1726
  formats: {
1563
1727
  onnx: {
1564
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1728
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1565
1729
  sizeMB: 3.2
1566
1730
  },
1567
- openvino: {
1568
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1569
- sizeMB: 3.2,
1570
- runtimes: ["python"]
1571
- },
1731
+ openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
1572
1732
  coreml: {
1573
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1733
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1574
1734
  sizeMB: 3.2,
1575
1735
  runtimes: ["python"]
1576
1736
  }
@@ -2158,71 +2318,174 @@ var EngineFactory = class {
2158
2318
  }
2159
2319
  };
2160
2320
  //#endregion
2161
- //#region src/detection-pipeline/postprocess/dispatch.ts
2162
- var VALID_KINDS = new Set([
2163
- "detections",
2164
- "classifications",
2165
- "embedding",
2166
- "text",
2167
- "mask"
2168
- ]);
2169
- /**
2170
- * Type guard: validates that a structured payload from the Python pool is a
2171
- * well-formed StepOutput discriminated union.
2172
- *
2173
- * The Python inference_pool.py always sets `output.structured` with a `kind`
2174
- * field. This guard narrows `Record<string, unknown>` to `StepOutput` without
2175
- * resorting to double-cast.
2176
- */
2177
- function isStepOutput(value) {
2178
- return typeof value === "object" && value !== null && typeof value["kind"] === "string" && VALID_KINDS.has(value["kind"]);
2179
- }
2180
- /**
2181
- * Return the structured payload as `StepOutput`, or null if it fails validation.
2182
- * Callers must handle null by proceeding to raw-tensor postprocessing.
2183
- */
2184
- function tryStructured(output) {
2185
- if (!output.structured) return null;
2186
- if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
2187
- return output.structured;
2188
- }
2189
- function postprocessYolo(output, stepDef) {
2190
- const structured = tryStructured(output);
2191
- if (structured) return structured;
2192
- const tensor = output.tensor;
2193
- if (!tensor) throw new Error("YOLO postprocessor: no tensor in engine output");
2194
- const labels = stepDef.labels ?? [];
2195
- const numClasses = labels.length || 80;
2196
- const numBoxes = tensor.length / (4 + numClasses);
2197
- const letterbox = output.letterbox;
2198
- const dets = [];
2199
- for (let i = 0; i < numBoxes; i++) {
2200
- const cx = tensor[0 * numBoxes + i];
2201
- const cy = tensor[1 * numBoxes + i];
2202
- const w = tensor[2 * numBoxes + i];
2203
- const h = tensor[3 * numBoxes + i];
2204
- let bestScore = -Infinity;
2205
- let bestClass = 0;
2206
- for (let j = 0; j < numClasses; j++) {
2207
- const score = tensor[(4 + j) * numBoxes + i];
2208
- if (score > bestScore) {
2209
- bestScore = score;
2210
- bestClass = j;
2211
- }
2212
- }
2213
- if (bestScore <= 0) continue;
2214
- let x1 = cx - w / 2;
2215
- let y1 = cy - h / 2;
2216
- let x2 = cx + w / 2;
2217
- let y2 = cy + h / 2;
2218
- if (letterbox) {
2219
- x1 = (x1 - letterbox.padX) / letterbox.scale;
2220
- y1 = (y1 - letterbox.padY) / letterbox.scale;
2221
- x2 = (x2 - letterbox.padX) / letterbox.scale;
2222
- y2 = (y2 - letterbox.padY) / letterbox.scale;
2223
- }
2224
- const label = labels[bestClass] ?? String(bestClass);
2225
- dets.push({
2321
+ //#region src/detection-pipeline/engine-provisioner.ts
2322
+ /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
2323
+ var BACKOFF_SCHEDULE_MS = [
2324
+ 5e3,
2325
+ 15e3,
2326
+ 3e4,
2327
+ 6e4,
2328
+ 12e4,
2329
+ 3e5
2330
+ ];
2331
+ var IDLE_STATE = {
2332
+ runtimeId: null,
2333
+ device: null,
2334
+ state: "idle"
2335
+ };
2336
+ var EngineProvisioner = class {
2337
+ fx;
2338
+ current = IDLE_STATE;
2339
+ /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
2340
+ generation = 0;
2341
+ cancelTimer = null;
2342
+ retryIndex = 0;
2343
+ constructor(fx) {
2344
+ this.fx = fx;
2345
+ }
2346
+ get state() {
2347
+ return this.current;
2348
+ }
2349
+ isReady() {
2350
+ return this.current.state === "ready";
2351
+ }
2352
+ select(runtimeId, device) {
2353
+ this.generation++;
2354
+ this.retryIndex = 0;
2355
+ this.clearTimer();
2356
+ this.transition({
2357
+ runtimeId,
2358
+ device,
2359
+ state: "installing",
2360
+ progress: 0
2361
+ });
2362
+ this.provision(this.generation, runtimeId, device);
2363
+ }
2364
+ dispose() {
2365
+ this.generation++;
2366
+ this.clearTimer();
2367
+ }
2368
+ clearTimer() {
2369
+ if (this.cancelTimer !== null) {
2370
+ this.cancelTimer();
2371
+ this.cancelTimer = null;
2372
+ }
2373
+ }
2374
+ transition(next) {
2375
+ this.current = next;
2376
+ this.fx.onChange(next);
2377
+ }
2378
+ async provision(gen, runtimeId, device) {
2379
+ try {
2380
+ await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
2381
+ if (gen !== this.generation) return;
2382
+ this.transition({
2383
+ runtimeId,
2384
+ device,
2385
+ state: "verifying",
2386
+ progress: 100
2387
+ });
2388
+ await this.fx.verify(runtimeId, device);
2389
+ if (gen !== this.generation) return;
2390
+ this.retryIndex = 0;
2391
+ this.transition({
2392
+ runtimeId,
2393
+ device,
2394
+ state: "ready"
2395
+ });
2396
+ } catch (err) {
2397
+ if (gen !== this.generation) return;
2398
+ const message = err instanceof Error ? err.message : String(err);
2399
+ const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
2400
+ this.retryIndex++;
2401
+ const nextRetryAt = this.fx.now() + delay;
2402
+ this.transition({
2403
+ runtimeId,
2404
+ device,
2405
+ state: "failed",
2406
+ error: message,
2407
+ nextRetryAt
2408
+ });
2409
+ this.cancelTimer = this.fx.setTimer(delay, () => {
2410
+ if (gen !== this.generation) return;
2411
+ this.cancelTimer = null;
2412
+ this.transition({
2413
+ runtimeId,
2414
+ device,
2415
+ state: "installing",
2416
+ progress: 0
2417
+ });
2418
+ this.provision(gen, runtimeId, device);
2419
+ });
2420
+ }
2421
+ }
2422
+ };
2423
+ //#endregion
2424
+ //#region src/detection-pipeline/postprocess/dispatch.ts
2425
+ var VALID_KINDS = new Set([
2426
+ "detections",
2427
+ "classifications",
2428
+ "embedding",
2429
+ "text",
2430
+ "mask"
2431
+ ]);
2432
+ /**
2433
+ * Type guard: validates that a structured payload from the Python pool is a
2434
+ * well-formed StepOutput discriminated union.
2435
+ *
2436
+ * The Python inference_pool.py always sets `output.structured` with a `kind`
2437
+ * field. This guard narrows `Record<string, unknown>` to `StepOutput` without
2438
+ * resorting to double-cast.
2439
+ */
2440
+ function isStepOutput(value) {
2441
+ return typeof value === "object" && value !== null && typeof value["kind"] === "string" && VALID_KINDS.has(value["kind"]);
2442
+ }
2443
+ /**
2444
+ * Return the structured payload as `StepOutput`, or null if it fails validation.
2445
+ * Callers must handle null by proceeding to raw-tensor postprocessing.
2446
+ */
2447
+ function tryStructured(output) {
2448
+ if (!output.structured) return null;
2449
+ if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
2450
+ return output.structured;
2451
+ }
2452
+ function postprocessYolo(output, stepDef) {
2453
+ const structured = tryStructured(output);
2454
+ if (structured) return structured;
2455
+ const tensor = output.tensor;
2456
+ if (!tensor) throw new Error("YOLO postprocessor: no tensor in engine output");
2457
+ const labels = stepDef.labels ?? [];
2458
+ const numClasses = labels.length || 80;
2459
+ const numBoxes = tensor.length / (4 + numClasses);
2460
+ const letterbox = output.letterbox;
2461
+ const dets = [];
2462
+ for (let i = 0; i < numBoxes; i++) {
2463
+ const cx = tensor[i];
2464
+ const cy = tensor[1 * numBoxes + i];
2465
+ const w = tensor[2 * numBoxes + i];
2466
+ const h = tensor[3 * numBoxes + i];
2467
+ let bestScore = -Infinity;
2468
+ let bestClass = 0;
2469
+ for (let j = 0; j < numClasses; j++) {
2470
+ const score = tensor[(4 + j) * numBoxes + i];
2471
+ if (score > bestScore) {
2472
+ bestScore = score;
2473
+ bestClass = j;
2474
+ }
2475
+ }
2476
+ if (bestScore <= 0) continue;
2477
+ let x1 = cx - w / 2;
2478
+ let y1 = cy - h / 2;
2479
+ let x2 = cx + w / 2;
2480
+ let y2 = cy + h / 2;
2481
+ if (letterbox) {
2482
+ x1 = (x1 - letterbox.padX) / letterbox.scale;
2483
+ y1 = (y1 - letterbox.padY) / letterbox.scale;
2484
+ x2 = (x2 - letterbox.padX) / letterbox.scale;
2485
+ y2 = (y2 - letterbox.padY) / letterbox.scale;
2486
+ }
2487
+ const label = labels[bestClass] ?? String(bestClass);
2488
+ dets.push({
2226
2489
  class: label,
2227
2490
  score: bestScore,
2228
2491
  bbox: [
@@ -2353,7 +2616,7 @@ function postprocessArcface(output, _stepDef) {
2353
2616
  let sumSq = 0;
2354
2617
  for (let i = 0; i < tensor.length; i++) sumSq += tensor[i] * tensor[i];
2355
2618
  const norm = Math.sqrt(sumSq);
2356
- const normalized = new Array(tensor.length);
2619
+ const normalized = Array.from({ length: tensor.length });
2357
2620
  for (let i = 0; i < tensor.length; i++) normalized[i] = norm === 0 ? 0 : tensor[i] / norm;
2358
2621
  return {
2359
2622
  kind: "embedding",
@@ -3389,403 +3652,143 @@ var PipelineExecutor = class {
3389
3652
  return output;
3390
3653
  }
3391
3654
  async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
3392
- for (const child of children) {
3393
- if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
3394
- const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
3395
- if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
3396
- if (isBboxDegenerate(parentDetection.bbox)) continue;
3397
- try {
3398
- const childStart = Date.now();
3399
- const fullFrameJpeg = await fullFrameJpegProvider();
3400
- const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
3401
- let cropJpegBuf;
3402
- let cropW;
3403
- let cropH;
3404
- if (modelEntry?.faceAlignment) {
3405
- const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
3406
- if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
3407
- const lms = parentDetection.landmarks;
3408
- if (lms && lms.length >= 5) {
3409
- const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
3410
- cropJpegBuf = aligned.jpeg;
3411
- cropW = aligned.width;
3412
- cropH = aligned.height;
3413
- } else {
3414
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3415
- cropJpegBuf = crop.jpeg;
3416
- cropW = crop.width;
3417
- cropH = crop.height;
3418
- }
3419
- } else {
3420
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3421
- cropJpegBuf = crop.jpeg;
3422
- cropW = crop.width;
3423
- cropH = crop.height;
3424
- }
3425
- const childOutput = await this.executeStep(child, {
3426
- kind: "jpeg",
3427
- data: cropJpegBuf
3428
- }, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
3429
- const childMs = Date.now() - childStart;
3430
- const detailsBefore = ctx.details.length;
3431
- applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
3432
- if (childOutput.kind === "detections" && child.children.length > 0) {
3433
- const newDetails = ctx.details.slice(detailsBefore);
3434
- for (const detail of newDetails) {
3435
- detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
3436
- if (detail.landmarks) {
3437
- const [px1, py1] = parentDetection.bbox;
3438
- detail.landmarks = detail.landmarks.map((l) => ({
3439
- x: l.x + px1,
3440
- y: l.y + py1
3441
- }));
3442
- }
3443
- await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
3444
- }
3445
- }
3446
- } catch {}
3447
- }
3448
- }
3449
- matchesInputClasses(className, inputClasses) {
3450
- if (inputClasses.length === 0) return true;
3451
- return inputClasses.includes(className);
3452
- }
3453
- /**
3454
- * Apply the object-detection step's macro filter + per-macro
3455
- * minConfidence sliders introduced in the Phase 6 step rework.
3456
- *
3457
- * Expected `settings` shape:
3458
- * - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
3459
- * Empty array = all allowed (legacy behaviour).
3460
- * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
3461
- */
3462
- matchesMacroFilter(macroClass, score, settings) {
3463
- if (!settings) return true;
3464
- const enabled = settings["enabledMacroClasses"];
3465
- if (Array.isArray(enabled) && enabled.length > 0) {
3466
- if (!enabled.includes(macroClass)) return false;
3467
- }
3468
- const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3469
- if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3470
- return true;
3471
- }
3472
- };
3473
- function capitalize(s) {
3474
- if (s.length === 0) return s;
3475
- return s.charAt(0).toUpperCase() + s.slice(1);
3476
- }
3477
- //#endregion
3478
- //#region src/detection-pipeline/pipeline/tree-builder.ts
3479
- /**
3480
- * Build an executable tree from user config.
3481
- *
3482
- * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3483
- * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3484
- * Throws if step not loaded.
3485
- */
3486
- function buildExecutableTree(steps, getEngine) {
3487
- return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3488
- }
3489
- function buildNode(step, getEngine) {
3490
- const definition = getStepDefinition(step.addonId);
3491
- const engine = getEngine(step.addonId);
3492
- const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3493
- const mergedSettings = {
3494
- ...collectSchemaDefaults(step.addonId),
3495
- ...step.settings
3496
- };
3497
- return {
3498
- stepId: step.addonId,
3499
- definition,
3500
- engine,
3501
- modelId: step.modelId,
3502
- inputClasses: definition.inputClasses ?? [],
3503
- enabled: step.enabled,
3504
- children,
3505
- ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3506
- };
3507
- }
3508
- /**
3509
- * Walk the step's declared `getConfigSchema()` and collect every field
3510
- * with a primitive `default` value into a plain record. Group fields
3511
- * are flattened — a nested `{ type: 'group', fields: [...] }` is
3512
- * entered recursively. Fields without a declared default are skipped.
3513
- */
3514
- function collectSchemaDefaults(stepId) {
3515
- const schema = getStep(stepId).getConfigSchema();
3516
- const out = {};
3517
- walkFieldsForDefaults(schema, out);
3518
- return out;
3519
- }
3520
- function walkFieldsForDefaults(fields, out) {
3521
- for (const field of fields) {
3522
- if ("type" in field && field.type === "group" && "fields" in field) {
3523
- walkFieldsForDefaults(field.fields, out);
3524
- continue;
3525
- }
3526
- if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3527
- }
3528
- }
3529
- //#endregion
3530
- //#region src/detection-pipeline/runtimes.ts
3531
- var AUTO = {
3532
- value: "auto",
3533
- label: "Auto"
3534
- };
3535
- var CPU = {
3536
- value: "cpu",
3537
- label: "CPU"
3538
- };
3539
- var RUNTIMES = [
3540
- {
3541
- id: "onnx",
3542
- label: "ONNX Runtime",
3543
- supports: () => true,
3544
- devices: (hw) => {
3545
- if (!hw) return [CPU];
3546
- const out = [CPU];
3547
- if (hw.gpu?.type === "nvidia") out.push({
3548
- value: "cuda",
3549
- label: "CUDA"
3550
- });
3551
- if (hw.npu?.type === "apple-ane") out.push({
3552
- value: "coreml",
3553
- label: "CoreML EP"
3554
- });
3555
- return out;
3556
- },
3557
- defaultDevice: "cpu",
3558
- modelFormat: "onnx",
3559
- pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3560
- tuning: {
3561
- concurrency: 4,
3562
- batchMode: "list",
3563
- maxBatchSize: 8,
3564
- intraOpThreads: 0
3565
- }
3566
- },
3567
- {
3568
- id: "openvino",
3569
- label: "OpenVINO",
3570
- supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
3571
- devices: (hw) => {
3572
- if (!hw) return [AUTO];
3573
- const out = [AUTO, CPU];
3574
- if (hw.gpu?.type === "intel") out.push({
3575
- value: "gpu",
3576
- label: "GPU"
3577
- });
3578
- if (hw.npu?.type === "intel-npu") out.push({
3579
- value: "npu",
3580
- label: "NPU"
3581
- });
3582
- return out;
3583
- },
3584
- defaultDevice: "auto",
3585
- modelFormat: "openvino",
3586
- pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3587
- tuning: {
3588
- concurrency: 1,
3589
- batchMode: "none",
3590
- numStreams: 0
3591
- }
3592
- },
3593
- {
3594
- id: "coreml",
3595
- label: "CoreML",
3596
- supports: (env) => env.platform === "darwin",
3597
- devices: (hw) => {
3598
- const all = {
3599
- value: "all",
3600
- label: "All (ANE + GPU + CPU)"
3601
- };
3602
- if (!hw) return [all];
3603
- const out = [all];
3604
- if (hw.npu?.type === "apple-ane") out.push({
3605
- value: "ane",
3606
- label: "Apple Neural Engine"
3607
- });
3608
- out.push({
3609
- value: "gpu",
3610
- label: "GPU"
3611
- }, CPU);
3612
- return out;
3613
- },
3614
- defaultDevice: "all",
3615
- modelFormat: "coreml",
3616
- pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3617
- tuning: {
3618
- concurrency: 1,
3619
- batchMode: "none",
3620
- windowMs: 8,
3621
- maxBatchSize: 8,
3622
- numWorkers: 1
3623
- }
3624
- }
3625
- ];
3626
- function def(id) {
3627
- const d = RUNTIMES.find((r) => r.id === id);
3628
- if (!d) throw new Error(`Unknown runtime: ${id}`);
3629
- return d;
3630
- }
3631
- function supportedRuntimes(env) {
3632
- return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3633
- }
3634
- function runtimeDevices(id, hardware) {
3635
- return def(id).devices(hardware);
3636
- }
3637
- function defaultDeviceFor(id) {
3638
- return def(id).defaultDevice;
3639
- }
3640
- function pythonRequirementsFor(id) {
3641
- return def(id).pythonRequirements;
3642
- }
3643
- function modelFormatFor(id) {
3644
- return def(id).modelFormat;
3645
- }
3646
- function tuningFor(id) {
3647
- return def(id).tuning;
3648
- }
3649
- function runtimeLabel(id) {
3650
- return def(id).label;
3651
- }
3652
- /**
3653
- * Proactive-install hint kept for back-compat (re-exported from index.ts).
3654
- * Intel on Linux warrants installing openvino; coremltools covers macOS;
3655
- * openvino has no AMD/NVIDIA backend.
3656
- */
3657
- function shouldInstallOpenvino(env) {
3658
- if (env.platform === "darwin") return false;
3659
- return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3660
- }
3661
- //#endregion
3662
- //#region src/detection-pipeline/engine-provisioner.ts
3663
- /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3664
- var BACKOFF_SCHEDULE_MS = [
3665
- 5e3,
3666
- 15e3,
3667
- 3e4,
3668
- 6e4,
3669
- 12e4,
3670
- 3e5
3671
- ];
3672
- var IDLE_STATE = {
3673
- runtimeId: null,
3674
- device: null,
3675
- state: "idle"
3676
- };
3677
- var EngineProvisioner = class {
3678
- fx;
3679
- current = IDLE_STATE;
3680
- /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
3681
- generation = 0;
3682
- cancelTimer = null;
3683
- retryIndex = 0;
3684
- constructor(fx) {
3685
- this.fx = fx;
3686
- }
3687
- get state() {
3688
- return this.current;
3689
- }
3690
- isReady() {
3691
- return this.current.state === "ready";
3692
- }
3693
- select(runtimeId, device) {
3694
- this.generation++;
3695
- this.retryIndex = 0;
3696
- this.clearTimer();
3697
- this.transition({
3698
- runtimeId,
3699
- device,
3700
- state: "installing",
3701
- progress: 0
3702
- });
3703
- this.provision(this.generation, runtimeId, device);
3704
- }
3705
- dispose() {
3706
- this.generation++;
3707
- this.clearTimer();
3708
- }
3709
- clearTimer() {
3710
- if (this.cancelTimer !== null) {
3711
- this.cancelTimer();
3712
- this.cancelTimer = null;
3655
+ for (const child of children) {
3656
+ if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
3657
+ const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
3658
+ if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
3659
+ if (isBboxDegenerate(parentDetection.bbox)) continue;
3660
+ try {
3661
+ const childStart = Date.now();
3662
+ const fullFrameJpeg = await fullFrameJpegProvider();
3663
+ const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
3664
+ let cropJpegBuf;
3665
+ let cropW;
3666
+ let cropH;
3667
+ if (modelEntry?.faceAlignment) {
3668
+ const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
3669
+ if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
3670
+ const lms = parentDetection.landmarks;
3671
+ if (lms && lms.length >= 5) {
3672
+ const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
3673
+ cropJpegBuf = aligned.jpeg;
3674
+ cropW = aligned.width;
3675
+ cropH = aligned.height;
3676
+ } else {
3677
+ const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3678
+ cropJpegBuf = crop.jpeg;
3679
+ cropW = crop.width;
3680
+ cropH = crop.height;
3681
+ }
3682
+ } else {
3683
+ const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3684
+ cropJpegBuf = crop.jpeg;
3685
+ cropW = crop.width;
3686
+ cropH = crop.height;
3687
+ }
3688
+ const childOutput = await this.executeStep(child, {
3689
+ kind: "jpeg",
3690
+ data: cropJpegBuf
3691
+ }, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
3692
+ const childMs = Date.now() - childStart;
3693
+ const detailsBefore = ctx.details.length;
3694
+ applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
3695
+ if (childOutput.kind === "detections" && child.children.length > 0) {
3696
+ const newDetails = ctx.details.slice(detailsBefore);
3697
+ for (const detail of newDetails) {
3698
+ detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
3699
+ if (detail.landmarks) {
3700
+ const [px1, py1] = parentDetection.bbox;
3701
+ detail.landmarks = detail.landmarks.map((l) => ({
3702
+ x: l.x + px1,
3703
+ y: l.y + py1
3704
+ }));
3705
+ }
3706
+ await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
3707
+ }
3708
+ }
3709
+ } catch {}
3713
3710
  }
3714
3711
  }
3715
- transition(next) {
3716
- this.current = next;
3717
- this.fx.onChange(next);
3712
+ matchesInputClasses(className, inputClasses) {
3713
+ if (inputClasses.length === 0) return true;
3714
+ return inputClasses.includes(className);
3718
3715
  }
3719
- async provision(gen, runtimeId, device) {
3720
- try {
3721
- await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
3722
- if (gen !== this.generation) return;
3723
- this.transition({
3724
- runtimeId,
3725
- device,
3726
- state: "verifying",
3727
- progress: 100
3728
- });
3729
- await this.fx.verify(runtimeId, device);
3730
- if (gen !== this.generation) return;
3731
- this.retryIndex = 0;
3732
- this.transition({
3733
- runtimeId,
3734
- device,
3735
- state: "ready"
3736
- });
3737
- } catch (err) {
3738
- if (gen !== this.generation) return;
3739
- const message = err instanceof Error ? err.message : String(err);
3740
- const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
3741
- this.retryIndex++;
3742
- const nextRetryAt = this.fx.now() + delay;
3743
- this.transition({
3744
- runtimeId,
3745
- device,
3746
- state: "failed",
3747
- error: message,
3748
- nextRetryAt
3749
- });
3750
- this.cancelTimer = this.fx.setTimer(delay, () => {
3751
- if (gen !== this.generation) return;
3752
- this.cancelTimer = null;
3753
- this.transition({
3754
- runtimeId,
3755
- device,
3756
- state: "installing",
3757
- progress: 0
3758
- });
3759
- this.provision(gen, runtimeId, device);
3760
- });
3716
+ /**
3717
+ * Apply the object-detection step's macro filter + per-macro
3718
+ * minConfidence sliders introduced in the Phase 6 step rework.
3719
+ *
3720
+ * Expected `settings` shape:
3721
+ * - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
3722
+ * Empty array = all allowed (legacy behaviour).
3723
+ * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
3724
+ */
3725
+ matchesMacroFilter(macroClass, score, settings) {
3726
+ if (!settings) return true;
3727
+ const enabled = settings["enabledMacroClasses"];
3728
+ if (Array.isArray(enabled) && enabled.length > 0) {
3729
+ if (!enabled.includes(macroClass)) return false;
3761
3730
  }
3731
+ const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3732
+ if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3733
+ return true;
3762
3734
  }
3763
3735
  };
3736
+ function capitalize(s) {
3737
+ if (s.length === 0) return s;
3738
+ return s.charAt(0).toUpperCase() + s.slice(1);
3739
+ }
3764
3740
  //#endregion
3765
- //#region src/detection-pipeline/auto-pick.ts
3766
- var PREFERENCE = [
3767
- "coreml",
3768
- "openvino",
3769
- "onnx"
3770
- ];
3741
+ //#region src/detection-pipeline/pipeline/tree-builder.ts
3771
3742
  /**
3772
- * Pure function picks the best supported runtime for the given hardware env.
3773
- *
3774
- * Logic:
3775
- * 1. If `bestBackendHint` is in the supported set, use it.
3776
- * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
3777
- * 3. Floor to `'onnx'` (always supported).
3743
+ * Build an executable tree from user config.
3778
3744
  *
3779
- * Device is always `defaultDeviceFor(chosen)`.
3745
+ * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3746
+ * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3747
+ * Throws if step not loaded.
3780
3748
  */
3781
- function pickBestRuntime(env, bestBackendHint) {
3782
- const supported = supportedRuntimes(env);
3783
- const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
3749
+ function buildExecutableTree(steps, getEngine) {
3750
+ return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3751
+ }
3752
+ function buildNode(step, getEngine) {
3753
+ const definition = getStepDefinition(step.addonId);
3754
+ const engine = getEngine(step.addonId);
3755
+ const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3756
+ const mergedSettings = {
3757
+ ...collectSchemaDefaults(step.addonId),
3758
+ ...step.settings
3759
+ };
3784
3760
  return {
3785
- runtimeId: chosen,
3786
- device: defaultDeviceFor(chosen)
3761
+ stepId: step.addonId,
3762
+ definition,
3763
+ engine,
3764
+ modelId: step.modelId,
3765
+ inputClasses: definition.inputClasses ?? [],
3766
+ enabled: step.enabled,
3767
+ children,
3768
+ ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3787
3769
  };
3788
3770
  }
3771
+ /**
3772
+ * Walk the step's declared `getConfigSchema()` and collect every field
3773
+ * with a primitive `default` value into a plain record. Group fields
3774
+ * are flattened — a nested `{ type: 'group', fields: [...] }` is
3775
+ * entered recursively. Fields without a declared default are skipped.
3776
+ */
3777
+ function collectSchemaDefaults(stepId) {
3778
+ const schema = getStep(stepId).getConfigSchema();
3779
+ const out = {};
3780
+ walkFieldsForDefaults(schema, out);
3781
+ return out;
3782
+ }
3783
+ function walkFieldsForDefaults(fields, out) {
3784
+ for (const field of fields) {
3785
+ if ("type" in field && field.type === "group" && "fields" in field) {
3786
+ walkFieldsForDefaults(field.fields, out);
3787
+ continue;
3788
+ }
3789
+ if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3790
+ }
3791
+ }
3789
3792
  //#endregion
3790
3793
  //#region src/detection-pipeline/provider.ts
3791
3794
  /**
@@ -3988,6 +3991,27 @@ function toProbedHardware(hw) {
3988
3991
  gpu: hw.gpu ? { type: hw.gpu.type } : null
3989
3992
  };
3990
3993
  }
3994
+ var ONNX_FLOOR = {
3995
+ runtime: "python",
3996
+ backend: "onnx",
3997
+ format: "onnx",
3998
+ device: "cpu"
3999
+ };
4000
+ /**
4001
+ * Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
4002
+ * env. Used wherever the old `detectBestEngine()` sync probe fell back — the
4003
+ * result is identical (onnx / cpu) but is now derived through the shared rules
4004
+ * instead of duplicated inline.
4005
+ */
4006
+ function onnxFloorPick() {
4007
+ const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
4008
+ return {
4009
+ runtime: "python",
4010
+ backend: pick.runtimeId,
4011
+ format: modelFormatFor(pick.runtimeId),
4012
+ device: pick.device
4013
+ };
4014
+ }
3991
4015
  var DetectionPipelineProvider = class DetectionPipelineProvider {
3992
4016
  modelsDir;
3993
4017
  eventBus;
@@ -4046,6 +4070,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4046
4070
  */
4047
4071
  needsAutoPick = false;
4048
4072
  /**
4073
+ * Unsubscribe handle for the deferred-auto-pick `platform-probe` ready
4074
+ * listener (armed in `setApi` when the probe isn't ready yet). Cleared once
4075
+ * the engine is resolved — either by the listener firing or by the boot
4076
+ * safety-net `ensureBootEngineProvisioned`.
4077
+ */
4078
+ deferredAutoPickUnsub = null;
4079
+ /**
4049
4080
  * Warm cache for benchmark engine-override runs.
4050
4081
  *
4051
4082
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -4077,8 +4108,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4077
4108
  this.readStore = () => settings.readAddonStore();
4078
4109
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4079
4110
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4080
- this.currentEngine = DetectionPipelineProvider.detectBestEngine();
4081
- this.log.info("Engine selected (default)", { meta: {
4111
+ this.currentEngine = ONNX_FLOOR;
4112
+ this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4082
4113
  runtime: this.currentEngine.runtime,
4083
4114
  backend: this.currentEngine.backend,
4084
4115
  format: this.currentEngine.format
@@ -4143,69 +4174,78 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4143
4174
  isReady() {
4144
4175
  return this.ready;
4145
4176
  }
4146
- /** Detect the best inference engine for this platform. */
4147
- static detectBestEngine() {
4148
- const platform = process.platform;
4149
- const arch = process.arch;
4150
- if (platform === "darwin" && arch === "arm64") return {
4151
- runtime: "python",
4152
- backend: "coreml",
4153
- format: "coreml",
4154
- device: "all"
4155
- };
4156
- if (platform === "darwin") return {
4157
- runtime: "python",
4158
- backend: "coreml",
4159
- format: "coreml",
4160
- device: "gpu"
4161
- };
4162
- try {
4163
- const { execFileSync } = __require("node:child_process");
4164
- execFileSync("nvidia-smi", ["--query-gpu=name", "--format=csv,noheader"], {
4165
- timeout: 3e3,
4166
- stdio: "pipe"
4167
- });
4168
- return {
4169
- runtime: "python",
4170
- backend: "onnx",
4171
- format: "onnx",
4172
- device: "cuda"
4173
- };
4174
- } catch {}
4175
- try {
4176
- const fsmod = __require("node:fs");
4177
- const isIntel = __require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
4178
- const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
4179
- const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
4180
- if (isIntel && (hasIgpu || hasNpu)) return {
4181
- runtime: "python",
4182
- backend: "openvino",
4183
- format: "openvino",
4184
- device: "auto"
4185
- };
4186
- } catch {}
4187
- return {
4188
- runtime: "python",
4189
- backend: "onnx",
4190
- format: "onnx",
4191
- device: "cpu"
4192
- };
4193
- }
4194
4177
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
4195
4178
  async setApi(addonCtx) {
4196
4179
  this.addonCtx = addonCtx;
4197
- if (this.needsAutoPick) {
4180
+ if (this.needsAutoPick) if (this.addonCtx.useCapability("platform-probe").isReady) {
4198
4181
  await this.autoPickAndPersist();
4199
4182
  this.needsAutoPick = false;
4183
+ this.startProvisioningForCurrentEngine();
4184
+ } else {
4185
+ const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4186
+ if (state !== "ready") return;
4187
+ this.cancelDeferredAutoPick();
4188
+ if (!this.needsAutoPick) return;
4189
+ this.autoPickAndPersist().then(() => {
4190
+ this.needsAutoPick = false;
4191
+ this.startProvisioningForCurrentEngine();
4192
+ });
4193
+ });
4194
+ this.deferredAutoPickUnsub = unsubscribe;
4195
+ this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
4200
4196
  }
4197
+ else this.startProvisioningForCurrentEngine();
4198
+ }
4199
+ /** Tear down the deferred-auto-pick probe listener, if still armed. */
4200
+ cancelDeferredAutoPick() {
4201
+ if (this.deferredAutoPickUnsub) {
4202
+ this.deferredAutoPickUnsub();
4203
+ this.deferredAutoPickUnsub = null;
4204
+ }
4205
+ }
4206
+ /**
4207
+ * Boot safety-net: deterministically provision the hardware-probed engine.
4208
+ *
4209
+ * Called from the addon's `onInitialize` right after `reprobeEngine` has
4210
+ * written this node's probe-driven selection (e.g. `engineBackend=openvino`)
4211
+ * to the store. When first-boot auto-pick was DEFERRED (probe not ready at
4212
+ * `setApi`), the engine would otherwise stay `idle` — selected but never
4213
+ * provisioned — because the `onCapabilityStateChange('platform-probe')` ready
4214
+ * edge can be missed when the cap is already 'ready' by the time we subscribe
4215
+ * (the old eager-onnx-floor masked this; removing it surfaced a node that
4216
+ * boots with no engine at all). This loads the now-persisted selection and
4217
+ * starts provisioning it, so the node reliably comes up on its real best
4218
+ * engine (openvino on Intel) with no onnx floor and no missed boot. No-op
4219
+ * when provisioning already started (persisted-engine path / listener fired)
4220
+ * or when nothing has been selected yet.
4221
+ */
4222
+ async ensureBootEngineProvisioned() {
4223
+ if (this.getEngineProvisioning().state !== "idle") return;
4224
+ const stored = await this.loadEngine();
4225
+ if (!stored) return;
4226
+ this.currentEngine = stored;
4227
+ this.needsAutoPick = false;
4228
+ this.cancelDeferredAutoPick();
4229
+ this.log.info("Boot engine provisioning from probed selection", { meta: {
4230
+ runtime: stored.runtime,
4231
+ backend: stored.backend,
4232
+ device: stored.device ?? null
4233
+ } });
4201
4234
  this.startProvisioningForCurrentEngine();
4202
4235
  }
4203
4236
  /**
4204
4237
  * Auto-pick the best supported runtime at first boot (no stored engine).
4205
4238
  * Uses the platform-probe cap's hardware + bestScore hint when available;
4206
4239
  * falls back to platform/arch when the probe cap is not yet reachable.
4240
+ *
4207
4241
  * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4208
- * boots load it via `loadEngine()` and skip this path.
4242
+ * boots load it via `loadEngine()` and skip this path — but ONLY when the
4243
+ * probe actually answered (real `hardware` or a `bestScore` hint). If the
4244
+ * probe query failed (cold-start race, cap momentarily unreachable), the
4245
+ * pick floors to onnx purely for lack of information; persisting that would
4246
+ * LOCK onnx and skip auto-pick on every future boot even after the
4247
+ * accelerator surfaces. In that case we set the in-memory floor for liveness
4248
+ * but leave the store untouched so the next boot re-attempts the pick.
4209
4249
  */
4210
4250
  async autoPickAndPersist() {
4211
4251
  let hardware = null;
@@ -4213,7 +4253,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4213
4253
  try {
4214
4254
  const api = this.addonCtx?.api;
4215
4255
  if (api) {
4216
- const caps = await api.platformProbe.getCapabilities.query();
4256
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
4217
4257
  hardware = caps?.hardware ?? null;
4218
4258
  const bs = caps?.bestScore;
4219
4259
  if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
@@ -4227,9 +4267,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4227
4267
  device: pick.device
4228
4268
  };
4229
4269
  this.currentEngine = engine;
4270
+ if (!(hardware !== null || bestBackendHint !== null)) {
4271
+ this.log.warn("Auto-pick: probe returned no hardware/hint — using onnx floor WITHOUT persisting", { meta: {
4272
+ backend: pick.runtimeId,
4273
+ device: pick.device
4274
+ } });
4275
+ return;
4276
+ }
4277
+ const apNode = this.localProbeNodeId();
4230
4278
  await this.writeStore({
4231
- engineBackend: pick.runtimeId,
4232
- engineDevice: pick.device
4279
+ [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
4280
+ [nodeEngineKey("engineDevice", apNode)]: pick.device
4233
4281
  });
4234
4282
  this.log.info("Auto-picked engine at first boot", { meta: {
4235
4283
  backend: pick.runtimeId,
@@ -4402,11 +4450,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4402
4450
  * callers fall back to the registry's safe minimum. The engine OFFER derives
4403
4451
  * from hardware ONLY — install state (probe `scores`) never gates it.
4404
4452
  */
4453
+ /**
4454
+ * The local Moleculer node id (child-suffix stripped). MUST be passed to every
4455
+ * `platformProbe.getCapabilities` query: the cap is a singleton and a query
4456
+ * with no nodeId resolves to the HUB's probe — so on a remote agent the engine
4457
+ * decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
4458
+ * have) and pin a device the node can't run, breaking provisioning.
4459
+ */
4460
+ localProbeNodeId() {
4461
+ const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
4462
+ return raw.includes("/") ? raw.split("/")[0] : raw;
4463
+ }
4405
4464
  async fetchProbeGatingData() {
4406
4465
  try {
4407
4466
  const api = this.addonCtx?.api;
4408
4467
  if (!api) return { hardware: null };
4409
- return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
4468
+ return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
4410
4469
  } catch {
4411
4470
  return { hardware: null };
4412
4471
  }
@@ -5471,7 +5530,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5471
5530
  this.engineFactory = null;
5472
5531
  this.executor = null;
5473
5532
  }
5474
- for (const id of [...this.deviceProxies.keys()]) this.releaseDeviceProxy(id);
5533
+ for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5475
5534
  }
5476
5535
  /**
5477
5536
  * Resolve and cache a {@link DeviceProxy} for the given camera. Pins
@@ -5655,25 +5714,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5655
5714
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
5656
5715
  * to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
5657
5716
  * Returns null when neither source has anything; the caller keeps
5658
- * whatever `detectBestEngine()` picked at construction.
5717
+ * the onnx floor set at construction until autoPickAndPersist() runs.
5659
5718
  */
5660
5719
  async loadEngine() {
5661
5720
  const store = await this.readStore();
5662
5721
  const storedRuntime = store["engineRuntime"];
5663
- const storedBackend = store["engineBackend"];
5722
+ const node = this.localProbeNodeId();
5723
+ const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5664
5724
  if (typeof storedBackend === "string" && storedBackend.length > 0) {
5665
5725
  const backend = storedBackend;
5666
- const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5667
- const detected = DetectionPipelineProvider.detectBestEngine();
5726
+ const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5727
+ const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5728
+ const floor = onnxFloorPick();
5668
5729
  const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5669
5730
  if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
5670
- this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5731
+ this.log.warn("Stored engine backend unavailable on this node — falling back to onnx floor", { meta: {
5671
5732
  stored: migratedBackend,
5672
- fallback: `${detected.backend}`
5733
+ fallback: `${floor.backend}`
5673
5734
  } });
5674
- return detected;
5735
+ return floor;
5675
5736
  }
5676
- const device = storedDevice || detected.device;
5737
+ const device = storedDevice || floor.device;
5677
5738
  return {
5678
5739
  runtime: "python",
5679
5740
  backend: migratedBackend,
@@ -5845,18 +5906,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5845
5906
  const api = this.addonCtx?.api;
5846
5907
  let best;
5847
5908
  if (api) try {
5848
- const caps = await api.platformProbe.getCapabilities.query();
5909
+ const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
5849
5910
  const bs = caps?.bestScore;
5850
5911
  if (bs && bs.runtime === "python") {
5851
5912
  const probeBackend = bs.backend;
5852
5913
  const probeDevice = (() => {
5853
5914
  const hw = caps.hardware;
5854
- if (probeBackend === "coreml") return hw?.npu?.type === "apple-ane" ? "ane" : "all";
5855
- if (probeBackend === "openvino") {
5856
- if (hw?.npu?.type === "intel-npu") return "npu";
5857
- if (hw?.gpu?.type === "intel") return "gpu";
5858
- return "auto";
5859
- }
5915
+ if (probeBackend === "openvino") return defaultDeviceFor("openvino");
5916
+ if (probeBackend === "coreml") return defaultDeviceFor("coreml");
5860
5917
  if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
5861
5918
  return "cpu";
5862
5919
  })();
@@ -5866,16 +5923,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5866
5923
  format: backendToFormat(probeBackend),
5867
5924
  device: probeDevice
5868
5925
  };
5869
- } else best = DetectionPipelineProvider.detectBestEngine();
5926
+ } else best = onnxFloorPick();
5870
5927
  } catch {
5871
- best = DetectionPipelineProvider.detectBestEngine();
5928
+ best = onnxFloorPick();
5872
5929
  }
5873
- else best = DetectionPipelineProvider.detectBestEngine();
5930
+ else best = onnxFloorPick();
5874
5931
  const probedLabel = `${best.backend}/${best.device ?? "default"}`;
5932
+ const rpNode = this.localProbeNodeId();
5875
5933
  await this.writeStore({
5876
- probedBestEngine: probedLabel,
5877
- engineBackend: best.backend,
5878
- engineDevice: best.device ?? "cpu"
5934
+ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
5935
+ [nodeEngineKey("engineBackend", rpNode)]: best.backend,
5936
+ [nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
5879
5937
  });
5880
5938
  this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
5881
5939
  backend: best.backend,
@@ -6298,7 +6356,8 @@ var DEFAULT_CONFIG = {
6298
6356
  engineRuntime: "python",
6299
6357
  engineBackend: "onnx",
6300
6358
  engineDevice: "cpu",
6301
- probedBestEngine: ""
6359
+ probedBestEngine: "",
6360
+ activeEngine: ""
6302
6361
  };
6303
6362
  /** Derive the model-format from a backend value. Called by the provider. */
6304
6363
  function backendToFormat(backend) {
@@ -6341,6 +6400,15 @@ var POOL_BOUND_KEYS = [
6341
6400
  ];
6342
6401
  var DetectionPipelineAddon = class extends BaseAddon {
6343
6402
  provider = null;
6403
+ /** Last non-null probed hardware PER NODE — reused when the probe transiently
6404
+ * returns null so offered backends / device lists don't collapse. Keyed by
6405
+ * node because the hub addon probes other nodes when serving their config. */
6406
+ lastGoodHardwareByNode = /* @__PURE__ */ new Map();
6407
+ /** This node's effective engine selection, cached from the node-scoped store
6408
+ * so the synchronous `resolveBackendTuning` and the reprobe gate don't read
6409
+ * the cluster-shared bare keys. Refreshed at init + on every config change. */
6410
+ nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
6411
+ nodeProbedBestEngine = "";
6344
6412
  engineMetricsTimer = null;
6345
6413
  /** Snapshot-equality cache for engine-metrics emit. Most ticks
6346
6414
  * the engine inventory is unchanged (no model load/unload), so
@@ -6395,6 +6463,14 @@ var DetectionPipelineAddon = class extends BaseAddon {
6395
6463
  tooltip: "Re-probe engine"
6396
6464
  }]
6397
6465
  }),
6466
+ this.field({
6467
+ type: "text",
6468
+ key: "activeEngine",
6469
+ label: "Active engine",
6470
+ 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.",
6471
+ readonlyField: true,
6472
+ default: ""
6473
+ }),
6398
6474
  this.field({
6399
6475
  type: "select",
6400
6476
  key: "engineBackend",
@@ -6531,14 +6607,18 @@ var DetectionPipelineAddon = class extends BaseAddon {
6531
6607
  * probe is unreachable). Stored backend / device snap back to the registry
6532
6608
  * floor / default when they fall outside the offered set.
6533
6609
  */
6534
- async getGlobalSettings(overlay) {
6535
- const ctx = this.ctxIfReady;
6536
- const stored = ctx?.settings ? await ctx.settings.readAddonStore() ?? {} : {};
6610
+ async getGlobalSettings(overlay, _cap, nodeId) {
6611
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6612
+ const rawStored = await this.resolveUiSettingsStore();
6613
+ if (rawStored === null) return null;
6614
+ const stored = projectNodeEngine(rawStored, targetNode);
6615
+ const storedBackendRaw = stored["engineBackend"];
6616
+ if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6537
6617
  const merged = overlay ? {
6538
6618
  ...stored,
6539
6619
  ...overlay
6540
6620
  } : stored;
6541
- const env = await this.probeHardwareEnv();
6621
+ const env = await this.probeHardwareEnv(targetNode);
6542
6622
  const hardware = env.hardware;
6543
6623
  const offered = supportedRuntimes(env);
6544
6624
  const runtimeBackends = offered.map((id) => ({
@@ -6546,14 +6626,22 @@ var DetectionPipelineAddon = class extends BaseAddon {
6546
6626
  label: runtimeLabel(id)
6547
6627
  }));
6548
6628
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6549
- const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6629
+ const backend = (() => {
6630
+ const rid = toRuntimeId(storedBackend);
6631
+ if (rid === "onnx") return "onnx";
6632
+ if (offered.includes(rid)) return rid;
6633
+ return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6634
+ })();
6550
6635
  const deviceOptions = runtimeDevices(backend, hardware);
6551
6636
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6552
6637
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6638
+ const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6639
+ const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6553
6640
  const raw = {
6554
6641
  ...merged,
6555
6642
  engineBackend: backend,
6556
- engineDevice: device
6643
+ engineDevice: device,
6644
+ activeEngine
6557
6645
  };
6558
6646
  const schema = this.globalSettingsSchema();
6559
6647
  if (!schema) return { sections: [] };
@@ -6584,15 +6672,15 @@ var DetectionPipelineAddon = class extends BaseAddon {
6584
6672
  if (field.type === "slider" && "key" in field) {
6585
6673
  const tuned = tuning[field.key];
6586
6674
  const sliderField = field;
6587
- let patched = typeof tuned === "number" ? {
6675
+ let patchedField = typeof tuned === "number" ? {
6588
6676
  ...sliderField,
6589
6677
  default: tuned
6590
6678
  } : sliderField;
6591
- if (sliderField.key === "concurrency" && backend === "coreml") patched = {
6592
- ...patched,
6679
+ if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
6680
+ ...patchedField,
6593
6681
  max: 4
6594
6682
  };
6595
- return patched;
6683
+ return patchedField;
6596
6684
  }
6597
6685
  return field;
6598
6686
  })
@@ -6609,23 +6697,41 @@ var DetectionPipelineAddon = class extends BaseAddon {
6609
6697
  * Protected seam — overridable by tests (canned hardware) and reused by the
6610
6698
  * Phase 2 auto-pick path. NEVER reads install state.
6611
6699
  */
6612
- async probeHardwareEnv() {
6613
- const hardware = await this.resolveProbeHardware();
6700
+ /**
6701
+ * Resolve the persisted UI settings store, or `null` when it can't be read
6702
+ * right now (ctx/settings not wired — mid-restart). A readable-but-empty
6703
+ * store (genuine first boot) returns `{}`, never null. `getGlobalSettings`
6704
+ * uses the null signal to avoid serving fabricated empty-store defaults
6705
+ * during the restart window. Seam kept protected so tests can drive the
6706
+ * unreadable vs empty vs populated cases without faking a full AddonContext.
6707
+ */
6708
+ async resolveUiSettingsStore() {
6709
+ const settings = this.ctxIfReady?.settings;
6710
+ if (!settings) return null;
6711
+ return await settings.readAddonStore() ?? {};
6712
+ }
6713
+ async probeHardwareEnv(nodeId) {
6714
+ const node = nodeId ?? this.localNodeId();
6715
+ const hardware = await this.resolveProbeHardware(node);
6716
+ if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
6717
+ const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
6614
6718
  return {
6615
6719
  platform: process.platform,
6616
6720
  arch: process.arch,
6617
- hardware
6721
+ hardware: effective
6618
6722
  };
6619
6723
  }
6620
6724
  /**
6621
- * Fetch the probed hardware from the platform-probe cap. Returns null when
6622
- * the cap is not reachable (caller falls back to the registry's safe minimum).
6725
+ * Fetch the probed hardware from the platform-probe cap for `nodeId` (default
6726
+ * = self). Returns null when the cap is not reachable (caller falls back to
6727
+ * the registry's safe minimum).
6623
6728
  */
6624
- async resolveProbeHardware() {
6729
+ async resolveProbeHardware(nodeId) {
6625
6730
  try {
6626
6731
  const api = this.ctxIfReady?.api;
6627
6732
  if (!api) return null;
6628
- const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6733
+ const node = nodeId ?? this.localNodeId();
6734
+ const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, nodePin(node)))?.hardware;
6629
6735
  if (!hw) return null;
6630
6736
  return {
6631
6737
  npu: hw.npu ? { type: hw.npu.type } : null,
@@ -6636,6 +6742,61 @@ var DetectionPipelineAddon = class extends BaseAddon {
6636
6742
  }
6637
6743
  }
6638
6744
  /**
6745
+ * Bare node id used to scope the engine cascade in the shared store. MUST
6746
+ * match the provider's `localProbeNodeId()` (kernel.localNodeId, default
6747
+ * 'hub') so the UI write path (this) and the provider read/write path
6748
+ * (loadEngine / autoPick / reprobe) target the SAME `<key>@<nodeId>`. The old
6749
+ * `?? this.ctx.id` fallback resolved to the ADDON id ('detection-pipeline')
6750
+ * when kernel.localNodeId was absent, so UI saves landed on a key the
6751
+ * provider never read — silently losing the per-node selection.
6752
+ */
6753
+ localNodeId() {
6754
+ return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
6755
+ }
6756
+ /**
6757
+ * Refresh this node's cached engine selection from the node-scoped store.
6758
+ * `resolveBackendTuning` is synchronous and the reprobe gate runs before the
6759
+ * provider exists, so both read these cached fields instead of the
6760
+ * cluster-shared bare keys (which belong to no single node). Best-effort: a
6761
+ * transiently-unreadable store leaves the last cached values in place.
6762
+ */
6763
+ async refreshNodeEngineFromStore() {
6764
+ const store = await this.resolveUiSettingsStore();
6765
+ if (store === null) return;
6766
+ const node = this.localNodeId();
6767
+ const backend = readNodeEngineValue(store, "engineBackend", node);
6768
+ if (typeof backend === "string" && backend !== "") this.nodeEngineBackend = backend;
6769
+ const probed = readNodeEngineValue(store, "probedBestEngine", node);
6770
+ this.nodeProbedBestEngine = typeof probed === "string" ? probed : "";
6771
+ }
6772
+ /**
6773
+ * Persist a settings patch, mirroring the engine cascade fields to the TARGET
6774
+ * node's scoped keys so each node keeps an INDEPENDENT engine selection in the
6775
+ * cluster-central store. The hub addon serves writes for every node, so it
6776
+ * scopes by the requested `nodeId` (default self).
6777
+ *
6778
+ * When the target IS this node, `super.updateGlobalSettings` drives the normal
6779
+ * apply path (`resolveConfig` / `onConfigChanged` / `requiresRestart` restart),
6780
+ * and the bare engine keys it writes are shadowed by the node-scoped keys on
6781
+ * read. When the target is a SIBLING node, we persist the scoped engine keys +
6782
+ * the non-engine bare keys but DON'T run this node's restart/reprovision — the
6783
+ * owning node applies its own engine selection on its next (re)start.
6784
+ */
6785
+ async updateGlobalSettings(patch, nodeId) {
6786
+ const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6787
+ const patchRecord = patch;
6788
+ const scopedEngine = {};
6789
+ for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
6790
+ if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
6791
+ if (targetNode === this.localNodeId()) {
6792
+ await super.updateGlobalSettings(patch, nodeId);
6793
+ return;
6794
+ }
6795
+ const sharedPatch = {};
6796
+ for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
6797
+ if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
6798
+ }
6799
+ /**
6639
6800
  * Resolve the effective pool tuning for the configured backend.
6640
6801
  *
6641
6802
  * Reads the registry's `tuningFor(backend)` and ignores any persisted
@@ -6649,7 +6810,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6649
6810
  * a reason to disagree.
6650
6811
  */
6651
6812
  resolveBackendTuning() {
6652
- const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6813
+ const t = tuningFor(toRuntimeId(this.nodeEngineBackend ?? DEFAULT_CONFIG.engineBackend));
6653
6814
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6654
6815
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6655
6816
  return {
@@ -6668,6 +6829,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6668
6829
  relativePath: ""
6669
6830
  }).catch(() => "camstack-data/models");
6670
6831
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6832
+ await this.refreshNodeEngineFromStore();
6671
6833
  this.pythonAddonDir = resolveAddonPythonDir();
6672
6834
  const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6673
6835
  if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
@@ -6689,9 +6851,12 @@ var DetectionPipelineAddon = class extends BaseAddon {
6689
6851
  });
6690
6852
  await this.provider.init();
6691
6853
  await this.provider.setApi(this.ctx);
6692
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6854
+ if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6693
6855
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6694
6856
  });
6857
+ await this.provider.ensureBootEngineProvisioned().catch((err) => {
6858
+ this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6859
+ });
6695
6860
  await this.provider.warmPool();
6696
6861
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
6697
6862
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -6741,8 +6906,8 @@ var DetectionPipelineAddon = class extends BaseAddon {
6741
6906
  /**
6742
6907
  * Proactively install the OpenVINO Python package when Intel hardware
6743
6908
  * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6744
- * provider is constructed, so the module is available when the platform
6745
- * probe runs its next `import openvino.runtime` check.
6909
+ * provider is constructed, so the module is RUNNABLE when the engine is
6910
+ * first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
6746
6911
  *
6747
6912
  * Failure is non-fatal: a warning is logged and the addon continues with
6748
6913
  * the onnx-cpu baseline. The hardware query itself is also best-effort —
@@ -6811,6 +6976,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6811
6976
  * lifecycle (engineFactory rebuild on next runPipeline).
6812
6977
  */
6813
6978
  async onConfigChanged() {
6979
+ await this.refreshNodeEngineFromStore();
6814
6980
  if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6815
6981
  this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6816
6982
  });