@camstack/addon-pipeline 1.0.8 → 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 (35) hide show
  1. package/dist/audio-analyzer/index.js +5 -5
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +636 -659
  4. package/dist/detection-pipeline/index.mjs +624 -647
  5. package/dist/{model-download-service-C7AjBsX9-rXY-VFDk.js → model-download-service-RxAOiYvX-C8rTRJy_.js} +36 -6
  6. package/dist/{model-download-service-C7AjBsX9-B0ekM6dF.mjs → model-download-service-RxAOiYvX-CMAvhgO7.mjs} +36 -6
  7. package/dist/recorder/index.js +3 -3
  8. package/dist/recorder/index.mjs +1 -1
  9. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BFy9iszl.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DrohyZ5L.mjs} +3 -3
  10. package/dist/stream-broker/{hostInit-zRy9SzlX.mjs → hostInit-zLZbYJcg.mjs} +3 -3
  11. package/dist/stream-broker/index.js +7 -7
  12. package/dist/stream-broker/index.mjs +1 -1
  13. package/dist/stream-broker/remoteEntry.js +1 -1
  14. package/package.json +1 -1
  15. package/python/inference_pool.py +65 -6
  16. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  17. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  18. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  20. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  21. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  22. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  23. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  24. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  25. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  26. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  27. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  28. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  29. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  30. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  31. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  32. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  33. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  34. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  35. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
@@ -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");
5
+ const require_model_download_service_RxAOiYvX = require("../model-download-service-RxAOiYvX-C8rTRJy_.js");
6
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,77 +2325,108 @@ var EngineFactory = class {
2165
2325
  }
2166
2326
  };
2167
2327
  //#endregion
2168
- //#region src/detection-pipeline/engine-store-keys.ts
2169
- /**
2170
- * Per-node scoping for the detection-pipeline engine cascade.
2171
- *
2172
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
2173
- * settings-store cap is hub-resident; every node's detection instance reads and
2174
- * writes the same keys). That is correct for node-agnostic settings (pipeline
2175
- * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
2176
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
2177
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
2178
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
2179
- * another node that has no NPU.
2180
- *
2181
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
2182
- * Everything else in the store stays shared. A legacy un-scoped value (written
2183
- * before this change, or by an older build) is read as a migration fallback and
2184
- * re-persisted under the node-scoped key on the next write.
2185
- */
2186
- var ENGINE_CASCADE_KEYS = [
2187
- "engineBackend",
2188
- "engineDevice",
2189
- "probedBestEngine"
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
2190
2337
  ];
2191
- function isEngineCascadeKey(key) {
2192
- return ENGINE_CASCADE_KEYS.includes(key);
2193
- }
2194
- /**
2195
- * Normalise a raw kernel node id to the bare node id used for scoping.
2196
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
2197
- * per-NODE, so strip the addon segment. Falls back to `hub`.
2198
- */
2199
- function normalizeEngineNodeId(rawNodeId) {
2200
- const raw = rawNodeId ?? "hub";
2201
- return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
2202
- }
2203
- /** The node-scoped store key for an engine cascade field. */
2204
- function nodeEngineKey(base, nodeId) {
2205
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
2206
- }
2207
- /**
2208
- * Read an engine cascade value for a node: the node-scoped key if present,
2209
- * otherwise the legacy un-scoped value (migration), otherwise undefined.
2210
- */
2211
- function readNodeEngineValue(store, base, nodeId) {
2212
- const scoped = store[nodeEngineKey(base, nodeId)];
2213
- return scoped !== void 0 ? scoped : store[base];
2214
- }
2215
- /**
2216
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
2217
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
2218
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
2219
- * keys for OTHER nodes are dropped from the projection (not relevant to this
2220
- * node's form).
2221
- */
2222
- function projectNodeEngine(store, nodeId) {
2223
- const out = {};
2224
- const scopedForAnyNode = /* @__PURE__ */ new Set();
2225
- for (const key of Object.keys(store)) {
2226
- const atIdx = key.indexOf("@");
2227
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
2228
- scopedForAnyNode.add(key);
2229
- continue;
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;
2230
2379
  }
2231
- out[key] = store[key];
2232
2380
  }
