@camstack/addon-pipeline 1.0.8 → 1.1.1

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 (55) hide show
  1. package/dist/audio-analyzer/index.js +104 -29
  2. package/dist/audio-analyzer/index.mjs +100 -25
  3. package/dist/audio-codec-nodeav/index.js +1 -1
  4. package/dist/audio-codec-nodeav/index.mjs +1 -1
  5. package/dist/decoder-nodeav/index.js +1 -1
  6. package/dist/decoder-nodeav/index.mjs +1 -1
  7. package/dist/detection-pipeline/index.js +685 -577
  8. package/dist/detection-pipeline/index.mjs +673 -565
  9. package/dist/{dist-BA6DR_jV.mjs → dist-BWc-HYQz.mjs} +194 -1
  10. package/dist/{dist-BLcTVvol.js → dist-DnD2tm7T.js} +194 -1
  11. package/dist/{model-download-service-C7AjBsX9-B0ekM6dF.mjs → model-download-service-C-IHWnXx-3Mmeob3l.mjs} +36 -6
  12. package/dist/{model-download-service-C7AjBsX9-rXY-VFDk.js → model-download-service-C-IHWnXx-BnQ_awK4.js} +36 -6
  13. package/dist/motion-wasm/index.js +1 -1
  14. package/dist/motion-wasm/index.mjs +1 -1
  15. package/dist/pipeline-runner/index.js +14 -10
  16. package/dist/pipeline-runner/index.mjs +14 -10
  17. package/dist/recorder/index.js +4 -4
  18. package/dist/recorder/index.mjs +2 -2
  19. package/dist/stream-broker/_stub.js +1 -1
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BFy9iszl.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Do7lgO8N.mjs} +3 -3
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-FRD2eBuz.mjs +26 -0
  22. package/dist/stream-broker/{hostInit-zRy9SzlX.mjs → hostInit-D5y5VMK8.mjs} +3 -3
  23. package/dist/stream-broker/index.js +8 -8
  24. package/dist/stream-broker/index.mjs +2 -2
  25. package/dist/stream-broker/remoteEntry.js +1 -1
  26. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js → MaskShapeCanvas-DI4BY7W2-5UPreLSr.js} +1 -1
  27. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CQzEnQoq.js → MotionZonesSettings-NcxxQN8r-Bxqs-CpZ.js} +1 -1
  28. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js → PrivacyMaskSettings-APgPLF7p-BDMPeMJd.js} +1 -1
  29. package/embed-dist/assets/{index-CSuLwWK-.js → index-BgGwqHYl.js} +9 -9
  30. package/embed-dist/index.html +1 -1
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +65 -6
  33. package/python/postprocessors/saliency.py +47 -1
  34. package/python/postprocessors/test_saliency.py +23 -0
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COa17XL2.mjs +0 -26
  36. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  37. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  38. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  39. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  40. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  41. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  42. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  43. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  44. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  45. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  46. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  47. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  48. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  49. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  50. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  51. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  52. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  53. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  54. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  55. 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");
6
- const require_dist = require("../dist-BLcTVvol.js");
5
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
6
+ const require_dist = require("../dist-DnD2tm7T.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_C_IHWnXx.__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_C_IHWnXx.__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_C_IHWnXx.__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_C_IHWnXx.__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,46 @@ 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
+ };
1046
+ /**
1047
+ * Build a precision-variant catalog entry (OpenVINO-only) derived from a base
1048
+ * detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
1049
+ * NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
1050
+ * accuracy cost). The IRs live next to the base `.xml` on HF as
1051
+ * `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
1052
+ * (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
1053
+ */
1054
+ var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
1055
+ id: `${baseId}-${precision}`,
1056
+ name: `${baseName} (${precision.toUpperCase()})`,
1057
+ description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
1058
+ inputSize: {
1059
+ width: 640,
1060
+ height: 640
1061
+ },
1062
+ labels: [],
1063
+ preprocessMode: "letterbox",
1064
+ formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) }
1065
+ });
782
1066
  var MLPACKAGE_FILES = [
783
1067
  "Manifest.json",
784
1068
  "Data/com.apple.CoreML/model.mlmodel",
@@ -807,11 +1091,7 @@ var OBJECT_DETECTION_MODELS = [
807
1091
  files: [...MLPACKAGE_FILES],
808
1092
  runtimes: ["python"]
809
1093
  },
810
- openvino: {
811
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"),
812
- sizeMB: 6,
813
- runtimes: ["python"]
814
- }
1094
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
815
1095
  }
816
1096
  },
817
1097
  {
@@ -836,11 +1116,7 @@ var OBJECT_DETECTION_MODELS = [
836
1116
  files: [...MLPACKAGE_FILES],
837
1117
  runtimes: ["python"]
838
1118
  },
839
- openvino: {
840
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"),
841
- sizeMB: 16,
842
- runtimes: ["python"]
843
- }
1119
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
844
1120
  }
845
1121
  },
846
1122
  {
@@ -865,11 +1141,7 @@ var OBJECT_DETECTION_MODELS = [
865
1141
  files: [...MLPACKAGE_FILES],
866
1142
  runtimes: ["python"]
867
1143
  },
868
- openvino: {
869
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"),
870
- sizeMB: 49,
871
- runtimes: ["python"]
872
- }
1144
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
873
1145
  }
874
1146
  },
875
1147
  {
@@ -894,11 +1166,7 @@ var OBJECT_DETECTION_MODELS = [
894
1166
  files: [...MLPACKAGE_FILES],
895
1167
  runtimes: ["python"]
896
1168
  },
897
- openvino: {
898
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"),
899
- sizeMB: 9,
900
- runtimes: ["python"]
901
- }
1169
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
902
1170
  }
903
1171
  },
904
1172
  {
@@ -923,11 +1191,7 @@ var OBJECT_DETECTION_MODELS = [
923
1191
  files: [...MLPACKAGE_FILES],
924
1192
  runtimes: ["python"]
925
1193
  },
926
- openvino: {
927
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"),
928
- sizeMB: 36,
929
- runtimes: ["python"]
930
- }
1194
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
931
1195
  }
932
1196
  },
