@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
@@ -1,11 +1,255 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-BA6DR_jV.mjs";
3
- import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C7AjBsX9-B0ekM6dF.mjs";
2
+ import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-BWc-HYQz.mjs";
3
+ import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-3Mmeob3l.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,46 @@ 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
+ };
1039
+ /**
1040
+ * Build a precision-variant catalog entry (OpenVINO-only) derived from a base
1041
+ * detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
1042
+ * NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
1043
+ * accuracy cost). The IRs live next to the base `.xml` on HF as
1044
+ * `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
1045
+ * (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
1046
+ */
1047
+ var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
1048
+ id: `${baseId}-${precision}`,
1049
+ name: `${baseName} (${precision.toUpperCase()})`,
1050
+ description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
1051
+ inputSize: {
1052
+ width: 640,
1053
+ height: 640
1054
+ },
1055
+ labels: [],
1056
+ preprocessMode: "letterbox",
1057
+ formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) }
1058
+ });
775
1059
  var MLPACKAGE_FILES = [
776
1060
  "Manifest.json",
777
1061
  "Data/com.apple.CoreML/model.mlmodel",
@@ -800,11 +1084,7 @@ var OBJECT_DETECTION_MODELS = [
800
1084
  files: [...MLPACKAGE_FILES],
801
1085
  runtimes: ["python"]
802
1086
  },
803
- openvino: {
804
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"),
805
- sizeMB: 6,
806
- runtimes: ["python"]
807
- }
1087
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
808
1088
  }
809
1089
  },
810
1090
  {
@@ -829,11 +1109,7 @@ var OBJECT_DETECTION_MODELS = [
829
1109
  files: [...MLPACKAGE_FILES],
830
1110
  runtimes: ["python"]
831
1111
  },
832
- openvino: {
833
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"),
834
- sizeMB: 16,
835
- runtimes: ["python"]
836
- }
1112
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
837
1113
  }
838
1114
  },
839
1115
  {
@@ -858,11 +1134,7 @@ var OBJECT_DETECTION_MODELS = [
858
1134
  files: [...MLPACKAGE_FILES],
859
1135
  runtimes: ["python"]
860
1136
  },
861
- openvino: {
862
- url: hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"),
863
- sizeMB: 49,
864
- runtimes: ["python"]
865
- }
1137
+ openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
866
1138
  }
867
1139
  },
868
1140
  {
@@ -887,11 +1159,7 @@ var OBJECT_DETECTION_MODELS = [
887
1159
  files: [...MLPACKAGE_FILES],
888
1160
  runtimes: ["python"]
889
1161
  },
890
- openvino: {
891
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"),
892
- sizeMB: 9,
893
- runtimes: ["python"]
894
- }
1162
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
895
1163
  }
896
1164
  },
897
1165
  {
@@ -916,11 +1184,7 @@ var OBJECT_DETECTION_MODELS = [
916
1184
  files: [...MLPACKAGE_FILES],
917
1185
  runtimes: ["python"]
918
1186
  },
919
- openvino: {
920
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"),
921
- sizeMB: 36,
922
- runtimes: ["python"]
923
- }
1187
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
924
1188
  }
925
1189
  },
926
1190
  {
@@ -945,11 +1209,7 @@ var OBJECT_DETECTION_MODELS = [
945
1209
  files: [...MLPACKAGE_FILES],
946
1210
  runtimes: ["python"]
947
1211
  },
948
- openvino: {
949
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"),
950
- sizeMB: 78,
951
- runtimes: ["python"]
952
- }
1212
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
953
1213
  }
954
1214
  },
955
1215
  {
@@ -974,11 +1234,7 @@ var OBJECT_DETECTION_MODELS = [
974
1234
  files: [...MLPACKAGE_FILES],
975
1235
  runtimes: ["python"]
976
1236
  },
977
- openvino: {
978
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"),
979
- sizeMB: 95,
980
- runtimes: ["python"]
981
- }
1237
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
982
1238
  }
983
1239
  },
984
1240
  {
@@ -1003,11 +1259,7 @@ var OBJECT_DETECTION_MODELS = [
1003
1259
  files: [...MLPACKAGE_FILES],
1004
1260
  runtimes: ["python"]
1005
1261
  },
1006
- openvino: {
1007
- url: hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"),
1008
- sizeMB: 213,
1009
- runtimes: ["python"]
1010
- }
1262
+ openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
1011
1263
  }
1012
1264
  },
1013
1265
  {
@@ -1032,11 +1284,7 @@ var OBJECT_DETECTION_MODELS = [
1032
1284
  files: [...MLPACKAGE_FILES],
1033
1285
  runtimes: ["python"]
1034
1286
  },
1035
- openvino: {
1036
- url: hfScrypted("openvino/scrypted_yolov9t_relu/best.xml"),
1037
- sizeMB: 6,
1038
- runtimes: ["python"]
1039
- }
1287
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
1040
1288
  }
1041
1289
  },
1042
1290
  {
@@ -1061,11 +1309,7 @@ var OBJECT_DETECTION_MODELS = [
1061
1309
  files: [...MLPACKAGE_FILES],
1062
1310
  runtimes: ["python"]
1063
1311
  },
1064
- openvino: {
1065
- url: hfScrypted("openvino/scrypted_yolov9s_relu/best.xml"),
1066
- sizeMB: 16,
1067
- runtimes: ["python"]
1068
- }
1312
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
1069
1313
  }
1070
1314
  },