2233
- for (const base of ENGINE_CASCADE_KEYS) {
2234
- const value = readNodeEngineValue(store, base, nodeId);
2235
- if (value !== void 0) out[base] = value;
2381
+ transition(next) {
2382
+ this.current = next;
2383
+ this.fx.onChange(next);
2236
2384
  }
2237
- return out;
2238
- }
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
+ };
2239
2430
  //#endregion
2240
2431
  //#region src/detection-pipeline/postprocess/dispatch.ts
2241
2432
  var VALID_KINDS = new Set([
@@ -3423,463 +3614,188 @@ var PipelineExecutor = class {
3423
3614
  inferenceMs: Date.now() - inferenceStart,
3424
3615
  postprocessMs: 0,
3425
3616
  inputType,
3426
- inputWidth,
3427
- inputHeight,
3428
- parentDetection: parentClass,
3429
- output: errorOutput,
3430
- error: errorMsg
3431
- });
3432
- stepTimings.push({
3433
- source: step.stepId,
3434
- modelId: step.modelId,
3435
- ms: Date.now() - preprocessStart,
3436
- detectionCount: 0
3437
- });
3438
- throw new Error(`Inference failed for step "${step.stepId}": ${errorMsg}`, { cause: err });
3439
- }
3440
- const structured = engineOutput.structured ?? {};
3441
- const inferenceMs = typeof structured.inferenceMs === "number" ? structured.inferenceMs : Date.now() - inferenceStart;
3442
- if (typeof structured.preprocessMs === "number") poolAgg.preprocessMs += structured.preprocessMs;
3443
- if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3444
- if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3445
- const postprocessStart = Date.now();
3446
- const output = dispatchPostprocess(engineOutput, step.definition);
3447
- const postprocessMs = Date.now() - postprocessStart;
3448
- stepTimings.push({
3449
- source: step.stepId,
3450
- modelId: step.modelId,
3451
- ms: preprocessMs + inferenceMs + postprocessMs,
3452
- detectionCount: output.kind === "detections" ? output.detections.length : 0
3453
- });
3454
- if (traceBuilder.isActive) traceBuilder.addStep({
3455
- stepId: step.stepId,
3456
- modelId: step.modelId,
3457
- slot: step.definition.slot,
3458
- postprocessor: step.definition.postprocessor,
3459
- preprocessMs,
3460
- inferenceMs,
3461
- postprocessMs,
3462
- inputType,
3463
- inputWidth,
3464
- inputHeight,
3465
- parentDetection: parentClass,
3466
- output
3467
- });
3468
- return output;
3469
- }
3470
- async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
3471
- for (const child of children) {
3472
- if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
3473
- const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
3474
- if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
3475
- if (isBboxDegenerate(parentDetection.bbox)) continue;
3476
- try {
3477
- const childStart = Date.now();
3478
- const fullFrameJpeg = await fullFrameJpegProvider();
3479
- const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
3480
- let cropJpegBuf;
3481
- let cropW;
3482
- let cropH;
3483
- if (modelEntry?.faceAlignment) {
3484
- const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
3485
- if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
3486
- const lms = parentDetection.landmarks;
3487
- if (lms && lms.length >= 5) {
3488
- const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
3489
- cropJpegBuf = aligned.jpeg;
3490
- cropW = aligned.width;
3491
- cropH = aligned.height;
3492
- } else {
3493
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3494
- cropJpegBuf = crop.jpeg;
3495
- cropW = crop.width;
3496
- cropH = crop.height;
3497
- }
3498
- } else {
3499
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3500
- cropJpegBuf = crop.jpeg;
3501
- cropW = crop.width;
3502
- cropH = crop.height;
3503
- }
3504
- const childOutput = await this.executeStep(child, {
3505
- kind: "jpeg",
3506
- data: cropJpegBuf
3507
- }, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
3508
- const childMs = Date.now() - childStart;
3509
- const detailsBefore = ctx.details.length;
3510
- applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
3511
- if (childOutput.kind === "detections" && child.children.length > 0) {
3512
- const newDetails = ctx.details.slice(detailsBefore);
3513
- for (const detail of newDetails) {
3514
- detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
3515
- if (detail.landmarks) {
3516
- const [px1, py1] = parentDetection.bbox;
3517
- detail.landmarks = detail.landmarks.map((l) => ({
3518
- x: l.x + px1,
3519
- y: l.y + py1
3520
- }));
3521
- }
3522
- await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
3523
- }
3524
- }
3525
- } catch {}
3526
- }
3527
- }
3528
- matchesInputClasses(className, inputClasses) {
3529
- if (inputClasses.length === 0) return true;
3530
- return inputClasses.includes(className);
3531
- }
3532
- /**
3533
- * Apply the object-detection step's macro filter + per-macro
3534
- * minConfidence sliders introduced in the Phase 6 step rework.
3535
- *
3536
- * Expected `settings` shape:
3537
- * - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
3538
- * Empty array = all allowed (legacy behaviour).
3539
- * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
3540
- */
3541
- matchesMacroFilter(macroClass, score, settings) {
3542
- if (!settings) return true;
3543
- const enabled = settings["enabledMacroClasses"];
3544
- if (Array.isArray(enabled) && enabled.length > 0) {
3545
- if (!enabled.includes(macroClass)) return false;
3546
- }
3547
- const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3548
- if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3549
- return true;
3550
- }
3551
- };
3552
- function capitalize(s) {
3553
- if (s.length === 0) return s;
3554
- return s.charAt(0).toUpperCase() + s.slice(1);
3555
- }
3556
- //#endregion
3557
- //#region src/detection-pipeline/pipeline/tree-builder.ts
3558
- /**
3559
- * Build an executable tree from user config.
3560
- *
3561
- * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3562
- * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3563
- * Throws if step not loaded.
3564
- */
3565
- function buildExecutableTree(steps, getEngine) {
3566
- return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3567
- }
3568
- function buildNode(step, getEngine) {
3569
- const definition = getStepDefinition(step.addonId);
3570
- const engine = getEngine(step.addonId);
3571
- const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3572
- const mergedSettings = {
3573
- ...collectSchemaDefaults(step.addonId),
3574
- ...step.settings
3575
- };
3576
- return {
3577
- stepId: step.addonId,
3578
- definition,
3579
- engine,
3580
- modelId: step.modelId,
3581
- inputClasses: definition.inputClasses ?? [],
3582
- enabled: step.enabled,
3583
- children,
3584
- ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3585
- };
3586
- }
3587
- /**
3588
- * Walk the step's declared `getConfigSchema()` and collect every field
3589
- * with a primitive `default` value into a plain record. Group fields
3590
- * are flattened — a nested `{ type: 'group', fields: [...] }` is
3591
- * entered recursively. Fields without a declared default are skipped.
3592
- */
3593
- function collectSchemaDefaults(stepId) {
3594
- const schema = getStep(stepId).getConfigSchema();
3595
- const out = {};
3596
- walkFieldsForDefaults(schema, out);
3597
- return out;
3598
- }
3599
- function walkFieldsForDefaults(fields, out) {
3600
- for (const field of fields) {
3601
- if ("type" in field && field.type === "group" && "fields" in field) {
3602
- walkFieldsForDefaults(field.fields, out);
3603
- continue;
3604
- }
3605
- if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3606
- }
3607
- }
3608
- //#endregion
3609
- //#region src/detection-pipeline/runtimes.ts
3610
- var KNOWN_PLATFORMS = [
3611
- "darwin",
3612
- "linux",
3613
- "win32"
3614
- ];
3615
- var KNOWN_ARCHES = ["arm64", "x64"];
3616
- var KNOWN_GPU_TYPES = [
3617
- "nvidia",
3618
- "amd",
3619
- "intel",
3620
- "apple"
3621
- ];
3622
- var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
3623
- function toKnownPlatform(p) {
3624
- return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
3625
- }
3626
- function toKnownArch(a) {
3627
- return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
3628
- }
3629
- function gpuInfoFrom(hw) {
3630
- if (!hw.gpu) return null;
3631
- const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
3632
- if (!type) return null;
3633
- return {
3634
- type,
3635
- name: ""
3636
- };
3637
- }
3638
- function npuInfoFrom(hw) {
3639
- if (!hw.npu) return null;
3640
- const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
3641
- if (!type) return null;
3642
- return { type };
3643
- }
3644
- function envToHardwareInfo(env) {
3645
- if (!env.hardware) return null;
3646
- return {
3647
- platform: toKnownPlatform(env.platform),
3648
- arch: toKnownArch(env.arch),
3649
- cpuModel: "",
3650
- cpuCores: 0,
3651
- totalRAM_MB: 0,
3652
- availableRAM_MB: 0,
3653
- gpu: gpuInfoFrom(env.hardware),
3654
- npu: npuInfoFrom(env.hardware)
3655
- };
3656
- }
3657
- function probedToHardwareInfo(hw) {
3658
- if (!hw) return null;
3659
- return {
3660
- platform: toKnownPlatform(process.platform),
3661
- arch: toKnownArch(process.arch),
3662
- cpuModel: "",
3663
- cpuCores: 0,
3664
- totalRAM_MB: 0,
3665
- availableRAM_MB: 0,
3666
- gpu: gpuInfoFrom(hw),
3667
- npu: npuInfoFrom(hw)
3668
- };
3669
- }
3670
- var RUNTIME_DETAIL = {
3671
- onnx: {
3672
- label: "ONNX Runtime",
3673
- pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3674
- tuning: {
3675
- concurrency: 4,
3676
- batchMode: "list",
3677
- maxBatchSize: 8,
3678
- intraOpThreads: 0
3679
- }
3680
- },
3681
- openvino: {
3682
- label: "OpenVINO",
3683
- pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3684
- tuning: {
3685
- concurrency: 1,
3686
- batchMode: "none",
3687
- numStreams: 0
3688
- }
3689
- },
3690
- coreml: {
3691
- label: "CoreML",
3692
- pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3693
- tuning: {
3694
- concurrency: 1,
3695
- batchMode: "none",
3696
- windowMs: 8,
3697
- maxBatchSize: 8,
3698
- numWorkers: 1
3699
- }
3700
- }
3701
- };
3702
- /**
3703
- * Returns the list of supported runtime IDs for the given hardware env.
3704
- * Delegates to `@camstack/types` `supportedRuntimes`.
3705
- */
3706
- function supportedRuntimes(env) {
3707
- return require_dist.supportedRuntimes(envToHardwareInfo(env));
3708
- }
3709
- /**
3710
- * Returns the device options for a given runtime and probed hardware.
3711
- * Delegates to `@camstack/types` `runtimeDevices`.
3712
- */
3713
- function runtimeDevices(id, hardware) {
3714
- return require_dist.runtimeDevices(id, probedToHardwareInfo(hardware));
3715
- }
3716
- /**
3717
- * Returns the default device string for a given runtime.
3718
- * Delegates to `@camstack/types` `defaultDeviceFor`.
3719
- */
3720
- function defaultDeviceFor(id) {
3721
- return require_dist.defaultDeviceFor(id);
3722
- }
3723
- /** Model format for each inference runtime supported by the detection pipeline. */
3724
- var RUNTIME_FORMAT = {
3725
- onnx: "onnx",
3726
- openvino: "openvino",
3727
- coreml: "coreml"
3728
- };
3729
- /**
3730
- * Returns the model format required for a given runtime.
3731
- * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
3732
- * matching the engine-provisioner's own ModelFormat type.
3733
- */
3734
- function modelFormatFor(id) {
3735
- return RUNTIME_FORMAT[id];
3736
- }
3737
- function pythonRequirementsFor(id) {
3738
- return RUNTIME_DETAIL[id].pythonRequirements;
3739
- }
3740
- function tuningFor(id) {
3741
- return RUNTIME_DETAIL[id].tuning;
3742
- }
3743
- function runtimeLabel(id) {
3744
- return RUNTIME_DETAIL[id].label;
3745
- }
3746
- /**
3747
- * Proactive-install hint kept for back-compat (re-exported from index.ts).
3748
- * Intel on Linux warrants installing openvino; coremltools covers macOS;
3749
- * openvino has no AMD/NVIDIA backend.
3750
- */
3751
- function shouldInstallOpenvino(env) {
3752
- if (env.platform === "darwin") return false;
3753
- return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3754
- }
3755
- //#endregion
3756
- //#region src/detection-pipeline/engine-provisioner.ts
3757
- /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3758
- var BACKOFF_SCHEDULE_MS = [
3759
- 5e3,
3760
- 15e3,
3761
- 3e4,
3762
- 6e4,
3763
- 12e4,
3764
- 3e5
3765
- ];
3766
- var IDLE_STATE = {
3767
- runtimeId: null,
3768
- device: null,
3769
- state: "idle"
3770
- };
3771
- var EngineProvisioner = class {
3772
- fx;
3773
- current = IDLE_STATE;
3774
- /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
3775
- generation = 0;
3776
- cancelTimer = null;
3777
- retryIndex = 0;
3778
- constructor(fx) {
3779
- this.fx = fx;
3780
- }
3781
- get state() {
3782
- return this.current;
3783
- }
3784
- isReady() {
3785
- return this.current.state === "ready";
3786
- }
3787
- select(runtimeId, device) {
3788
- this.generation++;
3789
- this.retryIndex = 0;
3790
- this.clearTimer();
3791
- this.transition({
3792
- runtimeId,
3793
- device,
3794
- state: "installing",
3795
- progress: 0
3617
+ inputWidth,
3618
+ inputHeight,
3619
+ parentDetection: parentClass,
3620
+ output: errorOutput,
3621
+ error: errorMsg
3622
+ });
3623
+ stepTimings.push({
3624
+ source: step.stepId,
3625
+ modelId: step.modelId,
3626
+ ms: Date.now() - preprocessStart,
3627
+ detectionCount: 0
3628
+ });
3629
+ throw new Error(`Inference failed for step "${step.stepId}": ${errorMsg}`, { cause: err });
3630
+ }
3631
+ const structured = engineOutput.structured ?? {};
3632
+ const inferenceMs = typeof structured.inferenceMs === "number" ? structured.inferenceMs : Date.now() - inferenceStart;
3633
+ if (typeof structured.preprocessMs === "number") poolAgg.preprocessMs += structured.preprocessMs;
3634
+ if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3635
+ if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3636
+ const postprocessStart = Date.now();
3637
+ const output = dispatchPostprocess(engineOutput, step.definition);
3638
+ const postprocessMs = Date.now() - postprocessStart;
3639
+ stepTimings.push({
3640
+ source: step.stepId,
3641
+ modelId: step.modelId,
3642
+ ms: preprocessMs + inferenceMs + postprocessMs,
3643
+ detectionCount: output.kind === "detections" ? output.detections.length : 0
3796
3644
  });
3797
- this.provision(this.generation, runtimeId, device);
3798
- }
3799
- dispose() {
3800
- this.generation++;
3801
- this.clearTimer();
3645
+ if (traceBuilder.isActive) traceBuilder.addStep({
3646
+ stepId: step.stepId,
3647
+ modelId: step.modelId,
3648
+ slot: step.definition.slot,
3649
+ postprocessor: step.definition.postprocessor,
3650
+ preprocessMs,
3651
+ inferenceMs,
3652
+ postprocessMs,
3653
+ inputType,
3654
+ inputWidth,
3655
+ inputHeight,
3656
+ parentDetection: parentClass,
3657
+ output
3658
+ });
3659
+ return output;
3802
3660
  }
