@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
@@ -1,11 +1,255 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
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-C7AjBsX9-B0ekM6dF.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,77 +2318,108 @@ var EngineFactory = class {
2158
2318
  }
2159
2319
  };
2160
2320
  //#endregion
2161
- //#region src/detection-pipeline/engine-store-keys.ts
2162
- /**
2163
- * Per-node scoping for the detection-pipeline engine cascade.
2164
- *
2165
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
2166
- * settings-store cap is hub-resident; every node's detection instance reads and
2167
- * writes the same keys). That is correct for node-agnostic settings (pipeline
2168
- * steps, tuning) but WRONG for the engine cascade — `engineBackend` /
2169
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
2170
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
2171
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
2172
- * another node that has no NPU.
2173
- *
2174
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
2175
- * Everything else in the store stays shared. A legacy un-scoped value (written
2176
- * before this change, or by an older build) is read as a migration fallback and
2177
- * re-persisted under the node-scoped key on the next write.
2178
- */
2179
- var ENGINE_CASCADE_KEYS = [
2180
- "engineBackend",
2181
- "engineDevice",
2182
- "probedBestEngine"
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
2183
2330
  ];
2184
- function isEngineCascadeKey(key) {
2185
- return ENGINE_CASCADE_KEYS.includes(key);
2186
- }
2187
- /**
2188
- * Normalise a raw kernel node id to the bare node id used for scoping.
2189
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
2190
- * per-NODE, so strip the addon segment. Falls back to `hub`.
2191
- */
2192
- function normalizeEngineNodeId(rawNodeId) {
2193
- const raw = rawNodeId ?? "hub";
2194
- return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
2195
- }
2196
- /** The node-scoped store key for an engine cascade field. */
2197
- function nodeEngineKey(base, nodeId) {
2198
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
2199
- }
2200
- /**
2201
- * Read an engine cascade value for a node: the node-scoped key if present,
2202
- * otherwise the legacy un-scoped value (migration), otherwise undefined.
2203
- */
2204
- function readNodeEngineValue(store, base, nodeId) {
2205
- const scoped = store[nodeEngineKey(base, nodeId)];
2206
- return scoped !== void 0 ? scoped : store[base];
2207
- }
2208
- /**
2209
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
2210
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
2211
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
2212
- * keys for OTHER nodes are dropped from the projection (not relevant to this
2213
- * node's form).
2214
- */
2215
- function projectNodeEngine(store, nodeId) {
2216
- const out = {};
2217
- const scopedForAnyNode = /* @__PURE__ */ new Set();
2218
- for (const key of Object.keys(store)) {
2219
- const atIdx = key.indexOf("@");
2220
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
2221
- scopedForAnyNode.add(key);
2222
- continue;
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;
2223
2372
  }
2224
- out[key] = store[key];
2225
2373
  }
2226
- for (const base of ENGINE_CASCADE_KEYS) {
2227
- const value = readNodeEngineValue(store, base, nodeId);
2228
- if (value !== void 0) out[base] = value;
2374
+ transition(next) {
2375
+ this.current = next;
2376
+ this.fx.onChange(next);
2229
2377
  }
2230
- return out;
2231
- }
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
+ };
2232
2423
  //#endregion
2233
2424
  //#region src/detection-pipeline/postprocess/dispatch.ts