1071
1315
  {
@@ -1090,11 +1334,7 @@ var OBJECT_DETECTION_MODELS = [
1090
1334
  files: [...MLPACKAGE_FILES],
1091
1335
  runtimes: ["python"]
1092
1336
  },
1093
- openvino: {
1094
- url: hfScrypted("openvino/scrypted_yolov9c_relu/best.xml"),
1095
- sizeMB: 49,
1096
- runtimes: ["python"]
1097
- }
1337
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
1098
1338
  }
1099
1339
  },
1100
1340
  {
@@ -1119,13 +1359,23 @@ var OBJECT_DETECTION_MODELS = [
1119
1359
  files: [...MLPACKAGE_FILES],
1120
1360
  runtimes: ["python"]
1121
1361
  },
1122
- openvino: {
1123
- url: hfScrypted("openvino/scrypted_yolov9m_relu/best.xml"),
1124
- sizeMB: 38,
1125
- runtimes: ["python"]
1126
- }
1362
+ openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
1127
1363
  }
1128
- }
1364
+ },
1365
+ ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5),
1366
+ ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3),
1367
+ ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15),
1368
+ ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8),
1369
+ ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5),
1370
+ ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
1371
+ ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19),
1372
+ ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
1373
+ ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41),
1374
+ ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
1375
+ ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50),
1376
+ ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
1377
+ ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112),
1378
+ ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56)
1129
1379
  ];
1130
1380
  var FACE_DETECTION_MODELS = [{
1131
1381
  id: "scrfd-2.5g",
@@ -1152,11 +1402,7 @@ var FACE_DETECTION_MODELS = [{
1152
1402
  files: [...MLPACKAGE_FILES],
1153
1403
  runtimes: ["python"]
1154
1404
  },
1155
- openvino: {
1156
- url: hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"),
1157
- sizeMB: 1.8,
1158
- runtimes: ["python"]
1159
- }
1405
+ openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
1160
1406
  }
1161
1407
  }, {
1162
1408
  id: "scrypted-yolov9t-face",
@@ -1183,11 +1429,7 @@ var FACE_DETECTION_MODELS = [{
1183
1429
  files: [...MLPACKAGE_FILES],
1184
1430
  runtimes: ["python"]
1185
1431
  },
1186
- openvino: {
1187
- url: hfScrypted("openvino/scrypted_yolov9t_relu_face/best.xml"),
1188
- sizeMB: 6,
1189
- runtimes: ["python"]
1190
- }
1432
+ openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
1191
1433
  }
1192
1434
  }];
1193
1435
  var FACE_EMBEDDING_MODELS = [{
@@ -1217,11 +1459,7 @@ var FACE_EMBEDDING_MODELS = [{
1217
1459
  files: [...MLPACKAGE_FILES],
1218
1460
  runtimes: ["python"]
1219
1461
  },
1220
- openvino: {
1221
- url: hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"),
1222
- sizeMB: 65,
1223
- runtimes: ["python"]
1224
- }
1462
+ openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
1225
1463
  }
1226
1464
  }, {
1227
1465
  id: "inception-resnet-v1",
@@ -1248,11 +1486,7 @@ var FACE_EMBEDDING_MODELS = [{
1248
1486
  files: [...MLPACKAGE_FILES],
1249
1487
  runtimes: ["python"]
1250
1488
  },
1251
- openvino: {
1252
- url: hfScrypted("openvino/inception_resnet_v1/best.xml"),
1253
- sizeMB: 45,
1254
- runtimes: ["python"]
1255
- }
1489
+ openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
1256
1490
  }
1257
1491
  }];
1258
1492
  var PLATE_DETECTION_MODELS = [{
@@ -1280,11 +1514,7 @@ var PLATE_DETECTION_MODELS = [{
1280
1514
  files: [...MLPACKAGE_FILES],
1281
1515
  runtimes: ["python"]
1282
1516
  },
1283
- openvino: {
1284
- url: hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"),
1285
- sizeMB: 6.1,
1286
- runtimes: ["python"]
1287
- }
1517
+ openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
1288
1518
  }
1289
1519
  }];
1290
1520
  var PLATE_OCR_MODELS = [{
@@ -1312,11 +1542,7 @@ var PLATE_OCR_MODELS = [{
1312
1542
  files: [...MLPACKAGE_FILES],
1313
1543
  runtimes: ["python"]
1314
1544
  },
1315
- openvino: {
1316
- url: hfScrypted("openvino/vgg_english_g2/best.xml"),
1317
- sizeMB: 7.2,
1318
- runtimes: ["python"]
1319
- }
1545
+ openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
1320
1546
  }
1321
1547
  }];
1322
1548
  var ANIMAL_CLASSIFIER_MODELS = [{
@@ -1345,11 +1571,7 @@ var ANIMAL_CLASSIFIER_MODELS = [{
1345
1571
  files: [...MLPACKAGE_FILES],
1346
1572
  runtimes: ["python"]
1347
1573
  },
1348
- openvino: {
1349
- url: hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"),
1350
- sizeMB: 164,
1351
- runtimes: ["python"]
1352
- }
1574
+ openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
1353
1575
  }
1354
1576
  }];