933
1197
  {
@@ -952,11 +1216,7 @@ var OBJECT_DETECTION_MODELS = [
952
1216
  files: [...MLPACKAGE_FILES],
953
1217
  runtimes: ["python"]
954
1218
  },
955
- openvino: {
956
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"),
957
- sizeMB: 78,
958
- runtimes: ["python"]
959
- }
1219
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
960
1220
  }
961
1221
  },
962
1222
  {
@@ -981,11 +1241,7 @@ var OBJECT_DETECTION_MODELS = [
981
1241
  files: [...MLPACKAGE_FILES],
982
1242
  runtimes: ["python"]
983
1243
  },
984
- openvino: {
985
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"),
986
- sizeMB: 95,
987
- runtimes: ["python"]
988
- }
1244
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
989
1245
  }
990
1246
  },
991
1247
  {
@@ -1010,11 +1266,7 @@ var OBJECT_DETECTION_MODELS = [
1010
1266
  files: [...MLPACKAGE_FILES],
1011
1267
  runtimes: ["python"]
1012
1268
  },
1013
- openvino: {
1014
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"),
1015
- sizeMB: 213,
1016
- runtimes: ["python"]
1017
- }
1269
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
1018
1270
  }
1019
1271
  },
1020
1272
  {
@@ -1039,11 +1291,7 @@ var OBJECT_DETECTION_MODELS = [
1039
1291
  files: [...MLPACKAGE_FILES],
1040
1292
  runtimes: ["python"]
1041
1293
  },
1042
- openvino: {
1043
- url: hfScrypted("openvino/scrypted_yolov9t_relu/best.xml"),
1044
- sizeMB: 6,
1045
- runtimes: ["python"]
1046
- }
1294
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
1047
1295
  }
1048
1296
  },
1049
1297
  {
@@ -1068,11 +1316,7 @@ var OBJECT_DETECTION_MODELS = [
1068
1316
  files: [...MLPACKAGE_FILES],
1069
1317
  runtimes: ["python"]
1070
1318
  },
1071
- openvino: {
1072
- url: hfScrypted("openvino/scrypted_yolov9s_relu/best.xml"),
1073
- sizeMB: 16,
1074
- runtimes: ["python"]
1075
- }
1319
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
1076
1320
  }
1077
1321
  },
1078
1322
  {
@@ -1097,11 +1341,7 @@ var OBJECT_DETECTION_MODELS = [
1097
1341
  files: [...MLPACKAGE_FILES],
1098
1342
  runtimes: ["python"]
1099
1343
  },
1100
- openvino: {
1101
- url: hfScrypted("openvino/scrypted_yolov9c_relu/best.xml"),
1102
- sizeMB: 49,
1103
- runtimes: ["python"]
1104
- }
1344
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
1105
1345
  }
1106
1346
  },
1107
1347
  {
@@ -1126,13 +1366,23 @@ var OBJECT_DETECTION_MODELS = [
1126
1366
  files: [...MLPACKAGE_FILES],
1127
1367
  runtimes: ["python"]
1128
1368
  },
1129
- openvino: {
1130
- url: hfScrypted("openvino/scrypted_yolov9m_relu/best.xml"),
1131
- sizeMB: 38,
1132
- runtimes: ["python"]
1133
- }
1369
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
1134
1370
  }
1135
- }
1371
+ },
1372
+ ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5),
1373
+ ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3),
1374
+ ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15),
1375
+ ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8),
1376
+ ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5),
1377
+ ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
1378
+ ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19),
1379
+ ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
1380
+ ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41),
1381
+ ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
1382
+ ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50),
1383
+ ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
1384
+ ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112),
1385
+ ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56)
1136
1386
  ];
1137
1387
  var FACE_DETECTION_MODELS = [{
1138
1388
  id: "scrfd-2.5g",
@@ -1159,11 +1409,7 @@ var FACE_DETECTION_MODELS = [{
1159
1409
  files: [...MLPACKAGE_FILES],
1160
1410
  runtimes: ["python"]
1161
1411
  },
1162
- openvino: {
1163
- url: hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"),
1164
- sizeMB: 1.8,
1165
- runtimes: ["python"]
1166
- }
1412
+ openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
1167
1413
  }
1168
1414
  }, {
1169
1415
  id: "scrypted-yolov9t-face",
@@ -1190,11 +1436,7 @@ var FACE_DETECTION_MODELS = [{
1190
1436
  files: [...MLPACKAGE_FILES],
1191
1437
  runtimes: ["python"]
1192
1438
  },
1193
- openvino: {
1194
- url: hfScrypted("openvino/scrypted_yolov9t_relu_face/best.xml"),
1195
- sizeMB: 6,
1196
- runtimes: ["python"]
1197
- }
1439
+ openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
1198
1440
  }
1199
1441
  }];
1200
1442
  var FACE_EMBEDDING_MODELS = [{
@@ -1224,11 +1466,7 @@ var FACE_EMBEDDING_MODELS = [{
1224
1466
  files: [...MLPACKAGE_FILES],
1225
1467
  runtimes: ["python"]
1226
1468
  },
1227
- openvino: {
1228
- url: hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"),
1229
- sizeMB: 65,
1230
- runtimes: ["python"]
1231
- }
1469
+ openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
1232
1470
  }
1233
1471
  }, {
1234
1472
  id: "inception-resnet-v1",
@@ -1255,11 +1493,7 @@ var FACE_EMBEDDING_MODELS = [{
1255
1493
  files: [...MLPACKAGE_FILES],
1256
1494
  runtimes: ["python"]
1257
1495
  },
1258
- openvino: {
1259
- url: hfScrypted("openvino/inception_resnet_v1/best.xml"),
1260
- sizeMB: 45,
1261
- runtimes: ["python"]
1262
- }
1496
+ openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
1263
1497
  }
1264
1498
  }];
1265
1499
  var PLATE_DETECTION_MODELS = [{
@@ -1287,11 +1521,7 @@ var PLATE_DETECTION_MODELS = [{
1287
1521
  files: [...MLPACKAGE_FILES],
1288
1522
  runtimes: ["python"]
1289
1523
  },
1290
- openvino: {
1291
- url: hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"),
1292
- sizeMB: 6.1,
1293
- runtimes: ["python"]
1294
- }
1524
+ openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
1295
1525
  }
1296
1526
  }];