2234
2425
  var VALID_KINDS = new Set([
@@ -3416,463 +3607,188 @@ var PipelineExecutor = class {
3416
3607
  inferenceMs: Date.now() - inferenceStart,
3417
3608
  postprocessMs: 0,
3418
3609
  inputType,
3419
- inputWidth,
3420
- inputHeight,
3421
- parentDetection: parentClass,
3422
- output: errorOutput,
3423
- error: errorMsg
3424
- });
3425
- stepTimings.push({
3426
- source: step.stepId,
3427
- modelId: step.modelId,
3428
- ms: Date.now() - preprocessStart,
3429
- detectionCount: 0
3430
- });
3431
- throw new Error(`Inference failed for step "${step.stepId}": ${errorMsg}`, { cause: err });
3432
- }
3433
- const structured = engineOutput.structured ?? {};
3434
- const inferenceMs = typeof structured.inferenceMs === "number" ? structured.inferenceMs : Date.now() - inferenceStart;
3435
- if (typeof structured.preprocessMs === "number") poolAgg.preprocessMs += structured.preprocessMs;
3436
- if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3437
- if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3438
- const postprocessStart = Date.now();
3439
- const output = dispatchPostprocess(engineOutput, step.definition);
3440
- const postprocessMs = Date.now() - postprocessStart;
3441
- stepTimings.push({
3442
- source: step.stepId,
3443
- modelId: step.modelId,
3444
- ms: preprocessMs + inferenceMs + postprocessMs,
3445
- detectionCount: output.kind === "detections" ? output.detections.length : 0
3446
- });
3447
- if (traceBuilder.isActive) traceBuilder.addStep({
3448
- stepId: step.stepId,
3449
- modelId: step.modelId,
3450
- slot: step.definition.slot,
3451
- postprocessor: step.definition.postprocessor,
3452
- preprocessMs,
3453
- inferenceMs,
3454
- postprocessMs,
3455
- inputType,
3456
- inputWidth,
3457
- inputHeight,
3458
- parentDetection: parentClass,
3459
- output
3460
- });
3461
- return output;
3462
- }
3463
- async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
3464
- for (const child of children) {
3465
- if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
3466
- const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
3467
- if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
3468
- if (isBboxDegenerate(parentDetection.bbox)) continue;
3469
- try {
3470
- const childStart = Date.now();
3471
- const fullFrameJpeg = await fullFrameJpegProvider();
3472
- const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
3473
- let cropJpegBuf;
3474
- let cropW;
3475
- let cropH;
3476
- if (modelEntry?.faceAlignment) {
3477
- const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
3478
- if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
3479
- const lms = parentDetection.landmarks;
3480
- if (lms && lms.length >= 5) {
3481
- const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
3482
- cropJpegBuf = aligned.jpeg;
3483
- cropW = aligned.width;
3484
- cropH = aligned.height;
3485
- } else {
3486
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3487
- cropJpegBuf = crop.jpeg;
3488
- cropW = crop.width;
3489
- cropH = crop.height;
3490
- }
3491
- } else {
3492
- const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
3493
- cropJpegBuf = crop.jpeg;
3494
- cropW = crop.width;
3495
- cropH = crop.height;
3496
- }
3497
- const childOutput = await this.executeStep(child, {
3498
- kind: "jpeg",
3499
- data: cropJpegBuf
3500
- }, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
3501
- const childMs = Date.now() - childStart;
3502
- const detailsBefore = ctx.details.length;
3503
- applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
3504
- if (childOutput.kind === "detections" && child.children.length > 0) {
3505
- const newDetails = ctx.details.slice(detailsBefore);
3506
- for (const detail of newDetails) {
3507
- detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
3508
- if (detail.landmarks) {
3509
- const [px1, py1] = parentDetection.bbox;
3510
- detail.landmarks = detail.landmarks.map((l) => ({
3511
- x: l.x + px1,
3512
- y: l.y + py1
3513
- }));
3514
- }
3515
- await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
3516
- }
3517
- }
3518
- } catch {}
3519
- }
3520
- }
3521
- matchesInputClasses(className, inputClasses) {
3522
- if (inputClasses.length === 0) return true;
3523
- return inputClasses.includes(className);
3524
- }
3525
- /**
3526
- * Apply the object-detection step's macro filter + per-macro
3527
- * minConfidence sliders introduced in the Phase 6 step rework.
3528
- *
3529
- * Expected `settings` shape:
3530
- * - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
3531
- * Empty array = all allowed (legacy behaviour).
3532
- * - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
3533
- */
3534
- matchesMacroFilter(macroClass, score, settings) {
3535
- if (!settings) return true;
3536
- const enabled = settings["enabledMacroClasses"];
3537
- if (Array.isArray(enabled) && enabled.length > 0) {
3538
- if (!enabled.includes(macroClass)) return false;
3539
- }
3540
- const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3541
- if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3542
- return true;
3543
- }
3544
- };
3545
- function capitalize(s) {
3546
- if (s.length === 0) return s;
3547
- return s.charAt(0).toUpperCase() + s.slice(1);
3548
- }
3549
- //#endregion
3550
- //#region src/detection-pipeline/pipeline/tree-builder.ts
3551
- /**
3552
- * Build an executable tree from user config.
3553
- *
3554
- * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3555
- * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3556
- * Throws if step not loaded.
3557
- */
3558
- function buildExecutableTree(steps, getEngine) {
3559
- return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3560
- }
3561
- function buildNode(step, getEngine) {
3562
- const definition = getStepDefinition(step.addonId);
3563
- const engine = getEngine(step.addonId);
3564
- const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3565
- const mergedSettings = {
3566
- ...collectSchemaDefaults(step.addonId),
3567
- ...step.settings
3568
- };
3569
- return {
3570
- stepId: step.addonId,
3571
- definition,
3572
- engine,
3573
- modelId: step.modelId,
3574
- inputClasses: definition.inputClasses ?? [],
3575
- enabled: step.enabled,
3576
- children,
3577
- ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3578
- };
3579
- }
3580
- /**
3581
- * Walk the step's declared `getConfigSchema()` and collect every field
3582
- * with a primitive `default` value into a plain record. Group fields
3583
- * are flattened — a nested `{ type: 'group', fields: [...] }` is
3584
- * entered recursively. Fields without a declared default are skipped.
3585
- */
3586
- function collectSchemaDefaults(stepId) {
3587
- const schema = getStep(stepId).getConfigSchema();
3588
- const out = {};
3589
- walkFieldsForDefaults(schema, out);
3590
- return out;
3591
- }
3592
- function walkFieldsForDefaults(fields, out) {
3593
- for (const field of fields) {
3594
- if ("type" in field && field.type === "group" && "fields" in field) {
3595
- walkFieldsForDefaults(field.fields, out);
3596
- continue;
3597
- }
3598
- if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3599
- }
3600
- }
3601
- //#endregion
3602
- //#region src/detection-pipeline/runtimes.ts
3603
- var KNOWN_PLATFORMS = [
3604
- "darwin",
3605
- "linux",
3606
- "win32"
3607
- ];
3608
- var KNOWN_ARCHES = ["arm64", "x64"];
3609
- var KNOWN_GPU_TYPES = [
3610
- "nvidia",
3611
- "amd",
3612
- "intel",
3613
- "apple"
3614
- ];
3615
- var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
3616
- function toKnownPlatform(p) {
3617
- return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
3618
- }
3619
- function toKnownArch(a) {
3620
- return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
3621
- }
3622
- function gpuInfoFrom(hw) {
3623
- if (!hw.gpu) return null;
3624
- const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
3625
- if (!type) return null;
3626
- return {
3627
- type,
3628
- name: ""
3629
- };
3630
- }
3631
- function npuInfoFrom(hw) {
3632
- if (!hw.npu) return null;
3633
- const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
3634
- if (!type) return null;
3635
- return { type };
3636
- }
3637
- function envToHardwareInfo(env) {
3638
- if (!env.hardware) return null;
3639
- return {
3640
- platform: toKnownPlatform(env.platform),
3641
- arch: toKnownArch(env.arch),
3642
- cpuModel: "",
3643
- cpuCores: 0,
3644
- totalRAM_MB: 0,
3645
- availableRAM_MB: 0,
3646
- gpu: gpuInfoFrom(env.hardware),
3647
- npu: npuInfoFrom(env.hardware)
3648
- };
3649
- }
3650
- function probedToHardwareInfo(hw) {
3651
- if (!hw) return null;
3652
- return {
3653
- platform: toKnownPlatform(process.platform),
3654
- arch: toKnownArch(process.arch),
3655
- cpuModel: "",
3656
- cpuCores: 0,
3657
- totalRAM_MB: 0,
3658
- availableRAM_MB: 0,
3659
- gpu: gpuInfoFrom(hw),
3660
- npu: npuInfoFrom(hw)
3661
- };
3662
- }
3663
- var RUNTIME_DETAIL = {
3664
- onnx: {
3665
- label: "ONNX Runtime",
3666
- pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3667
- tuning: {
3668
- concurrency: 4,
3669
- batchMode: "list",
3670
- maxBatchSize: 8,
3671
- intraOpThreads: 0
3672
- }
3673
- },
3674
- openvino: {
3675
- label: "OpenVINO",
3676
- pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3677
- tuning: {
3678
- concurrency: 1,
3679
- batchMode: "none",
3680
- numStreams: 0
3681
- }
3682
- },
3683
- coreml: {
3684
- label: "CoreML",
3685
- pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3686
- tuning: {
3687
- concurrency: 1,
3688
- batchMode: "none",
3689
- windowMs: 8,
3690
- maxBatchSize: 8,
3691
- numWorkers: 1
3692
- }
3693
- }
3694
- };
3695
- /**
3696
- * Returns the list of supported runtime IDs for the given hardware env.
3697
- * Delegates to `@camstack/types` `supportedRuntimes`.
3698
- */
3699
- function supportedRuntimes(env) {
3700
- return supportedRuntimes$1(envToHardwareInfo(env));
3701
- }
3702
- /**
3703
- * Returns the device options for a given runtime and probed hardware.
3704
- * Delegates to `@camstack/types` `runtimeDevices`.
3705
- */
3706
- function runtimeDevices(id, hardware) {
3707
- return runtimeDevices$1(id, probedToHardwareInfo(hardware));
3708
- }
3709
- /**
3710
- * Returns the default device string for a given runtime.
3711
- * Delegates to `@camstack/types` `defaultDeviceFor`.
3712
- */
3713
- function defaultDeviceFor(id) {
3714
- return defaultDeviceFor$1(id);
3715
- }
3716
- /** Model format for each inference runtime supported by the detection pipeline. */
3717
- var RUNTIME_FORMAT = {
3718
- onnx: "onnx",
3719
- openvino: "openvino",
3720
- coreml: "coreml"
3721
- };
3722
- /**
3723
- * Returns the model format required for a given runtime.
3724
- * Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
3725
- * matching the engine-provisioner's own ModelFormat type.
3726
- */
3727
- function modelFormatFor(id) {
3728
- return RUNTIME_FORMAT[id];
3729
- }
3730
- function pythonRequirementsFor(id) {
3731
- return RUNTIME_DETAIL[id].pythonRequirements;
3732
- }
3733
- function tuningFor(id) {
3734
- return RUNTIME_DETAIL[id].tuning;
3735
- }
3736
- function runtimeLabel(id) {
3737
- return RUNTIME_DETAIL[id].label;
3738
- }
3739
- /**
3740
- * Proactive-install hint kept for back-compat (re-exported from index.ts).
3741
- * Intel on Linux warrants installing openvino; coremltools covers macOS;
3742
- * openvino has no AMD/NVIDIA backend.
3743
- */
3744
- function shouldInstallOpenvino(env) {
3745
- if (env.platform === "darwin") return false;
3746
- return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3747
- }
3748
- //#endregion
3749
- //#region src/detection-pipeline/engine-provisioner.ts
3750
- /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3751
- var BACKOFF_SCHEDULE_MS = [
3752
- 5e3,
3753
- 15e3,
3754
- 3e4,
3755
- 6e4,
3756
- 12e4,
3757
- 3e5
3758
- ];
3759
- var IDLE_STATE = {
3760
- runtimeId: null,
3761
- device: null,
3762
- state: "idle"
3763
- };
3764
- var EngineProvisioner = class {
3765
- fx;
3766
- current = IDLE_STATE;
3767
- /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
3768
- generation = 0;
3769
- cancelTimer = null;
3770
- retryIndex = 0;
3771
- constructor(fx) {
3772
- this.fx = fx;
3773
- }
3774
- get state() {
3775
- return this.current;
3776
- }
3777
- isReady() {
3778
- return this.current.state === "ready";
3779
- }
3780
- select(runtimeId, device) {
3781
- this.generation++;
3782
- this.retryIndex = 0;
3783
- this.clearTimer();
3784
- this.transition({
3785
- runtimeId,
3786
- device,
3787
- state: "installing",
3788
- progress: 0
3610
+ inputWidth,
3611
+ inputHeight,
3612
+ parentDetection: parentClass,
3613
+ output: errorOutput,
3614
+ error: errorMsg
3615
+ });
3616
+ stepTimings.push({
3617
+ source: step.stepId,
3618
+ modelId: step.modelId,
3619
+ ms: Date.now() - preprocessStart,
3620
+ detectionCount: 0
3621
+ });
3622
+ throw new Error(`Inference failed for step "${step.stepId}": ${errorMsg}`, { cause: err });
3623
+ }
3624
+ const structured = engineOutput.structured ?? {};
3625
+ const inferenceMs = typeof structured.inferenceMs === "number" ? structured.inferenceMs : Date.now() - inferenceStart;
3626
+ if (typeof structured.preprocessMs === "number") poolAgg.preprocessMs += structured.preprocessMs;
3627
+ if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3628
+ if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3629
+ const postprocessStart = Date.now();
3630
+ const output = dispatchPostprocess(engineOutput, step.definition);
3631
+ const postprocessMs = Date.now() - postprocessStart;
3632
+ stepTimings.push({
3633
+ source: step.stepId,
3634
+ modelId: step.modelId,
3635
+ ms: preprocessMs + inferenceMs + postprocessMs,
3636
+ detectionCount: output.kind === "detections" ? output.detections.length : 0
3789
3637
  });
3790
- this.provision(this.generation, runtimeId, device);
3791
- }
3792
- dispose() {
3793
- this.generation++;
3794
- this.clearTimer();
3638
+ if (traceBuilder.isActive) traceBuilder.addStep({
3639
+ stepId: step.stepId,
3640
+ modelId: step.modelId,
3641
+ slot: step.definition.slot,
3642
+ postprocessor: step.definition.postprocessor,
3643
+ preprocessMs,
3644
+ inferenceMs,
3645
+ postprocessMs,
3646
+ inputType,
3647
+ inputWidth,
3648
+ inputHeight,
3649
+ parentDetection: parentClass,
3650
+ output
3651
+ });
3652
+ return output;
3795
3653
  }