1355
1577
  var BIRD_CLASSIFIER_MODELS = [{
@@ -1378,11 +1600,7 @@ var BIRD_CLASSIFIER_MODELS = [{
1378
1600
  files: [...MLPACKAGE_FILES],
1379
1601
  runtimes: ["python"]
1380
1602
  },
1381
- openvino: {
1382
- url: hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"),
1383
- sizeMB: 47,
1384
- runtimes: ["python"]
1385
- }
1603
+ openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
1386
1604
  },
1387
1605
  extraFiles: [{
1388
1606
  url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
@@ -1416,11 +1634,7 @@ var VEHICLE_CLASSIFIER_MODELS = [{
1416
1634
  files: [...MLPACKAGE_FILES],
1417
1635
  runtimes: ["python"]
1418
1636
  },
1419
- openvino: {
1420
- url: hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"),
1421
- sizeMB: 68,
1422
- runtimes: ["python"]
1423
- }
1637
+ openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
1424
1638
  },
1425
1639
  extraFiles: [{
1426
1640
  url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
@@ -1453,11 +1667,7 @@ var SEGMENTATION_REFINER_MODELS = [{
1453
1667
  files: [...MLPACKAGE_FILES],
1454
1668
  runtimes: ["python"]
1455
1669
  },
1456
- openvino: {
1457
- url: hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"),
1458
- sizeMB: 2.5,
1459
- runtimes: ["python"]
1460
- }
1670
+ openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
1461
1671
  }
1462
1672
  }];
1463
1673
  var INSTANCE_SEGMENTATION_MODELS = [
@@ -1483,11 +1693,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1483
1693
  files: [...MLPACKAGE_FILES],
1484
1694
  runtimes: ["python"]
1485
1695
  },
1486
- openvino: {
1487
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"),
1488
- sizeMB: 11,
1489
- runtimes: ["python"]
1490
- }
1696
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
1491
1697
  }
1492
1698
  },
1493
1699
  {
@@ -1512,11 +1718,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1512
1718
  files: [...MLPACKAGE_FILES],
1513
1719
  runtimes: ["python"]
1514
1720
  },
1515
- openvino: {
1516
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"),
1517
- sizeMB: 40,
1518
- runtimes: ["python"]
1519
- }
1721
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
1520
1722
  }
1521
1723
  },
1522
1724
  {
@@ -1541,11 +1743,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
1541
1743
  files: [...MLPACKAGE_FILES],
1542
1744
  runtimes: ["python"]
1543
1745
  },
1544
- openvino: {
1545
- url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"),
1546
- sizeMB: 90,
1547
- runtimes: ["python"]
1548
- }
1746
+ openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
1549
1747
  }
1550
1748
  }
1551
1749
  ];