1297
1527
  var PLATE_OCR_MODELS = [{
@@ -1319,11 +1549,7 @@ var PLATE_OCR_MODELS = [{
1319
1549
  files: [...MLPACKAGE_FILES],
1320
1550
  runtimes: ["python"]
1321
1551
  },
1322
- openvino: {
1323
- url: hfScrypted("openvino/vgg_english_g2/best.xml"),
1324
- sizeMB: 7.2,
1325
- runtimes: ["python"]
1326
- }
1552
+ openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
1327
1553
  }
1328
1554
  }];
1329
1555
  var ANIMAL_CLASSIFIER_MODELS = [{
@@ -1352,11 +1578,7 @@ var ANIMAL_CLASSIFIER_MODELS = [{
1352
1578
  files: [...MLPACKAGE_FILES],
1353
1579
  runtimes: ["python"]
1354
1580
  },
1355
- openvino: {
1356
- url: hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"),
1357
- sizeMB: 164,
1358
- runtimes: ["python"]
1359
- }
1581
+ openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
1360
1582
  }
1361
1583
  }];
1362
1584
  var BIRD_CLASSIFIER_MODELS = [{
@@ -1385,11 +1607,7 @@ var BIRD_CLASSIFIER_MODELS = [{
1385
1607
  files: [...MLPACKAGE_FILES],
1386
1608
  runtimes: ["python"]
1387
1609
  },
1388
- openvino: {
1389
- url: hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"),
1390
- sizeMB: 47,
1391
- runtimes: ["python"]
1392
- }
1610
+ openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
1393
1611
  },
1394
1612
  extraFiles: [{
1395
1613
  url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
@@ -1423,11 +1641,7 @@ var VEHICLE_CLASSIFIER_MODELS = [{
1423
1641
  files: [...MLPACKAGE_FILES],
1424
1642
  runtimes: ["python"]
1425
1643
  },
1426
- openvino: {
1427
- url: hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"),
1428
- sizeMB: 68,
1429
- runtimes: ["python"]
1430
- }
1644
+ openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
1431
1645
  },
1432
1646
  extraFiles: [{
1433
1647
  url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
@@ -1460,11 +1674,7 @@ var SEGMENTATION_REFINER_MODELS = [{
1460
1674
  files: [...MLPACKAGE_FILES],
1461
1675
  runtimes: ["python"]
1462
1676
  },
1463
- openvino: {
1464
- url: hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"),
1465
- sizeMB: 2.5,
1466
- runtimes: ["python"]
1467
- }
1677
+ openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
1468
1678
  }
1469
1679
  }];
1470
1680
  var INSTANCE_SEGMENTATION_MODELS = [
@@ -1490,11 +1700,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1490
1700
  files: [...MLPACKAGE_FILES],
1491
1701
  runtimes: ["python"]
1492
1702
  },
1493
- openvino: {
1494
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"),
1495
- sizeMB: 11,
1496
- runtimes: ["python"]
1497
- }
1703
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
1498
1704
  }
1499
1705
  },
1500
1706
  {
@@ -1519,11 +1725,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1519
1725
  files: [...MLPACKAGE_FILES],
1520
1726
  runtimes: ["python"]
1521
1727
  },
1522
- openvino: {
1523
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"),
1524
- sizeMB: 40,
1525
- runtimes: ["python"]
1526
- }
1728
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
1527
1729
  }
1528
1730
  },
1529
1731
  {
@@ -1548,11 +1750,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1548
1750
  files: [...MLPACKAGE_FILES],
1549
1751
  runtimes: ["python"]
1550
1752
  },
1551
- openvino: {
1552
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"),
1553
- sizeMB: 90,
1554
- runtimes: ["python"]
1555
- }
1753
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
1556
1754
  }
1557
1755
  }
1558
1756
  ];