3796
- clearTimer() {
3797
- if (this.cancelTimer !== null) {
3798
- this.cancelTimer();
3799
- this.cancelTimer = null;
3654
+ async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
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 {}
3800
3710
  }
3801
3711
  }
3802
- transition(next) {
3803
- this.current = next;
3804
- this.fx.onChange(next);
3712
+ matchesInputClasses(className, inputClasses) {
3713
+ if (inputClasses.length === 0) return true;
3714
+ return inputClasses.includes(className);
3805
3715
  }
3806
- async provision(gen, runtimeId, device) {
3807
- try {
3808
- await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
3809
- if (gen !== this.generation) return;
3810
- this.transition({
3811
- runtimeId,
3812
- device,
3813
- state: "verifying",
3814
- progress: 100
3815
- });
3816
- await this.fx.verify(runtimeId, device);
3817
- if (gen !== this.generation) return;
3818
- this.retryIndex = 0;
3819
- this.transition({
3820
- runtimeId,
3821
- device,
3822
- state: "ready"
3823
- });
3824
- } catch (err) {
3825
- if (gen !== this.generation) return;
3826
- const message = err instanceof Error ? err.message : String(err);
3827
- const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
3828
- this.retryIndex++;
3829
- const nextRetryAt = this.fx.now() + delay;
3830
- this.transition({
3831
- runtimeId,
3832
- device,
3833
- state: "failed",
3834
- error: message,
3835
- nextRetryAt
3836
- });
3837
- this.cancelTimer = this.fx.setTimer(delay, () => {
3838
- if (gen !== this.generation) return;
3839
- this.cancelTimer = null;
3840
- this.transition({
3841
- runtimeId,
3842
- device,
3843
- state: "installing",
3844
- progress: 0
3845
- });
3846
- this.provision(gen, runtimeId, device);
3847
- });
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;
3848
3730
  }
3731
+ const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3732
+ if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3733
+ return true;
3849
3734
  }
3850
3735
  };