@@ -1561,16 +1759,12 @@ var AUDIO_CLASSIFIER_MODELS = [{
1561
1759
  preprocessMode: "resize",
1562
1760
  formats: {
1563
1761
  onnx: {
1564
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1762
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1565
1763
  sizeMB: 3.2
1566
1764
  },
1567
- openvino: {
1568
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1569
- sizeMB: 3.2,
1570
- runtimes: ["python"]
1571
- },
1765
+ openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
1572
1766
  coreml: {
1573
- url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
1767
+ url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
1574
1768
  sizeMB: 3.2,
1575
1769
  runtimes: ["python"]
1576
1770
  }
@@ -1628,7 +1822,7 @@ var ObjectDetectionStep = class {
1628
1822
  "animal"
1629
1823
  ],
1630
1824
  models: [...OBJECT_DETECTION_MODELS],
1631
- defaultModelId: "yolov9s",
1825
+ defaultModelId: "yolo26n",
1632
1826
  defaultConfidence: .5,
1633
1827
  labels: COCO_80_LABELS.map((l) => l.id),
1634
1828
  classMap: COCO_TO_MACRO
@@ -1877,9 +2071,9 @@ var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
1877
2071
  enabledByDefault: false,
1878
2072
  defaultConfidence: .3
1879
2073
  });
1880
- var STEP_SEGMENTATION_REFINER = new PipelineStepBase({
1881
- id: "segmentation-refiner",
1882
- name: "Saliency Segmentation",
2074
+ var STEP_SEGMENTATION = new PipelineStepBase({
2075
+ id: "segmentation",
2076
+ name: "Segmentation",
1883
2077
  slot: "refiner",
1884
2078
  postprocessor: "saliency",
1885
2079
  extractMode: "crop-roi",
@@ -1891,7 +2085,7 @@ var STEP_SEGMENTATION_REFINER = new PipelineStepBase({
1891
2085
  defaultConfidence: 0,
1892
2086
  group: "Segmentation"
1893
2087
  });
1894
- var STEP_INSTANCE_SEGMENTATION = new PipelineStepBase({
2088
+ new PipelineStepBase({
1895
2089
  id: "instance-segmentation",
1896
2090
  name: "Instance Segmentation",
1897
2091
  slot: "refiner",
@@ -1918,8 +2112,7 @@ var ALL_PIPELINE_STEPS = [
1918
2112
  new AnimalClassifierStep(),
1919
2113
  STEP_BIRD_CLASSIFIER,
1920
2114
  STEP_VEHICLE_CLASSIFIER,
1921
- STEP_SEGMENTATION_REFINER,
1922
- STEP_INSTANCE_SEGMENTATION,
2115
+ STEP_SEGMENTATION,
1923
2116
  STEP_AUDIO_CLASSIFIER_INSTANCE
1924
2117
  ];
1925
2118
  /** Compat: flat array of StepDefinition for existing consumers */
@@ -2158,77 +2351,108 @@ var EngineFactory = class {
2158
2351
  }
2159
2352
  };
2160
2353
  //#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"
2354
+ //#region src/detection-pipeline/engine-provisioner.ts
2355
+ /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
2356
+ var BACKOFF_SCHEDULE_MS = [
2357
+ 5e3,
2358
+ 15e3,
2359
+ 3e4,
2360
+ 6e4,
2361
+ 12e4,
2362
+ 3e5
2183
2363
  ];
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;
2364
+ var IDLE_STATE = {
2365
+ runtimeId: null,
2366
+ device: null,
2367
+ state: "idle"
2368
+ };
2369
+ var EngineProvisioner = class {
2370
+ fx;
2371
+ current = IDLE_STATE;
2372
+ /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
2373
+ generation = 0;
2374
+ cancelTimer = null;
2375
+ retryIndex = 0;
2376
+ constructor(fx) {
2377
+ this.fx = fx;
2378
+ }
2379
+ get state() {
2380
+ return this.current;
2381
+ }
2382
+ isReady() {
2383
+ return this.current.state === "ready";
2384
+ }
2385
+ select(runtimeId, device) {
2386
+ this.generation++;
2387
+ this.retryIndex = 0;
2388
+ this.clearTimer();
2389
+ this.transition({
2390
+ runtimeId,
2391
+ device,
2392
+ state: "installing",
2393
+ progress: 0
2394
+ });
2395
+ this.provision(this.generation, runtimeId, device);
2396
+ }
2397
+ dispose() {
2398
+ this.generation++;
2399
+ this.clearTimer();
2400
+ }
2401
+ clearTimer() {
2402
+ if (this.cancelTimer !== null) {
2403
+ this.cancelTimer();
2404
+ this.cancelTimer = null;
2223
2405
  }
2224
- out[key] = store[key];
2225
2406
  }
2226
- for (const base of ENGINE_CASCADE_KEYS) {
2227
- const value = readNodeEngineValue(store, base, nodeId);
2228
- if (value !== void 0) out[base] = value;
2407
+ transition(next) {
2408
+ this.current = next;
2409
+ this.fx.onChange(next);
2229
2410
  }
2230
- return out;
2231
- }
2411
+ async provision(gen, runtimeId, device) {
2412
+ try {
2413
+ await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
2414
+ if (gen !== this.generation) return;
2415
+ this.transition({
2416
+ runtimeId,
2417
+ device,
2418
+ state: "verifying",
2419
+ progress: 100
2420
+ });
2421
+ await this.fx.verify(runtimeId, device);
2422
+ if (gen !== this.generation) return;
2423
+ this.retryIndex = 0;
2424
+ this.transition({
2425
+ runtimeId,
2426
+ device,
2427
+ state: "ready"
2428
+ });
2429
+ } catch (err) {
2430
+ if (gen !== this.generation) return;
2431
+ const message = err instanceof Error ? err.message : String(err);
2432
+ const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
2433
+ this.retryIndex++;
2434
+ const nextRetryAt = this.fx.now() + delay;
2435
+ this.transition({
2436
+ runtimeId,
2437
+ device,
2438
+ state: "failed",
2439
+ error: message,
2440
+ nextRetryAt
2441
+ });
2442
+ this.cancelTimer = this.fx.setTimer(delay, () => {
2443
+ if (gen !== this.generation) return;
2444
+ this.cancelTimer = null;
2445
+ this.transition({
2446
+ runtimeId,
2447
+ device,
2448
+ state: "installing",
2449
+ progress: 0
2450
+ });
2451
+ this.provision(gen, runtimeId, device);
2452
+ });
2453
+ }
2454
+ }
2455
+ };
2232
2456
  //#endregion
2233
2457
  //#region src/detection-pipeline/postprocess/dispatch.ts
2234
2458
  var VALID_KINDS = new Set([
@@ -3059,6 +3283,18 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
3059
3283
  parent.mask = output.mask;
3060
3284
  parent.maskWidth = output.maskWidth;
3061
3285
  parent.maskHeight = output.maskHeight;
3286
+ if (output.maskBbox !== void 0 && output.maskWidth > 0 && output.maskHeight > 0) {
3287
+ const [px1, py1, px2, py2] = parent.bbox;
3288
+ const parentW = px2 - px1;
3289
+ const parentH = py2 - py1;
3290
+ const [mbx, mby, mbw, mbh] = output.maskBbox;
3291
+ parent.refinedBbox = [
3292
+ px1 + mbx / output.maskWidth * parentW,
3293
+ py1 + mby / output.maskHeight * parentH,
3294
+ px1 + (mbx + mbw) / output.maskWidth * parentW,
3295
+ py1 + (mby + mbh) / output.maskHeight * parentH
3296
+ ];
3297
+ }
3062
3298
  break;
3063
3299
  }
3064
3300
  }
@@ -3109,6 +3345,12 @@ function buildFrameResult(input) {
3109
3345
  macroClass: m.macroClass,
3110
3346
  score: m.score,
3111
3347
  bbox,
3348
+ ...m.refinedBbox !== void 0 ? { refinedBbox: bboxTupleToRect(m.refinedBbox) } : {},
3349
+ ...m.mask !== void 0 && m.maskWidth !== void 0 && m.maskHeight !== void 0 ? {
3350
+ mask: m.mask,
3351
+ maskWidth: m.maskWidth,
3352
+ maskHeight: m.maskHeight
3353
+ } : {},
3112
3354
  labels,
3113
3355
  ...m.parentId !== void 0 ? { parentId: m.parentId } : {},
3114
3356
  ...embedding !== void 0 ? {
@@ -3540,338 +3782,97 @@ var PipelineExecutor = class {
3540
3782
  const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
3541
3783
  if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
3542
3784
  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));
3785
+ }
3786
+ };
3787
+ function capitalize(s) {
3788
+ if (s.length === 0) return s;
3789
+ return s.charAt(0).toUpperCase() + s.slice(1);
3701
3790
  }
3791
+ //#endregion
3792
+ //#region src/detection-pipeline/pipeline/tree-builder.ts
3702
3793
  /**
3703
- * Returns the device options for a given runtime and probed hardware.
3704
- * Delegates to `@camstack/types` `runtimeDevices`.
3794
+ * Build an executable tree from user config.
3795
+ *
3796
+ * @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
3797
+ * @param getEngine - Function that returns an IInferenceEngine for a step ID.
3798
+ * Throws if step not loaded.
3705
3799
  */
3706
- function runtimeDevices(id, hardware) {
3707
- return runtimeDevices$1(id, probedToHardwareInfo(hardware));
3800
+ function buildExecutableTree(steps, getEngine) {
3801
+ return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
3708
3802
  }
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);
3803
+ function buildNode(step, getEngine) {
3804
+ const definition = getStepDefinition(step.addonId);
3805
+ const engine = getEngine(step.addonId);
3806
+ const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
3807
+ const mergedSettings = {
3808
+ ...collectSchemaDefaults(step.addonId),
3809
+ ...step.settings
3810
+ };
3811
+ return {
3812
+ stepId: step.addonId,
3813
+ definition,
3814
+ engine,
3815
+ modelId: step.modelId,
3816
+ inputClasses: definition.inputClasses ?? [],
3817
+ enabled: step.enabled,
3818
+ children,
3819
+ ...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
3820
+ };
3715
3821
  }
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
3822
  /**
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.
3823
+ * Walk the step's declared `getConfigSchema()` and collect every field
3824
+ * with a primitive `default` value into a plain record. Group fields
3825
+ * are flattened — a nested `{ type: 'group', fields: [...] }` is
3826
+ * entered recursively. Fields without a declared default are skipped.
3726
3827
  */
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;
3828
+ function collectSchemaDefaults(stepId) {
3829
+ const schema = getStep(stepId).getConfigSchema();
3830
+ const out = {};
3831
+ walkFieldsForDefaults(schema, out);
3832
+ return out;
3735
3833
  }
3736
- function runtimeLabel(id) {
3737
- return RUNTIME_DETAIL[id].label;
3834
+ function walkFieldsForDefaults(fields, out) {
3835
+ for (const field of fields) {
3836
+ if ("type" in field && field.type === "group" && "fields" in field) {
3837
+ walkFieldsForDefaults(field.fields, out);
3838
+ continue;
3839
+ }
3840
+ if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
3841
+ }
3738
3842
  }
3843
+ //#endregion
3844
+ //#region src/detection-pipeline/registry/custom-models.ts
3739
3845
  /**
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.
3846
+ * Group a flat list of custom-model descriptors (as returned by the
3847
+ * `custom-model-registry` collection cap) into a `stepId entries` map for
3848
+ * the picker / resolution union. Pure; order within a step preserved.
3743
3849
  */
3744
- function shouldInstallOpenvino(env) {
3745
- if (env.platform === "darwin") return false;
3746
- return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3850
+ function groupCustomModelsByStep(descriptors) {
3851
+ const byStep = /* @__PURE__ */ new Map();
3852
+ for (const d of descriptors) {
3853
+ const arr = byStep.get(d.stepId) ?? [];
3854
+ arr.push(d.entry);
3855
+ byStep.set(d.stepId, arr);
3856
+ }
3857
+ return byStep;
3747
3858
  }
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
3789
- });
3790
- this.provision(this.generation, runtimeId, device);
3791
- }
3792
- dispose() {
3793
- this.generation++;
3794
- this.clearTimer();
3795
- }
3796
- clearTimer() {
3797
- if (this.cancelTimer !== null) {
3798
- this.cancelTimer();
3799
- this.cancelTimer = null;
3800
- }
3801
- }
3802
- transition(next) {
3803
- this.current = next;
3804
- this.fx.onChange(next);
3805
- }
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
- });
3848
- }
3849
- }
3850
- };
3851
- //#endregion
3852
- //#region src/detection-pipeline/auto-pick.ts
3853
- var PREFERENCE = [
3854
- "coreml",
3855
- "openvino",
3856
- "onnx"
3857
- ];
3858
3859
  /**
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).
3860
+ * Union a step's static catalog models with operator-registered custom
3861
+ * models. On an `id` collision the static catalog entry wins — a custom
3862
+ * model can never shadow a built-in one.
3865
3863
  *
3866
- * Device is always `defaultDeviceFor(chosen)`.
3864
+ * Pure + side-effect-free so it can be unit-tested in isolation and called
3865
+ * from the (free) `buildSchemaSlots` builder without any addon context.
3867
3866
  */
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";
3871
- return {
3872
- runtimeId: chosen,
3873
- device: defaultDeviceFor(chosen)
3874
- };
3867
+ function mergeCustomModels(staticModels, customModels) {
3868
+ const seen = new Set(staticModels.map((m) => m.id));
3869
+ const merged = [...staticModels];
3870
+ for (const m of customModels) {
3871
+ if (seen.has(m.id)) continue;
3872
+ seen.add(m.id);
3873
+ merged.push(m);
3874
+ }
3875
+ return merged;
3875
3876
  }
3876
3877
  //#endregion
3877
3878
  //#region src/detection-pipeline/provider.ts
@@ -4109,6 +4110,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4109
4110
  /** Addon context — ctx.api resolves lazily (direct caller created after boot) */
4110
4111
  addonCtx = null;
4111
4112
  /**
4113
+ * Short-lived cache of custom models pulled from the `custom-model-registry`
4114
+ * collection cap, grouped by step id. TTL-bounded so the picker / resolution
4115
+ * paths don't issue a cap round-trip on every call. Empty map = no provider
4116
+ * (or a query failure) → behaviour identical to the static-catalog-only path.
4117
+ */
4118
+ customModelsCache = null;
4119
+ /**
4112
4120
  * Per-device {@link DeviceProxy} cache used for zone gating at the
4113
4121
  * runtime path. Reads `state.zones.value` + `state.zoneRules.value`
4114
4122
  * synchronously per frame so detections inside an `exclude` zone
@@ -4154,6 +4162,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4154
4162
  */
4155
4163
  needsAutoPick = false;
4156
4164
  /**
4165
+ * Unsubscribe handle for the deferred-auto-pick `platform-probe` ready
4166
+ * listener (armed in `setApi` when the probe isn't ready yet). Cleared once
4167
+ * the engine is resolved — either by the listener firing or by the boot
4168
+ * safety-net `ensureBootEngineProvisioned`.
4169
+ */
4170
+ deferredAutoPickUnsub = null;
4171
+ /**
4157
4172
  * Warm cache for benchmark engine-override runs.
4158
4173
  *
4159
4174
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -4186,7 +4201,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4186
4201
  this.writeStore = (patch) => settings.writeAddonStore(patch);
4187
4202
  this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
4188
4203
  this.currentEngine = ONNX_FLOOR;
4189
- this.log.info("Engine selected (default)", { meta: {
4204
+ this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
4190
4205
  runtime: this.currentEngine.runtime,
4191
4206
  backend: this.currentEngine.backend,
4192
4207
  format: this.currentEngine.format
@@ -4259,26 +4274,70 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4259
4274
  this.needsAutoPick = false;
4260
4275
  this.startProvisioningForCurrentEngine();
4261
4276
  } else {
4262
- this.startProvisioningForCurrentEngine();
4263
4277
  const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
4264
4278
  if (state !== "ready") return;
4265
- unsubscribe();
4279
+ this.cancelDeferredAutoPick();
4266
4280
  if (!this.needsAutoPick) return;
4267
4281
  this.autoPickAndPersist().then(() => {
4268
4282
  this.needsAutoPick = false;
4269
4283
  this.startProvisioningForCurrentEngine();
4270
4284
  });
4271
4285
  });
4272
- this.addonCtx.addDisposer(unsubscribe);
4286
+ this.deferredAutoPickUnsub = unsubscribe;
4287
+ this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
4273
4288
  }
4274
4289
  else this.startProvisioningForCurrentEngine();
4275
4290
  }
4291
+ /** Tear down the deferred-auto-pick probe listener, if still armed. */
4292
+ cancelDeferredAutoPick() {
4293
+ if (this.deferredAutoPickUnsub) {
4294
+ this.deferredAutoPickUnsub();
4295
+ this.deferredAutoPickUnsub = null;
4296
+ }
4297
+ }
4298
+ /**
4299
+ * Boot safety-net: deterministically provision the hardware-probed engine.
4300
+ *
4301
+ * Called from the addon's `onInitialize` right after `reprobeEngine` has
4302
+ * written this node's probe-driven selection (e.g. `engineBackend=openvino`)
4303
+ * to the store. When first-boot auto-pick was DEFERRED (probe not ready at
4304
+ * `setApi`), the engine would otherwise stay `idle` — selected but never
4305
+ * provisioned — because the `onCapabilityStateChange('platform-probe')` ready
4306
+ * edge can be missed when the cap is already 'ready' by the time we subscribe
4307
+ * (the old eager-onnx-floor masked this; removing it surfaced a node that
4308
+ * boots with no engine at all). This loads the now-persisted selection and
4309
+ * starts provisioning it, so the node reliably comes up on its real best
4310
+ * engine (openvino on Intel) with no onnx floor and no missed boot. No-op
4311
+ * when provisioning already started (persisted-engine path / listener fired)
4312
+ * or when nothing has been selected yet.
4313
+ */
4314
+ async ensureBootEngineProvisioned() {
4315
+ if (this.getEngineProvisioning().state !== "idle") return;
4316
+ const stored = await this.loadEngine();
4317
+ if (!stored) return;
4318
+ this.currentEngine = stored;
4319
+ this.needsAutoPick = false;
4320
+ this.cancelDeferredAutoPick();
4321
+ this.log.info("Boot engine provisioning from probed selection", { meta: {
4322
+ runtime: stored.runtime,
4323
+ backend: stored.backend,
4324
+ device: stored.device ?? null
4325
+ } });
4326
+ this.startProvisioningForCurrentEngine();
4327
+ }
4276
4328
  /**
4277
4329
  * Auto-pick the best supported runtime at first boot (no stored engine).
4278
4330
  * Uses the platform-probe cap's hardware + bestScore hint when available;
4279
4331
  * falls back to platform/arch when the probe cap is not yet reachable.
4332
+ *
4280
4333
  * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4281
- * boots load it via `loadEngine()` and skip this path.
4334
+ * boots load it via `loadEngine()` and skip this path — but ONLY when the
4335
+ * probe actually answered (real `hardware` or a `bestScore` hint). If the
4336
+ * probe query failed (cold-start race, cap momentarily unreachable), the
4337
+ * pick floors to onnx purely for lack of information; persisting that would
4338
+ * LOCK onnx and skip auto-pick on every future boot even after the
4339
+ * accelerator surfaces. In that case we set the in-memory floor for liveness
4340
+ * but leave the store untouched so the next boot re-attempts the pick.
4282
4341
  */
4283
4342
  async autoPickAndPersist() {
4284
4343
  let hardware = null;
@@ -4300,6 +4359,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4300
4359
  device: pick.device
4301
4360
  };
4302
4361
  this.currentEngine = engine;
4362
+ if (!(hardware !== null || bestBackendHint !== null)) {
4363
+ this.log.warn("Auto-pick: probe returned no hardware/hint — using onnx floor WITHOUT persisting", { meta: {
4364
+ backend: pick.runtimeId,
4365
+ device: pick.device
4366
+ } });
4367
+ return;
4368
+ }
4303
4369
  const apNode = this.localProbeNodeId();
4304
4370
  await this.writeStore({
4305
4371
  [nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
@@ -4379,8 +4445,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4379
4445
  * runtime through installing → verifying → ready.
4380
4446
  */
4381
4447
  async onEngineSelectionChanged() {
4448
+ const prev = this.currentEngine;
4382
4449
  const stored = await this.loadEngine();
4383
4450
  if (stored) this.currentEngine = stored;
4451
+ if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
4452
+ this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4453
+ from: `${prev.backend}/${prev.device ?? "default"}`,
4454
+ to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
4455
+ } });
4456
+ try {
4457
+ await this.engineFactory.dispose();
4458
+ } catch {}
4459
+ this.engineFactory = null;
4460
+ this.executor = null;
4461
+ }
4384
4462
  this.startProvisioningForCurrentEngine();
4385
4463
  }