3803
- clearTimer() {
3804
- if (this.cancelTimer !== null) {
3805
- this.cancelTimer();
3806
- this.cancelTimer = null;
3661
+ async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
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 {}
3807
3717
  }
3808
3718
  }
3809
- transition(next) {
3810
- this.current = next;
3811
- this.fx.onChange(next);
3719
+ matchesInputClasses(className, inputClasses) {
3720
+ if (inputClasses.length === 0) return true;
3721
+ return inputClasses.includes(className);
3812
3722
  }
3813
- async provision(gen, runtimeId, device) {
3814
- try {
3815
- await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
3816
- if (gen !== this.generation) return;
3817
- this.transition({
3818
- runtimeId,
3819
- device,
3820
- state: "verifying",
3821
- progress: 100
3822
- });
3823
- await this.fx.verify(runtimeId, device);
3824
- if (gen !== this.generation) return;
3825
- this.retryIndex = 0;
3826
- this.transition({
3827
- runtimeId,
3828
- device,
3829
- state: "ready"
3830
- });
3831
- } catch (err) {
3832
- if (gen !== this.generation) return;
3833
- const message = err instanceof Error ? err.message : String(err);
3834
- const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
3835
- this.retryIndex++;
3836
- const nextRetryAt = this.fx.now() + delay;
3837
- this.transition({
3838
- runtimeId,
3839
- device,
3840
- state: "failed",
3841
- error: message,
3842
- nextRetryAt
3843
- });
3844
- this.cancelTimer = this.fx.setTimer(delay, () => {
3845
- if (gen !== this.generation) return;
3846
- this.cancelTimer = null;
3847
- this.transition({
3848
- runtimeId,
3849
- device,
3850
- state: "installing",
3851
- progress: 0
3852
- });
3853
- this.provision(gen, runtimeId, device);
3854
- });
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;
3855
3737
  }
3738
+ const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3739
+ if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3740
+ return true;
3856
3741
  }
3857
3742
  };