@@ -1568,16 +1766,12 @@ var AUDIO_CLASSIFIER_MODELS = [{
1568
1766
  preprocessMode: "resize",
1569
1767
  formats: {
1570
1768
  onnx: {
1571
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1769
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1572
1770
  sizeMB: 3.2
1573
1771
  },
1574
- openvino: {
1575
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1576
- sizeMB: 3.2,
1577
- runtimes: ["python"]
1578
- },
1772
+ openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
1579
1773
  coreml: {
1580
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1774
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1581
1775
  sizeMB: 3.2,
1582
1776
  runtimes: ["python"]
1583
1777
  }
@@ -1635,7 +1829,7 @@ var ObjectDetectionStep = class {
1635
1829
  "animal"
1636
1830
  ],
1637
1831
  models: [...OBJECT_DETECTION_MODELS],
1638
- defaultModelId: "yolov9s",
1832
+ defaultModelId: "yolo26n",
1639
1833
  defaultConfidence: .5,
1640
1834
  labels: require_dist.COCO_80_LABELS.map((l) => l.id),
1641
1835
  classMap: require_dist.COCO_TO_MACRO
@@ -1884,9 +2078,9 @@ var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
1884
2078
  enabledByDefault: false,
1885
2079
  defaultConfidence: .3
1886
2080
  });
1887
- var STEP_SEGMENTATION_REFINER = new PipelineStepBase({
1888
- id: "segmentation-refiner",
1889
- name: "Saliency Segmentation",
2081
+ var STEP_SEGMENTATION = new PipelineStepBase({
2082
+ id: "segmentation",
2083
+ name: "Segmentation",
1890
2084
  slot: "refiner",
1891
2085
  postprocessor: "saliency",
1892
2086
  extractMode: "crop-roi",
@@ -1898,7 +2092,7 @@ var STEP_SEGMENTATION_REFINER = new PipelineStepBase({
1898
2092
  defaultConfidence: 0,
1899
2093
  group: "Segmentation"
1900
2094
  });
1901
- var STEP_INSTANCE_SEGMENTATION = new PipelineStepBase({
2095
+ new PipelineStepBase({
1902
2096
  id: "instance-segmentation",
1903
2097
  name: "Instance Segmentation",
1904
2098
  slot: "refiner",
@@ -1925,8 +2119,7 @@ var ALL_PIPELINE_STEPS = [
1925
2119
  new AnimalClassifierStep(),
1926
2120
  STEP_BIRD_CLASSIFIER,
1927
2121
  STEP_VEHICLE_CLASSIFIER,
1928
- STEP_SEGMENTATION_REFINER,
1929
- STEP_INSTANCE_SEGMENTATION,
2122
+ STEP_SEGMENTATION,
1930
2123
  STEP_AUDIO_CLASSIFIER_INSTANCE
1931
2124
  ];
1932
2125
  /** Compat: flat array of StepDefinition for existing consumers */
@@ -2165,77 +2358,108 @@ var EngineFactory = class {
2165
2358
  }
2166
2359
  };
2167
2360
  //#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"
2361
+ //#region src/detection-pipeline/engine-provisioner.ts
2362
+ /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
2363
+ var BACKOFF_SCHEDULE_MS = [
2364
+ 5e3,
2365
+ 15e3,
2366
+ 3e4,
2367
+ 6e4,
2368
+ 12e4,
2369
+ 3e5
2190
2370
  ];
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;
2371
+ var IDLE_STATE = {
2372
+ runtimeId: null,
2373
+ device: null,
2374
+ state: "idle"
2375
+ };
2376
+ var EngineProvisioner = class {
2377
+ fx;
2378
+ current = IDLE_STATE;
2379
+ /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
2380
+ generation = 0;
2381
+ cancelTimer = null;
2382
+ retryIndex = 0;
2383
+ constructor(fx) {
2384
+ this.fx = fx;
2385
+ }
2386
+ get state() {
2387
+ return this.current;
2388
+ }
2389
+ isReady() {
2390
+ return this.current.state === "ready";
2391
+ }
2392
+ select(runtimeId, device) {
2393
+ this.generation++;
2394
+ this.retryIndex = 0;
2395
+ this.clearTimer();
2396
+ this.transition({
2397
+ runtimeId,
2398
+ device,
2399
+ state: "installing",
2400
+ progress: 0
2401
+ });
2402
+ this.provision(this.generation, runtimeId, device);
2403
+ }
2404
+ dispose() {
2405
+ this.generation++;
2406
+ this.clearTimer();
2407
+ }
2408
+ clearTimer() {
2409
+ if (this.cancelTimer !== null) {
2410
+ this.cancelTimer();
2411
+ this.cancelTimer = null;
2230
2412
  }
2231
- out[key] = store[key];
2232
2413
  }
2233
- for (const base of ENGINE_CASCADE_KEYS) {
2234
- const value = readNodeEngineValue(store, base, nodeId);
2235
- if (value !== void 0) out[base] = value;
2414
+ transition(next) {
2415
+ this.current = next;
2416
+ this.fx.onChange(next);
2236
2417
  }
2237
- return out;
2238
- }
2418
+ async provision(gen, runtimeId, device) {
2419
+ try {
2420
+ await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
2421
+ if (gen !== this.generation) return;
2422
+ this.transition({
2423
+ runtimeId,
2424
+ device,
2425
+ state: "verifying",
2426
+ progress: 100
2427
+ });
2428
+ await this.fx.verify(runtimeId, device);
2429
+ if (gen !== this.generation) return;
2430
+ this.retryIndex = 0;
2431
+ this.transition({
2432
+ runtimeId,
2433
+ device,
2434
+ state: "ready"
2435
+ });
2436
+ } catch (err) {
2437
+ if (gen !== this.generation) return;
2438
+ const message = err instanceof Error ? err.message : String(err);
2439
+ const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
2440
+ this.retryIndex++;
2441
+ const nextRetryAt = this.fx.now() + delay;
2442
+ this.transition({
2443
+ runtimeId,
2444
+ device,
2445
+ state: "failed",
2446
+ error: message,
2447
+ nextRetryAt
2448
+ });
2449
+ this.cancelTimer = this.fx.setTimer(delay, () => {
2450
+ if (gen !== this.generation) return;
2451
+ this.cancelTimer = null;
2452
+ this.transition({
2453
+ runtimeId,
2454
+ device,
2455
+ state: "installing",
2456
+ progress: 0
2457
+ });
2458
+ this.provision(gen, runtimeId, device);
2459
+ });
2460
+ }
2461
+ }
2462
+ };
2239
2463
  //#endregion
2240
2464
  //#region src/detection-pipeline/postprocess/dispatch.ts
2241
2465
  var VALID_KINDS = new Set([
@@ -3066,6 +3290,18 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
3066
3290
  parent.mask = output.mask;
3067
3291
  parent.maskWidth = output.maskWidth;
3068
3292
  parent.maskHeight = output.maskHeight;
3293
+ if (output.maskBbox !== void 0 && output.maskWidth > 0 && output.maskHeight > 0) {
3294
+ const [px1, py1, px2, py2] = parent.bbox;
3295
+ const parentW = px2 - px1;
3296
+ const parentH = py2 - py1;
3297
+ const [mbx, mby, mbw, mbh] = output.maskBbox;
3298
+ parent.refinedBbox = [
3299
+ px1 + mbx / output.maskWidth * parentW,
3300
+ py1 + mby / output.maskHeight * parentH,
3301
+ px1 + (mbx + mbw) / output.maskWidth * parentW,
3302
+ py1 + (mby + mbh) / output.maskHeight * parentH
3303
+ ];
3304
+ }
3069
3305
  break;
3070
3306
  }
3071
3307
  }
@@ -3116,6 +3352,12 @@ function buildFrameResult(input) {
3116
3352
  macroClass: m.macroClass,
3117
3353
  score: m.score,
3118
3354
  bbox,
3355
+ ...m.refinedBbox !== void 0 ? { refinedBbox: bboxTupleToRect(m.refinedBbox) } : {},
3356
+ ...m.mask !== void 0 && m.maskWidth !== void 0 && m.maskHeight !== void 0 ? {
3357
+ mask: m.mask,
3358
+ maskWidth: m.maskWidth,
3359
+ maskHeight: m.maskHeight
3360
+ } : {},
3119
3361
  labels,
3120
3362
  ...m.parentId !== void 0 ? { parentId: m.parentId } : {},
3121
3363
  ...embedding !== void 0 ? {
@@ -3547,338 +3789,97 @@ var PipelineExecutor = class {
3547
3789
  const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3548
3790
  if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3549
3791
  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));
3792
+ }
3793
+ };
3794
+ function capitalize(s) {
3795
+ if (s.length === 0) return s;
3796
+ return s.charAt(0).toUpperCase() + s.slice(1);
3708
3797
  }
3798
+ //#endregion
3799
+ //#region src/detection-pipeline/pipeline/tree-builder.ts
3709
3800
  /**
3710
- * Returns the device options for a given runtime and probed hardware.
3711
- * Delegates to `@camstack/types` `runtimeDevices`.
3801
+ * Build an executable tree from user config.
3802
+ *
3803
+ * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3804
+ * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3805
+ * Throws if step not loaded.
3712
3806
  */
3713
- function runtimeDevices(id, hardware) {
3714
- return require_dist.runtimeDevices(id, probedToHardwareInfo(hardware));
3807
+ function buildExecutableTree(steps, getEngine) {
3808
+ return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3715
3809
  }
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);
3810
+ function buildNode(step, getEngine) {
3811
+ const definition = getStepDefinition(step.addonId);
3812
+ const engine = getEngine(step.addonId);
3813
+ const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3814
+ const mergedSettings = {
3815
+ ...collectSchemaDefaults(step.addonId),
3816
+ ...step.settings
3817
+ };
3818
+ return {
3819
+ stepId: step.addonId,
3820
+ definition,
3821
+ engine,
3822
+ modelId: step.modelId,
3823
+ inputClasses: definition.inputClasses ?? [],
3824
+ enabled: step.enabled,
3825
+ children,
3826
+ ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3827
+ };
3722
3828
  }
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
3829
  /**
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.
3830
+ * Walk the step's declared `getConfigSchema()` and collect every field
3831
+ * with a primitive `default` value into a plain record. Group fields
3832
+ * are flattened — a nested `{ type: 'group', fields: [...] }` is
3833
+ * entered recursively. Fields without a declared default are skipped.
3733
3834
  */
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;
3835
+ function collectSchemaDefaults(stepId) {
3836
+ const schema = getStep(stepId).getConfigSchema();
3837
+ const out = {};
3838
+ walkFieldsForDefaults(schema, out);
3839
+ return out;
3742
3840
  }
3743
- function runtimeLabel(id) {
3744
- return RUNTIME_DETAIL[id].label;
3841
+ function walkFieldsForDefaults(fields, out) {
3842
+ for (const field of fields) {
3843
+ if ("type" in field && field.type === "group" && "fields" in field) {
3844
+ walkFieldsForDefaults(field.fields, out);
3845
+ continue;
3846
+ }
3847
+ if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3848
+ }
3745
3849
  }
3850
+ //#endregion
3851
+ //#region src/detection-pipeline/registry/custom-models.ts
3746
3852
  /**
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.
3853
+ * Group a flat list of custom-model descriptors (as returned by the
3854
+ * `custom-model-registry` collection cap) into a `stepId entries` map for
3855
+ * the picker / resolution union. Pure; order within a step preserved.
3750
3856
  */
3751
- function shouldInstallOpenvino(env) {
3752
- if (env.platform === "darwin") return false;
3753
- return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3857
+ function groupCustomModelsByStep(descriptors) {
3858
+ const byStep = /* @__PURE__ */ new Map();
3859
+ for (const d of descriptors) {
3860
+ const arr = byStep.get(d.stepId) ?? [];
3861
+ arr.push(d.entry);
3862
+ byStep.set(d.stepId, arr);
3863
+ }
3864
+ return byStep;
3754
3865
  }
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
3796
- });
3797
- this.provision(this.generation, runtimeId, device);
3798
- }
3799
- dispose() {
3800
- this.generation++;
3801
- this.clearTimer();
3802
- }
3803
- clearTimer() {
3804
- if (this.cancelTimer !== null) {
3805
- this.cancelTimer();
3806
- this.cancelTimer = null;
3807
- }
3808
- }
3809
- transition(next) {
3810
- this.current = next;
3811
- this.fx.onChange(next);
3812
- }
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
- });
3855
- }
3856
- }
3857
- };
3858
- //#endregion
3859
- //#region src/detection-pipeline/auto-pick.ts
3860
- var PREFERENCE = [
3861
- "coreml",
3862
- "openvino",
3863
- "onnx"
3864
- ];
3865
3866
  /**
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).
3867
+ * Union a step's static catalog models with operator-registered custom
3868
+ * models. On an `id` collision the static catalog entry wins — a custom
3869
+ * model can never shadow a built-in one.
3872
3870
  *
3873
- * Device is always `defaultDeviceFor(chosen)`.
3871
+ * Pure + side-effect-free so it can be unit-tested in isolation and called
3872
+ * from the (free) `buildSchemaSlots` builder without any addon context.
3874
3873
  */
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";
3878
- return {
3879
- runtimeId: chosen,
3880
- device: defaultDeviceFor(chosen)
3881
- };
3874
+ function mergeCustomModels(staticModels, customModels) {
3875
+ const seen = new Set(staticModels.map((m) => m.id));
3876
+ const merged = [...staticModels];
3877
+ for (const m of customModels) {
3878
+ if (seen.has(m.id)) continue;
3879
+ seen.add(m.id);
3880
+ merged.push(m);
3881
+ }
3882
+ return merged;
3882
3883
  }
3883
3884
  //#endregion
3884
3885
  //#region src/detection-pipeline/provider.ts
@@ -4116,6 +4117,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4116
4117
  /** Addon context — ctx.api resolves lazily (direct caller created after boot) */
4117
4118
  addonCtx = null;
4118
4119
  /**
4120
+ * Short-lived cache of custom models pulled from the `custom-model-registry`
4121
+ * collection cap, grouped by step id. TTL-bounded so the picker / resolution
4122
+ * paths don't issue a cap round-trip on every call. Empty map = no provider
4123
+ * (or a query failure) → behaviour identical to the static-catalog-only path.
4124
+ */
4125
+ customModelsCache = null;
4126
+ /**
4119
4127
  * Per-device {@link DeviceProxy} cache used for zone gating at the
4120
4128
  * runtime path. Reads `state.zones.value` + `state.zoneRules.value`
4121
4129
  * synchronously per frame so detections inside an `exclude` zone
@@ -4161,6 +4169,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4161
4169
  */
4162
4170
  needsAutoPick = false;
4163
4171
  /**
4172
+ * Unsubscribe handle for the deferred-auto-pick `platform-probe` ready
4173
+ * listener (armed in `setApi` when the probe isn't ready yet). Cleared once
4174
+ * the engine is resolved — either by the listener firing or by the boot
4175
+ * safety-net `ensureBootEngineProvisioned`.
4176
+ */
4177
+ deferredAutoPickUnsub = null;
4178
+ /**
4164
4179
  * Warm cache for benchmark engine-override runs.
4165
4180
  *
4166
4181
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -4193,7 +4208,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4193
4208
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4194
4209
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4195
4210
  this.currentEngine = ONNX_FLOOR;
4196
- this.log.info("Engine selected (default)", { meta: {
4211
+ this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4197
4212
  runtime: this.currentEngine.runtime,
4198
4213
  backend: this.currentEngine.backend,
4199
4214
  format: this.currentEngine.format
@@ -4266,26 +4281,70 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4266
4281
  this.needsAutoPick = false;
4267
4282
  this.startProvisioningForCurrentEngine();
4268
4283
  } else {
4269
- this.startProvisioningForCurrentEngine();
4270
4284
  const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4271
4285
  if (state !== "ready") return;
4272
- unsubscribe();
4286
+ this.cancelDeferredAutoPick();
4273
4287
  if (!this.needsAutoPick) return;
4274
4288
  this.autoPickAndPersist().then(() => {
4275
4289
  this.needsAutoPick = false;
4276
4290
  this.startProvisioningForCurrentEngine();
4277
4291
  });
4278
4292
  });
4279
- this.addonCtx.addDisposer(unsubscribe);
4293
+ this.deferredAutoPickUnsub = unsubscribe;
4294
+ this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
4280
4295
  }
4281
4296
  else this.startProvisioningForCurrentEngine();
4282
4297
  }
4298
+ /** Tear down the deferred-auto-pick probe listener, if still armed. */
4299
+ cancelDeferredAutoPick() {
4300
+ if (this.deferredAutoPickUnsub) {
4301
+ this.deferredAutoPickUnsub();
4302
+ this.deferredAutoPickUnsub = null;
4303
+ }
4304
+ }
4305
+ /**
4306
+ * Boot safety-net: deterministically provision the hardware-probed engine.
4307
+ *
4308
+ * Called from the addon's `onInitialize` right after `reprobeEngine` has
4309
+ * written this node's probe-driven selection (e.g. `engineBackend=openvino`)
4310
+ * to the store. When first-boot auto-pick was DEFERRED (probe not ready at
4311
+ * `setApi`), the engine would otherwise stay `idle` — selected but never
4312
+ * provisioned — because the `onCapabilityStateChange('platform-probe')` ready
4313
+ * edge can be missed when the cap is already 'ready' by the time we subscribe
4314
+ * (the old eager-onnx-floor masked this; removing it surfaced a node that
4315
+ * boots with no engine at all). This loads the now-persisted selection and
4316
+ * starts provisioning it, so the node reliably comes up on its real best
4317
+ * engine (openvino on Intel) with no onnx floor and no missed boot. No-op
4318
+ * when provisioning already started (persisted-engine path / listener fired)
4319
+ * or when nothing has been selected yet.
4320
+ */
4321
+ async ensureBootEngineProvisioned() {
4322
+ if (this.getEngineProvisioning().state !== "idle") return;
4323
+ const stored = await this.loadEngine();
4324
+ if (!stored) return;
4325
+ this.currentEngine = stored;
4326
+ this.needsAutoPick = false;
4327
+ this.cancelDeferredAutoPick();
4328
+ this.log.info("Boot engine provisioning from probed selection", { meta: {
4329
+ runtime: stored.runtime,
4330
+ backend: stored.backend,
4331
+ device: stored.device ?? null
4332
+ } });
4333
+ this.startProvisioningForCurrentEngine();
4334
+ }
4283
4335
  /**
4284
4336
  * Auto-pick the best supported runtime at first boot (no stored engine).
4285
4337
  * Uses the platform-probe cap's hardware + bestScore hint when available;
4286
4338
  * falls back to platform/arch when the probe cap is not yet reachable.
4339
+ *
4287
4340
  * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4288
- * boots load it via `loadEngine()` and skip this path.
4341
+ * boots load it via `loadEngine()` and skip this path — but ONLY when the
4342
+ * probe actually answered (real `hardware` or a `bestScore` hint). If the
4343
+ * probe query failed (cold-start race, cap momentarily unreachable), the
4344
+ * pick floors to onnx purely for lack of information; persisting that would
4345
+ * LOCK onnx and skip auto-pick on every future boot even after the
4346
+ * accelerator surfaces. In that case we set the in-memory floor for liveness
4347
+ * but leave the store untouched so the next boot re-attempts the pick.
4289
4348
  */
4290
4349
  async autoPickAndPersist() {
4291
4350
  let hardware = null;
@@ -4307,6 +4366,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4307
4366
  device: pick.device
4308
4367
  };
4309
4368
  this.currentEngine = engine;
4369
+ if (!(hardware !== null || bestBackendHint !== null)) {
4370
+ this.log.warn("Auto-pick: probe returned no hardware/hint — using onnx floor WITHOUT persisting", { meta: {
4371
+ backend: pick.runtimeId,
4372
+ device: pick.device
4373
+ } });
4374
+ return;
4375
+ }
4310
4376
  const apNode = this.localProbeNodeId();
4311
4377
  await this.writeStore({
4312
4378
  [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
@@ -4386,8 +4452,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4386
4452
  * runtime through installing → verifying → ready.
4387
4453
  */
4388
4454
  async onEngineSelectionChanged() {
4455
+ const prev = this.currentEngine;
4389
4456
  const stored = await this.loadEngine();
4390
4457
  if (stored) this.currentEngine = stored;
4458
+ if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
4459
+ this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4460
+ from: `${prev.backend}/${prev.device ?? "default"}`,
4461
+ to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
4462
+ } });
4463
+ try {
4464
+ await this.engineFactory.dispose();
4465
+ } catch {}
4466
+ this.engineFactory = null;
4467
+ this.executor = null;
4468
+ }
4391
4469
  this.startProvisioningForCurrentEngine();
4392
4470
  }