3736
+ function capitalize(s) {
3737
+ if (s.length === 0) return s;
3738
+ return s.charAt(0).toUpperCase() + s.slice(1);
3739
+ }
3851
3740
  //#endregion
3852
- //#region src/detection-pipeline/auto-pick.ts
3853
- var PREFERENCE = [
3854
- "coreml",
3855
- "openvino",
3856
- "onnx"
3857
- ];
3741
+ //#region src/detection-pipeline/pipeline/tree-builder.ts
3858
3742
  /**
3859
- * Pure function picks the best supported runtime for the given hardware env.
3860
- *
3861
- * Logic:
3862
- * 1. If `bestBackendHint` is in the supported set, use it.
3863
- * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
3864
- * 3. Floor to `'onnx'` (always supported).
3743
+ * Build an executable tree from user config.
3865
3744
  *
3866
- * 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.
3867
3748
  */
3868
- function pickBestRuntime(env, bestBackendHint) {
3869
- const supported = supportedRuntimes(env);
3870
- 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
+ };
3871
3760
  return {
3872
- runtimeId: chosen,
3873
- 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 } : {}
3874
3769
  };
3875
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
+ }
3876
3792
  //#endregion
3877
3793
  //#region src/detection-pipeline/provider.ts
3878
3794
  /**
@@ -4154,6 +4070,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4154
4070
  */