3743
+ function capitalize(s) {
3744
+ if (s.length === 0) return s;
3745
+ return s.charAt(0).toUpperCase() + s.slice(1);
3746
+ }
3858
3747
  //#endregion
3859
- //#region src/detection-pipeline/auto-pick.ts
3860
- var PREFERENCE = [
3861
- "coreml",
3862
- "openvino",
3863
- "onnx"
3864
- ];
3748
+ //#region src/detection-pipeline/pipeline/tree-builder.ts
3865
3749
  /**
3866
- * Pure function picks the best supported runtime for the given hardware env.
3867
- *
3868
- * Logic:
3869
- * 1. If `bestBackendHint` is in the supported set, use it.
3870
- * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
3871
- * 3. Floor to `'onnx'` (always supported).
3750
+ * Build an executable tree from user config.
3872
3751
  *
3873
- * 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.
3874
3755
  */
3875
- function pickBestRuntime(env, bestBackendHint) {
3876
- const supported = supportedRuntimes(env);
3877
- 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
+ };
3878
3767
  return {
3879
- runtimeId: chosen,
3880
- 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 } : {}
3881
3776
  };
3882
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
+ }
3883
3799
  //#endregion
3884
3800
  //#region src/detection-pipeline/provider.ts
3885
3801
  /**
@@ -4161,6 +4077,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4161
4077
  */