4393
4471
  /**
@@ -4425,9 +4503,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4425
4503
  if (!steps || steps.length === 0) return;
4426
4504
  for (const step of flattenSteps(steps)) {
4427
4505
  if (!step.enabled) continue;
4428
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4506
+ const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
4429
4507
  if (!modelEntry) continue;
4430
- if (require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4508
+ if (require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4431
4509
  await this.downloadWithRetry(modelEntry, format, 3);
4432
4510
  }
4433
4511
  }
@@ -4503,10 +4581,44 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4503
4581
  return { hardware: null };
4504
4582
  }
4505
4583
  }
4584
+ /**
4585
+ * Pull custom models from the `custom-model-registry` collection cap,
4586
+ * grouped by step id. 5s TTL cache. Fully graceful: when no provider is
4587
+ * registered (or the query fails) it returns an empty map and logs at most
4588
+ * a warning — callers then behave exactly like the static-catalog path.
4589
+ */
4590
+ async getCustomModels() {
4591
+ const now = Date.now();
4592
+ if (this.customModelsCache && now - this.customModelsCache.at < 5e3) return this.customModelsCache.byStep;
4593
+ let byStep = /* @__PURE__ */ new Map();
4594
+ try {
4595
+ const api = this.addonCtx?.api;
4596
+ if (api) {
4597
+ if ((await api.addons.listCapabilityProviders.query({ capName: "custom-model-registry" })).some((p) => p.isActive)) byStep = groupCustomModelsByStep(await api.customModelRegistry.listModels.query());
4598
+ }
4599
+ } catch (err) {
4600
+ this.log.warn("custom-model-registry query failed — using static catalog only", { meta: { error: require_dist.errMsg(err) } });
4601
+ }
4602
+ this.customModelsCache = {
4603
+ at: now,
4604
+ byStep
4605
+ };
4606
+ return byStep;
4607
+ }
4608
+ /**
4609
+ * Resolve a model id within a step to a catalog entry — static catalog
4610
+ * first, then the custom registry. Returns undefined if neither has it.
4611
+ */
4612
+ async resolveModelEntry(addonId, modelId) {
4613
+ const fromCatalog = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4614
+ if (fromCatalog) return fromCatalog;
4615
+ return (await this.getCustomModels()).get(addonId)?.find((m) => m.id === modelId);
4616
+ }
4506
4617
  async getSchema(engine) {
4507
4618
  if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
4508
4619
  const format = engine.format;
4509
- const slots = buildSchemaSlots(format, this.modelsDir);
4620
+ const customByStep = await this.getCustomModels();
4621
+ const slots = buildSchemaSlots(format, this.modelsDir, customByStep);
4510
4622
  const { hardware } = await this.fetchProbeGatingData();
4511
4623
  const env = runtimeEnvFromProcess(toProbedHardware(hardware));
4512
4624
  return {
@@ -4635,7 +4747,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4635
4747
  const formats = {};
4636
4748
  for (const [formatKey, entry] of Object.entries(m.formats)) {
4637
4749
  if (!entry) continue;
4638
- const downloaded = require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, m, formatKey);
4750
+ const downloaded = require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, m, formatKey);
4639
4751
  formats[formatKey] = {
4640
4752
  url: entry.url,
4641
4753
  sizeMB: entry.sizeMB,
@@ -4693,7 +4805,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4693
4805
  }
4694
4806
  async downloadModel(input) {
4695
4807
  const { modelId, format, addonId } = input;
4696
- const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4808
+ const modelEntry = await this.resolveModelEntry(addonId, modelId);
4697
4809
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4698
4810
  const formatEntry = modelEntry.formats[format];
4699
4811
  if (!formatEntry) throw new Error(`Model "${modelId}" has no ${format} format. Available: ${Object.keys(modelEntry.formats).join(", ")}`);
@@ -4756,9 +4868,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4756
4868
  }
4757
4869
  async deleteModel(input) {
4758
4870
  const { modelId, format, addonId } = input;
4759
- const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4871
+ const modelEntry = await this.resolveModelEntry(addonId, modelId);
4760
4872
  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`);
4873
+ if (!require_model_download_service_C_IHWnXx.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4762
4874
  this.log.info("Model deleted from disk", { meta: {
4763
4875
  modelId,
4764
4876
  format
@@ -5212,8 +5324,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5212
5324
  const work = (async () => {
5213
5325
  const format = this.currentEngine?.format ?? "onnx";
5214
5326
  for (const step of needed) {
5215
- 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)) {
5327
+ const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5328
+ if (modelEntry && !require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) {
5217
5329
  this.log.info("Downloading model for step", { meta: {
5218
5330
  modelId: step.modelId,
5219
5331
  format,
@@ -5238,7 +5350,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5238
5350
  /** Download a model with retry + exponential backoff */
5239
5351
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
5240
5352
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
5241
- await require_model_download_service_C7AjBsX9.ensureModel(this.modelsDir, entry, format, onProgress);
5353
+ await require_model_download_service_C_IHWnXx.ensureModel(this.modelsDir, entry, format, onProgress);
5242
5354
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
5243
5355
  return;
5244
5356
  } catch (err) {
@@ -5707,7 +5819,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5707
5819
  const format = this.currentEngine.format;
5708
5820
  const downloads = [];
5709
5821
  for (const step of flattenSteps(steps)) {
5710
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
5822
+ const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5711
5823
  if (!modelEntry) {
5712
5824
  this.log.warn("Model not found in step catalog — skipping download", { meta: {
5713
5825
  modelId: step.modelId,
@@ -5715,11 +5827,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5715
5827
  } });
5716
5828
  continue;
5717
5829
  }
5718
- if (!require_model_download_service_C7AjBsX9.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5830
+ if (!require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5719
5831
  modelId: step.modelId,
5720
5832
  format
5721
5833
  } });
5722
- downloads.push(require_model_download_service_C7AjBsX9.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
5834
+ downloads.push(require_model_download_service_C_IHWnXx.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
5723
5835
  }
5724
5836
  await Promise.all(downloads);
5725
5837
  await this.ensureBackendDeps(this.currentEngine);
@@ -6110,11 +6222,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6110
6222
  }
6111
6223
  }
6112
6224
  };
6113
- function buildSchemaSlots(format, modelsDir) {
6225
+ function buildSchemaSlots(format, modelsDir, customByStep) {
6114
6226
  const slotMap = /* @__PURE__ */ new Map();
6115
6227
  for (const pipelineStep of ALL_PIPELINE_STEPS) {
6116
6228
  const step = pipelineStep.definition;
6117
- const availableModels = step.models.filter((m) => m.formats[format]);
6229
+ const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format]);
6118
6230
  if (availableModels.length === 0) continue;
6119
6231
  const slot = step.slot;
6120
6232
  if (!slotMap.has(slot)) slotMap.set(slot, []);
@@ -6130,7 +6242,7 @@ function buildSchemaSlots(format, modelsDir) {
6130
6242
  id: m.id,
6131
6243
  name: m.name,
6132
6244
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
6133
- downloaded: require_model_download_service_C7AjBsX9.isModelDownloaded(modelsDir, m, f),
6245
+ downloaded: require_model_download_service_C_IHWnXx.isModelDownloaded(modelsDir, m, f),
6134
6246
  sizeMB: entry.sizeMB
6135
6247
  }]))
6136
6248
  })),
@@ -6219,8 +6331,7 @@ function buildDefaultStepTree(format) {
6219
6331
  makeStep("animal-classifier", [], { enabled: false }),
6220
6332
  makeStep("bird-classifier", [], { enabled: false }),
6221
6333
  makeStep("vehicle-classifier", [], { enabled: false }),
6222
- makeStep("segmentation-refiner", [], { enabled: false }),
6223
- makeStep("instance-segmentation", [], { enabled: false })
6334
+ makeStep("segmentation", [], { enabled: false })
6224
6335
  ].filter((s) => s !== null));
6225
6336
  const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
6226
6337
  runtime: "python",
@@ -6510,8 +6621,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6510
6621
  label: "Execution provider",
6511
6622
  options: [...STATIC_BACKEND_OPTIONS],
6512
6623
  default: DEFAULT_CONFIG.engineBackend,
6513
- immediate: true,
6514
- requiresRestart: true
6624
+ immediate: true
6515
6625
  }),
6516
6626
  this.field({
6517
6627
  type: "select",
@@ -6519,8 +6629,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6519
6629
  label: "Hardware device",
6520
6630
  options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
6521
6631
  default: DEFAULT_CONFIG.engineDevice,
6522
- immediate: true,
6523
- requiresRestart: true
6632
+ immediate: true
6524
6633
  })
6525
6634
  ]
6526
6635
  }, {
@@ -6559,8 +6668,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6559
6668
  "onnx",
6560
6669
  "cpu"
6561
6670
  ]
6562
- },
6563
- requiresRestart: true
6671
+ }
6564
6672
  }),
6565
6673
  this.field({
6566
6674
  type: "slider",
@@ -6577,8 +6685,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6577
6685
  showWhen: {
6578
6686
  field: "batchMode",
6579
6687
  notEquals: "none"
6580
- },
6581
- requiresRestart: true
6688
+ }
6582
6689
  }),
6583
6690
  this.field({
6584
6691
  type: "slider",
@@ -6595,8 +6702,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6595
6702
  showWhen: {
6596
6703
  field: "batchMode",
6597
6704
  notEquals: "none"
6598
- },
6599
- requiresRestart: true
6705
+ }
6600
6706
  }),
6601
6707
  this.field({
6602
6708
  type: "slider",
@@ -6609,8 +6715,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6609
6715
  default: void 0,
6610
6716
  showValue: true,
6611
6717
  nullable: true,
6612
- nullLabel: "Auto",
6613
- requiresRestart: true
6718
+ nullLabel: "Auto"
6614
6719
  }),
6615
6720
  this.field({
6616
6721
  type: "slider",
@@ -6623,8 +6728,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6623
6728
  default: void 0,
6624
6729
  showValue: true,
6625
6730
  nullable: true,
6626
- nullLabel: "Auto",
6627
- requiresRestart: true
6731
+ nullLabel: "Auto"
6628
6732
  })
6629
6733
  ]
6630
6734
  }] });
@@ -6887,6 +6991,9 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6887
6991
  if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6888
6992
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6889
6993
  });
6994
+ await this.provider.ensureBootEngineProvisioned().catch((err) => {
6995
+ this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6996
+ });
6890
6997
  await this.provider.warmPool();
6891
6998
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
6892
6999
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -6994,16 +7101,17 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6994
7101
  return false;
6995
7102
  }
6996
7103
  /**
6997
- * BaseAddon calls `onConfigChanged` after every settings write.
6998
- * Group-runner addons can't honour the schema's `requiresRestart:
6999
- * true` flag (the restart cap returns "process not found" for
7000
- * processes spawned in a kernel group worker). To make tuning
7001
- * changes actually take effect, watch the pool-bound subset and
7002
- * respawn the provider in place when it flips.
7003
- *
7004
- * Engine cascade (`engineRuntime/Backend/Device`) and audio
7005
- * settings don't require this those still rely on the addon
7006
- * lifecycle (engineFactory rebuild on next runPipeline).
7104
+ * BaseAddon calls `onConfigChanged` after every settings write. This is the
7105
+ * SOLE apply path for engine + tuning changes — none of those fields declare
7106
+ * `requiresRestart` anymore (an addon restart re-probes hardware + reloads
7107
+ * every model on every set, which is exactly what we want to avoid). Instead:
7108
+ * - Pool-bound tuning (`concurrency`/`batchMode`/`windowMs`/…) in-place
7109
+ * pool respawn when the snapshot flips (`poolConfigChanged` below).
7110
+ * - Engine cascade (`engineBackend`/`engineDevice`) → the provider's
7111
+ * `onEngineSelectionChanged` disposes the device-bound factory so the next
7112
+ * runPipeline rebuilds the pool on the new selection, in place.
7113
+ * Both apply optimistically the inference gate stays closed for the short
7114
+ * re-spin, frames are dropped (never crashed), no addon bounce.
7007
7115
  */
7008
7116
  async onConfigChanged() {
7009
7117
  await this.refreshNodeEngineFromStore();