4155
4071
  needsAutoPick = false;
4156
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
+ /**
4157
4080
  * Warm cache for benchmark engine-override runs.
4158
4081
  *
4159
4082
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -4186,7 +4109,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4186
4109
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4187
4110
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4188
4111
  this.currentEngine = ONNX_FLOOR;
4189
- this.log.info("Engine selected (default)", { meta: {
4112
+ this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4190
4113
  runtime: this.currentEngine.runtime,
4191
4114
  backend: this.currentEngine.backend,
4192
4115
  format: this.currentEngine.format
@@ -4259,26 +4182,70 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4259
4182
  this.needsAutoPick = false;
4260
4183
  this.startProvisioningForCurrentEngine();
4261
4184
  } else {
4262
- this.startProvisioningForCurrentEngine();
4263
4185
  const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4264
4186
  if (state !== "ready") return;
4265
- unsubscribe();
4187
+ this.cancelDeferredAutoPick();
4266
4188
  if (!this.needsAutoPick) return;
4267
4189
  this.autoPickAndPersist().then(() => {
4268
4190
  this.needsAutoPick = false;
4269
4191
  this.startProvisioningForCurrentEngine();
4270
4192
  });
4271
4193
  });
4272
- this.addonCtx.addDisposer(unsubscribe);
4194
+ this.deferredAutoPickUnsub = unsubscribe;
4195
+ this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
4273
4196
  }
4274
4197
  else this.startProvisioningForCurrentEngine();
4275
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
+ } });
4234
+ this.startProvisioningForCurrentEngine();
4235
+ }
4276
4236
  /**
4277
4237
  * Auto-pick the best supported runtime at first boot (no stored engine).
4278
4238
  * Uses the platform-probe cap's hardware + bestScore hint when available;
4279
4239
  * falls back to platform/arch when the probe cap is not yet reachable.
4240
+ *
4280
4241
  * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4281
- * 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.
4282
4249
  */
4283
4250
  async autoPickAndPersist() {
4284
4251
  let hardware = null;
@@ -4300,6 +4267,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4300
4267
  device: pick.device
4301
4268
  };
4302
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
+ }
4303
4277
  const apNode = this.localProbeNodeId();
4304
4278
  await this.writeStore({
4305
4279
  [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
@@ -6880,6 +6854,9 @@ var DetectionPipelineAddon = class extends BaseAddon {
6880
6854
  if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6881
6855
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6882
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
+ });
6883
6860
  await this.provider.warmPool();
6884
6861
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
6885
6862
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();