4162
4078
  needsAutoPick = false;
4163
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
+ /**
4164
4087
  * Warm cache for benchmark engine-override runs.
4165
4088
  *
4166
4089
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -4193,7 +4116,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4193
4116
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4194
4117
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4195
4118
  this.currentEngine = ONNX_FLOOR;
4196
- this.log.info("Engine selected (default)", { meta: {
4119
+ this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4197
4120
  runtime: this.currentEngine.runtime,
4198
4121
  backend: this.currentEngine.backend,
4199
4122
  format: this.currentEngine.format
@@ -4266,26 +4189,70 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4266
4189
  this.needsAutoPick = false;
4267
4190
  this.startProvisioningForCurrentEngine();
4268
4191
  } else {
4269
- this.startProvisioningForCurrentEngine();
4270
4192
  const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4271
4193
  if (state !== "ready") return;
4272
- unsubscribe();
4194
+ this.cancelDeferredAutoPick();
4273
4195
  if (!this.needsAutoPick) return;
4274
4196
  this.autoPickAndPersist().then(() => {
4275
4197
  this.needsAutoPick = false;
4276
4198
  this.startProvisioningForCurrentEngine();
4277
4199
  });
4278
4200
  });
4279
- this.addonCtx.addDisposer(unsubscribe);
4201
+ this.deferredAutoPickUnsub = unsubscribe;
4202
+ this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
4280
4203
  }
4281
4204
  else this.startProvisioningForCurrentEngine();
4282
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
+ } });
4241
+ this.startProvisioningForCurrentEngine();
4242
+ }
4283
4243
  /**
4284
4244
  * Auto-pick the best supported runtime at first boot (no stored engine).
4285
4245
  * Uses the platform-probe cap's hardware + bestScore hint when available;
4286
4246
  * falls back to platform/arch when the probe cap is not yet reachable.
4247
+ *
4287
4248
  * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4288
- * 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.
4289
4256
  */