4386
4464
  /**
@@ -4418,7 +4496,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4418
4496
  if (!steps || steps.length === 0) return;
4419
4497
  for (const step of flattenSteps(steps)) {
4420
4498
  if (!step.enabled) continue;
4421
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4499
+ const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
4422
4500
  if (!modelEntry) continue;
4423
4501
  if (isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4424
4502
  await this.downloadWithRetry(modelEntry, format, 3);
@@ -4496,10 +4574,44 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4496
4574
  return { hardware: null };
4497
4575
  }
4498
4576
  }
4577
+ /**
4578
+ * Pull custom models from the `custom-model-registry` collection cap,
4579
+ * grouped by step id. 5s TTL cache. Fully graceful: when no provider is
4580
+ * registered (or the query fails) it returns an empty map and logs at most
4581
+ * a warning — callers then behave exactly like the static-catalog path.
4582
+ */
4583
+ async getCustomModels() {
4584
+ const now = Date.now();
4585
+ if (this.customModelsCache && now - this.customModelsCache.at < 5e3) return this.customModelsCache.byStep;
4586
+ let byStep = /* @__PURE__ */ new Map();
4587
+ try {
4588
+ const api = this.addonCtx?.api;
4589
+ if (api) {
4590
+ if ((await api.addons.listCapabilityProviders.query({ capName: "custom-model-registry" })).some((p) => p.isActive)) byStep = groupCustomModelsByStep(await api.customModelRegistry.listModels.query());
4591
+ }
4592
+ } catch (err) {
4593
+ this.log.warn("custom-model-registry query failed — using static catalog only", { meta: { error: errMsg(err) } });
4594
+ }
4595
+ this.customModelsCache = {
4596
+ at: now,
4597
+ byStep
4598
+ };
4599
+ return byStep;
4600
+ }
4601
+ /**
4602
+ * Resolve a model id within a step to a catalog entry — static catalog
4603
+ * first, then the custom registry. Returns undefined if neither has it.
4604
+ */
4605
+ async resolveModelEntry(addonId, modelId) {
4606
+ const fromCatalog = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4607
+ if (fromCatalog) return fromCatalog;
4608
+ return (await this.getCustomModels()).get(addonId)?.find((m) => m.id === modelId);
4609
+ }
4499
4610
  async getSchema(engine) {
4500
4611
  if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
4501
4612
  const format = engine.format;
4502
- const slots = buildSchemaSlots(format, this.modelsDir);
4613
+ const customByStep = await this.getCustomModels();
4614
+ const slots = buildSchemaSlots(format, this.modelsDir, customByStep);
4503
4615
  const { hardware } = await this.fetchProbeGatingData();
4504
4616
  const env = runtimeEnvFromProcess(toProbedHardware(hardware));
4505
4617
  return {
@@ -4686,7 +4798,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4686
4798
  }
4687
4799
  async downloadModel(input) {
4688
4800
  const { modelId, format, addonId } = input;
4689
- const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4801
+ const modelEntry = await this.resolveModelEntry(addonId, modelId);
4690
4802
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4691
4803
  const formatEntry = modelEntry.formats[format];
4692
4804
  if (!formatEntry) throw new Error(`Model "${modelId}" has no ${format} format. Available: ${Object.keys(modelEntry.formats).join(", ")}`);
@@ -4749,7 +4861,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4749
4861
  }
4750
4862
  async deleteModel(input) {
4751
4863
  const { modelId, format, addonId } = input;
4752
- const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4864
+ const modelEntry = await this.resolveModelEntry(addonId, modelId);
4753
4865
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4754
4866
  if (!deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4755
4867
  this.log.info("Model deleted from disk", { meta: {
@@ -5205,7 +5317,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5205
5317
  const work = (async () => {
5206
5318
  const format = this.currentEngine?.format ?? "onnx";
5207
5319
  for (const step of needed) {
5208
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
5320
+ const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5209
5321
  if (modelEntry && !isModelDownloaded(this.modelsDir, modelEntry, format)) {
5210
5322
  this.log.info("Downloading model for step", { meta: {
5211
5323
  modelId: step.modelId,
@@ -5700,7 +5812,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5700
5812
  const format = this.currentEngine.format;
5701
5813
  const downloads = [];
5702
5814
  for (const step of flattenSteps(steps)) {
5703
- const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
5815
+ const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5704
5816
  if (!modelEntry) {
5705
5817
  this.log.warn("Model not found in step catalog — skipping download", { meta: {
5706
5818
  modelId: step.modelId,
@@ -6103,11 +6215,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6103
6215
  }
6104
6216
  }
6105
6217
  };
6106
- function buildSchemaSlots(format, modelsDir) {
6218
+ function buildSchemaSlots(format, modelsDir, customByStep) {
6107
6219
  const slotMap = /* @__PURE__ */ new Map();
6108
6220
  for (const pipelineStep of ALL_PIPELINE_STEPS) {
6109
6221
  const step = pipelineStep.definition;
6110
- const availableModels = step.models.filter((m) => m.formats[format]);
6222
+ const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format]);
6111
6223
  if (availableModels.length === 0) continue;
6112
6224
  const slot = step.slot;
6113
6225
  if (!slotMap.has(slot)) slotMap.set(slot, []);
@@ -6212,8 +6324,7 @@ function buildDefaultStepTree(format) {
6212
6324
  makeStep("animal-classifier", [], { enabled: false }),
6213
6325
  makeStep("bird-classifier", [], { enabled: false }),
6214
6326
  makeStep("vehicle-classifier", [], { enabled: false }),
6215
- makeStep("segmentation-refiner", [], { enabled: false }),
6216
- makeStep("instance-segmentation", [], { enabled: false })
6327
+ makeStep("segmentation", [], { enabled: false })
6217
6328
  ].filter((s) => s !== null));
6218
6329
  const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
6219
6330
  runtime: "python",
@@ -6503,8 +6614,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6503
6614
  label: "Execution provider",
6504
6615
  options: [...STATIC_BACKEND_OPTIONS],
6505
6616
  default: DEFAULT_CONFIG.engineBackend,
6506
- immediate: true,
6507
- requiresRestart: true
6617
+ immediate: true
6508
6618
  }),
6509
6619
  this.field({
6510
6620
  type: "select",
@@ -6512,8 +6622,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6512
6622
  label: "Hardware device",
6513
6623
  options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
6514
6624
  default: DEFAULT_CONFIG.engineDevice,
6515
- immediate: true,
6516
- requiresRestart: true
6625
+ immediate: true
6517
6626
  })
6518
6627
  ]
6519
6628
  }, {
@@ -6552,8 +6661,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6552
6661
  "onnx",
6553
6662
  "cpu"
6554
6663
  ]
6555
- },
6556
- requiresRestart: true
6664
+ }
6557
6665
  }),
6558
6666
  this.field({
6559
6667
  type: "slider",
@@ -6570,8 +6678,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6570
6678
  showWhen: {
6571
6679
  field: "batchMode",
6572
6680
  notEquals: "none"
6573
- },
6574
- requiresRestart: true
6681
+ }
6575
6682
  }),
6576
6683
  this.field({
6577
6684
  type: "slider",
@@ -6588,8 +6695,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6588
6695
  showWhen: {
6589
6696
  field: "batchMode",
6590
6697
  notEquals: "none"
6591
- },
6592
- requiresRestart: true
6698
+ }
6593
6699
  }),
6594
6700
  this.field({
6595
6701
  type: "slider",
@@ -6602,8 +6708,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6602
6708
  default: void 0,
6603
6709
  showValue: true,
6604
6710
  nullable: true,
6605
- nullLabel: "Auto",
6606
- requiresRestart: true
6711
+ nullLabel: "Auto"
6607
6712
  }),
6608
6713
  this.field({
6609
6714
  type: "slider",
@@ -6616,8 +6721,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6616
6721
  default: void 0,
6617
6722
  showValue: true,
6618
6723
  nullable: true,
6619
- nullLabel: "Auto",
6620
- requiresRestart: true
6724
+ nullLabel: "Auto"
6621
6725
  })
6622
6726
  ]
6623
6727
  }] });