4290
4257
  async autoPickAndPersist() {
4291
4258
  let hardware = null;
@@ -4307,6 +4274,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4307
4274
  device: pick.device
4308
4275
  };
4309
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
+ }
4310
4284
  const apNode = this.localProbeNodeId();
4311
4285
  await this.writeStore({
4312
4286
  [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
@@ -4427,7 +4401,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4427
4401
  if (!step.enabled) continue;
4428
4402
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4429
4403
  if (!modelEntry) continue;
4430
- 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;
4431
4405
  await this.downloadWithRetry(modelEntry, format, 3);
4432
4406
  }
4433
4407
  }
@@ -4635,7 +4609,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4635
4609
  const formats = {};
4636
4610
  for (const [formatKey, entry] of Object.entries(m.formats)) {
4637
4611
  if (!entry) continue;
4638
- 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);
4639
4613
  formats[formatKey] = {
4640
4614
  url: entry.url,
4641
4615
  sizeMB: entry.sizeMB,
@@ -4758,7 +4732,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4758
4732
  const { modelId, format, addonId } = input;
4759
4733
  const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4760
4734
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4761
- 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`);
4762
4736
  this.log.info("Model deleted from disk", { meta: {
4763
4737
  modelId,
4764
4738
  format
@@ -5213,7 +5187,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5213
5187
  const format = this.currentEngine?.format ?? "onnx";
5214
5188
  for (const step of needed) {
5215
5189
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
5216
- 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)) {
5217
5191
  this.log.info("Downloading model for step", { meta: {
5218
5192
  modelId: step.modelId,
5219
5193
  format,
@@ -5238,7 +5212,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5238
5212
  /** Download a model with retry + exponential backoff */
5239
5213
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
5240
5214
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
5241
- 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);
5242
5216
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
5243
5217
  return;
5244
5218
  } catch (err) {
@@ -5715,11 +5689,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5715
5689
  } });
5716
5690
  continue;
5717
5691
  }
5718
- 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: {
5719
5693
  modelId: step.modelId,
5720
5694
  format
5721
5695
  } });
5722
- 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(() => {}));
5723
5697
  }
5724
5698
  await Promise.all(downloads);
5725
5699
  await this.ensureBackendDeps(this.currentEngine);
@@ -6130,7 +6104,7 @@ function buildSchemaSlots(format, modelsDir) {
6130
6104
  id: m.id,
6131
6105
  name: m.name,
6132
6106
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
6133
- downloaded: require_model_download_service_C7AjBsX9.isModelDownloaded(modelsDir, m, f),
6107
+ downloaded: require_model_download_service_RxAOiYvX.isModelDownloaded(modelsDir, m, f),
6134
6108
  sizeMB: entry.sizeMB
6135
6109
  }]))
6136
6110
  })),
@@ -6887,6 +6861,9 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6887
6861
  if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6888
6862
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6889
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
+ });
6890
6867
  await this.provider.warmPool();
6891
6868
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
6892
6869
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();