@@ -6880,6 +6984,9 @@ var DetectionPipelineAddon = class extends BaseAddon {
6880
6984
  if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
6881
6985
  this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6882
6986
  });
6987
+ await this.provider.ensureBootEngineProvisioned().catch((err) => {
6988
+ this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
6989
+ });
6883
6990
  await this.provider.warmPool();
6884
6991
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
6885
6992
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -6987,16 +7094,17 @@ var DetectionPipelineAddon = class extends BaseAddon {
6987
7094
  return false;
6988
7095
  }
6989
7096
  /**
6990
- * BaseAddon calls `onConfigChanged` after every settings write.
6991
- * Group-runner addons can't honour the schema's `requiresRestart:
6992
- * true` flag (the restart cap returns "process not found" for
6993
- * processes spawned in a kernel group worker). To make tuning
6994
- * changes actually take effect, watch the pool-bound subset and
6995
- * respawn the provider in place when it flips.
6996
- *
6997
- * Engine cascade (`engineRuntime/Backend/Device`) and audio
6998
- * settings don't require this those still rely on the addon
6999
- * lifecycle (engineFactory rebuild on next runPipeline).
7097
+ * BaseAddon calls `onConfigChanged` after every settings write. This is the
7098
+ * SOLE apply path for engine + tuning changes — none of those fields declare
7099
+ * `requiresRestart` anymore (an addon restart re-probes hardware + reloads
7100
+ * every model on every set, which is exactly what we want to avoid). Instead:
7101
+ * - Pool-bound tuning (`concurrency`/`batchMode`/`windowMs`/…) in-place
7102
+ * pool respawn when the snapshot flips (`poolConfigChanged` below).
7103
+ * - Engine cascade (`engineBackend`/`engineDevice`) → the provider's
7104
+ * `onEngineSelectionChanged` disposes the device-bound factory so the next
7105
+ * runPipeline rebuilds the pool on the new selection, in place.
7106
+ * Both apply optimistically the inference gate stays closed for the short
7107
+ * re-spin, frames are dropped (never crashed), no addon bounce.
7000
7108
  */
7001
7109
  async onConfigChanged() {
7002
7110
  await this.refreshNodeEngineFromStore();