@camstack/addon-pipeline 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +7 -8
- package/dist/audio-analyzer/index.mjs +3 -4
- package/dist/audio-codec-nodeav/index.js +2 -2
- package/dist/audio-codec-nodeav/index.mjs +2 -2
- package/dist/decoder-nodeav/index.js +1 -1
- package/dist/decoder-nodeav/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +862 -696
- package/dist/detection-pipeline/index.mjs +850 -684
- package/dist/{dist-CjrjeaDd.mjs → dist-BA6DR_jV.mjs} +128 -5
- package/dist/{dist-G45MVm6i.js → dist-BLcTVvol.js} +151 -4
- package/dist/{model-download-service-C7AjBsX9-rXY-VFDk.js → model-download-service-RxAOiYvX-C8rTRJy_.js} +36 -6
- package/dist/{model-download-service-C7AjBsX9-B0ekM6dF.mjs → model-download-service-RxAOiYvX-CMAvhgO7.mjs} +36 -6
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +1 -1
- package/dist/pipeline-runner/index.mjs +1 -1
- package/dist/recorder/index.js +6 -6
- package/dist/recorder/index.mjs +4 -4
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Tbqpu0v3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DrohyZ5L.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COa17XL2.mjs +26 -0
- package/dist/stream-broker/{hostInit-tIev5Gd9.mjs → hostInit-zLZbYJcg.mjs} +3 -3
- package/dist/stream-broker/index.js +26 -26
- package/dist/stream-broker/index.mjs +20 -20
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C0kKwNX_.js → MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js} +1 -1
- package/embed-dist/assets/MotionZonesSettings-NcxxQN8r-CQzEnQoq.js +1 -0
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C2SRtNe6.js → PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js} +1 -1
- package/embed-dist/assets/{index-B2LRyXWh.js → index-CSuLwWK-.js} +3 -3
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +65 -6
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DCsgcqTa.mjs +0 -26
- package/embed-dist/assets/MotionZonesSettings-C1EEbk2V-CYtJc892.js +0 -1
|
@@ -2,17 +2,261 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_model_download_service_RxAOiYvX = require("../model-download-service-RxAOiYvX-C8rTRJy_.js");
|
|
6
|
+
const require_dist = require("../dist-BLcTVvol.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
|
-
node_fs =
|
|
8
|
+
node_fs = require_model_download_service_RxAOiYvX.__toESM(node_fs);
|
|
9
9
|
let node_path = require("node:path");
|
|
10
|
-
node_path =
|
|
10
|
+
node_path = require_model_download_service_RxAOiYvX.__toESM(node_path);
|
|
11
11
|
let node_os = require("node:os");
|
|
12
|
-
node_os =
|
|
12
|
+
node_os = require_model_download_service_RxAOiYvX.__toESM(node_os);
|
|
13
13
|
let node_child_process = require("node:child_process");
|
|
14
14
|
let sharp = require("sharp");
|
|
15
|
-
sharp =
|
|
15
|
+
sharp = require_model_download_service_RxAOiYvX.__toESM(sharp);
|
|
16
|
+
//#region src/detection-pipeline/engine-store-keys.ts
|
|
17
|
+
/**
|
|
18
|
+
* Per-node scoping for the detection-pipeline engine cascade.
|
|
19
|
+
*
|
|
20
|
+
* The detection addon's settings store is a single CLUSTER-SHARED blob (the
|
|
21
|
+
* settings-store cap is hub-resident; every node's detection instance reads and
|
|
22
|
+
* writes the same keys). That is correct for node-agnostic settings (pipeline
|
|
23
|
+
* steps, tuning) but WRONG for the engine cascade — `engineBackend` /
|
|
24
|
+
* `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
|
|
25
|
+
* and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
|
|
26
|
+
* selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
|
|
27
|
+
* another node that has no NPU.
|
|
28
|
+
*
|
|
29
|
+
* These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
|
|
30
|
+
* Everything else in the store stays shared. A legacy un-scoped value (written
|
|
31
|
+
* before this change, or by an older build) is read as a migration fallback and
|
|
32
|
+
* re-persisted under the node-scoped key on the next write.
|
|
33
|
+
*/
|
|
34
|
+
var ENGINE_CASCADE_KEYS = [
|
|
35
|
+
"engineBackend",
|
|
36
|
+
"engineDevice",
|
|
37
|
+
"probedBestEngine"
|
|
38
|
+
];
|
|
39
|
+
function isEngineCascadeKey(key) {
|
|
40
|
+
return ENGINE_CASCADE_KEYS.includes(key);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Normalise a raw kernel node id to the bare node id used for scoping.
|
|
44
|
+
* `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
|
|
45
|
+
* per-NODE, so strip the addon segment. Falls back to `hub`.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeEngineNodeId(rawNodeId) {
|
|
48
|
+
const raw = rawNodeId ?? "hub";
|
|
49
|
+
return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
|
|
50
|
+
}
|
|
51
|
+
/** The node-scoped store key for an engine cascade field. */
|
|
52
|
+
function nodeEngineKey(base, nodeId) {
|
|
53
|
+
return `${base}@${normalizeEngineNodeId(nodeId)}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read an engine cascade value for a node: the node-scoped key if present,
|
|
57
|
+
* otherwise the legacy un-scoped value (migration), otherwise undefined.
|
|
58
|
+
*/
|
|
59
|
+
function readNodeEngineValue(store, base, nodeId) {
|
|
60
|
+
const scoped = store[nodeEngineKey(base, nodeId)];
|
|
61
|
+
return scoped !== void 0 ? scoped : store[base];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Project a raw store onto the plain engine cascade keys for THIS node, so the
|
|
65
|
+
* UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
|
|
66
|
+
* the node's own selection. Non-engine keys are left untouched. Node-scoped
|
|
67
|
+
* keys for OTHER nodes are dropped from the projection (not relevant to this
|
|
68
|
+
* node's form).
|
|
69
|
+
*/
|
|
70
|
+
function projectNodeEngine(store, nodeId) {
|
|
71
|
+
const out = {};
|
|
72
|
+
const scopedForAnyNode = /* @__PURE__ */ new Set();
|
|
73
|
+
for (const key of Object.keys(store)) {
|
|
74
|
+
const atIdx = key.indexOf("@");
|
|
75
|
+
if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
|
|
76
|
+
scopedForAnyNode.add(key);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
out[key] = store[key];
|
|
80
|
+
}
|
|
81
|
+
for (const base of ENGINE_CASCADE_KEYS) {
|
|
82
|
+
const value = readNodeEngineValue(store, base, nodeId);
|
|
83
|
+
if (value !== void 0) out[base] = value;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/detection-pipeline/runtimes.ts
|
|
89
|
+
var KNOWN_PLATFORMS = [
|
|
90
|
+
"darwin",
|
|
91
|
+
"linux",
|
|
92
|
+
"win32"
|
|
93
|
+
];
|
|
94
|
+
var KNOWN_ARCHES = ["arm64", "x64"];
|
|
95
|
+
var KNOWN_GPU_TYPES = [
|
|
96
|
+
"nvidia",
|
|
97
|
+
"amd",
|
|
98
|
+
"intel",
|
|
99
|
+
"apple"
|
|
100
|
+
];
|
|
101
|
+
var KNOWN_NPU_TYPES = ["apple-ane", "intel-npu"];
|
|
102
|
+
function toKnownPlatform(p) {
|
|
103
|
+
return KNOWN_PLATFORMS.find((v) => v === p) ?? "linux";
|
|
104
|
+
}
|
|
105
|
+
function toKnownArch(a) {
|
|
106
|
+
return KNOWN_ARCHES.find((v) => v === a) ?? "x64";
|
|
107
|
+
}
|
|
108
|
+
function gpuInfoFrom(hw) {
|
|
109
|
+
if (!hw.gpu) return null;
|
|
110
|
+
const type = KNOWN_GPU_TYPES.find((v) => v === hw.gpu?.type);
|
|
111
|
+
if (!type) return null;
|
|
112
|
+
return {
|
|
113
|
+
type,
|
|
114
|
+
name: ""
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function npuInfoFrom(hw) {
|
|
118
|
+
if (!hw.npu) return null;
|
|
119
|
+
const type = KNOWN_NPU_TYPES.find((v) => v === hw.npu?.type);
|
|
120
|
+
if (!type) return null;
|
|
121
|
+
return { type };
|
|
122
|
+
}
|
|
123
|
+
function envToHardwareInfo(env) {
|
|
124
|
+
if (!env.hardware) return null;
|
|
125
|
+
return {
|
|
126
|
+
platform: toKnownPlatform(env.platform),
|
|
127
|
+
arch: toKnownArch(env.arch),
|
|
128
|
+
cpuModel: "",
|
|
129
|
+
cpuCores: 0,
|
|
130
|
+
totalRAM_MB: 0,
|
|
131
|
+
availableRAM_MB: 0,
|
|
132
|
+
gpu: gpuInfoFrom(env.hardware),
|
|
133
|
+
npu: npuInfoFrom(env.hardware)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function probedToHardwareInfo(hw) {
|
|
137
|
+
if (!hw) return null;
|
|
138
|
+
return {
|
|
139
|
+
platform: toKnownPlatform(process.platform),
|
|
140
|
+
arch: toKnownArch(process.arch),
|
|
141
|
+
cpuModel: "",
|
|
142
|
+
cpuCores: 0,
|
|
143
|
+
totalRAM_MB: 0,
|
|
144
|
+
availableRAM_MB: 0,
|
|
145
|
+
gpu: gpuInfoFrom(hw),
|
|
146
|
+
npu: npuInfoFrom(hw)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
var RUNTIME_DETAIL = {
|
|
150
|
+
onnx: {
|
|
151
|
+
label: "ONNX Runtime",
|
|
152
|
+
pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
|
|
153
|
+
tuning: {
|
|
154
|
+
concurrency: 4,
|
|
155
|
+
batchMode: "list",
|
|
156
|
+
maxBatchSize: 8,
|
|
157
|
+
intraOpThreads: 0
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
openvino: {
|
|
161
|
+
label: "OpenVINO",
|
|
162
|
+
pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
|
|
163
|
+
tuning: {
|
|
164
|
+
concurrency: 1,
|
|
165
|
+
batchMode: "none",
|
|
166
|
+
numStreams: 0
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
coreml: {
|
|
170
|
+
label: "CoreML",
|
|
171
|
+
pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
|
|
172
|
+
tuning: {
|
|
173
|
+
concurrency: 1,
|
|
174
|
+
batchMode: "none",
|
|
175
|
+
windowMs: 8,
|
|
176
|
+
maxBatchSize: 8,
|
|
177
|
+
numWorkers: 1
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Returns the list of supported runtime IDs for the given hardware env.
|
|
183
|
+
* Delegates to `@camstack/types` `supportedRuntimes`.
|
|
184
|
+
*/
|
|
185
|
+
function supportedRuntimes(env) {
|
|
186
|
+
return require_dist.supportedRuntimes(envToHardwareInfo(env));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Returns the device options for a given runtime and probed hardware.
|
|
190
|
+
* Delegates to `@camstack/types` `runtimeDevices`.
|
|
191
|
+
*/
|
|
192
|
+
function runtimeDevices(id, hardware) {
|
|
193
|
+
return require_dist.runtimeDevices(id, probedToHardwareInfo(hardware));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Returns the default device string for a given runtime.
|
|
197
|
+
* Delegates to `@camstack/types` `defaultDeviceFor`.
|
|
198
|
+
*/
|
|
199
|
+
function defaultDeviceFor(id) {
|
|
200
|
+
return require_dist.defaultDeviceFor(id);
|
|
201
|
+
}
|
|
202
|
+
/** Model format for each inference runtime supported by the detection pipeline. */
|
|
203
|
+
var RUNTIME_FORMAT = {
|
|
204
|
+
onnx: "onnx",
|
|
205
|
+
openvino: "openvino",
|
|
206
|
+
coreml: "coreml"
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Returns the model format required for a given runtime.
|
|
210
|
+
* Returns the locally-typed format string ('onnx' | 'openvino' | 'coreml')
|
|
211
|
+
* matching the engine-provisioner's own ModelFormat type.
|
|
212
|
+
*/
|
|
213
|
+
function modelFormatFor(id) {
|
|
214
|
+
return RUNTIME_FORMAT[id];
|
|
215
|
+
}
|
|
216
|
+
function pythonRequirementsFor(id) {
|
|
217
|
+
return RUNTIME_DETAIL[id].pythonRequirements;
|
|
218
|
+
}
|
|
219
|
+
function tuningFor(id) {
|
|
220
|
+
return RUNTIME_DETAIL[id].tuning;
|
|
221
|
+
}
|
|
222
|
+
function runtimeLabel(id) {
|
|
223
|
+
return RUNTIME_DETAIL[id].label;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Proactive-install hint kept for back-compat (re-exported from index.ts).
|
|
227
|
+
* Intel on Linux warrants installing openvino; coremltools covers macOS;
|
|
228
|
+
* openvino has no AMD/NVIDIA backend.
|
|
229
|
+
*/
|
|
230
|
+
function shouldInstallOpenvino(env) {
|
|
231
|
+
if (env.platform === "darwin") return false;
|
|
232
|
+
return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/detection-pipeline/auto-pick.ts
|
|
236
|
+
var PREFERENCE = [
|
|
237
|
+
"coreml",
|
|
238
|
+
"openvino",
|
|
239
|
+
"onnx"
|
|
240
|
+
];
|
|
241
|
+
/**
|
|
242
|
+
* Pure function — picks the best supported runtime for the given hardware env.
|
|
243
|
+
*
|
|
244
|
+
* Logic:
|
|
245
|
+
* 1. If `bestBackendHint` is in the supported set, use it.
|
|
246
|
+
* 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
|
|
247
|
+
* 3. Floor to `'onnx'` (always supported).
|
|
248
|
+
*
|
|
249
|
+
* Device is always `defaultDeviceFor(chosen)`.
|
|
250
|
+
*/
|
|
251
|
+
function pickBestRuntime(env, bestBackendHint) {
|
|
252
|
+
const supported = supportedRuntimes(env);
|
|
253
|
+
const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
|
|
254
|
+
return {
|
|
255
|
+
runtimeId: chosen,
|
|
256
|
+
device: defaultDeviceFor(chosen)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
16
260
|
//#region src/detection-pipeline/engine/shared-inference-pool.ts
|
|
17
261
|
var MSG_COMMAND = 0;
|
|
18
262
|
var MSG_INFER_JPEG = 1;
|
|
@@ -779,6 +1023,26 @@ var HF_REPO = "camstack/camstack-models";
|
|
|
779
1023
|
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
780
1024
|
var hf = (path) => require_dist.hfModelUrl(HF_REPO, path);
|
|
781
1025
|
var hfScrypted = (path) => require_dist.hfModelUrl(HF_SCRYPTED, path);
|
|
1026
|
+
/**
|
|
1027
|
+
* Build an OpenVINO format entry (always python runtime).
|
|
1028
|
+
*
|
|
1029
|
+
* OpenVINO IR is a two-file bundle: a `.xml` topology + a sibling `.bin`
|
|
1030
|
+
* weights file with the same basename. We declare the `.bin` in `files` so
|
|
1031
|
+
* the (format-agnostic) downloader fetches it alongside the `.xml` — without
|
|
1032
|
+
* the weights, OpenVINO compile fails with "Empty weights data in bin file".
|
|
1033
|
+
* A plain `.onnx` run through the OpenVINO runtime (e.g. yamnet) has no
|
|
1034
|
+
* sibling, so none is added.
|
|
1035
|
+
*/
|
|
1036
|
+
var ovFormat = (url, sizeMB) => {
|
|
1037
|
+
const base = url.split("/").pop() ?? "";
|
|
1038
|
+
const files = base.endsWith(".xml") ? [base.replace(/\.xml$/, ".bin")] : void 0;
|
|
1039
|
+
return {
|
|
1040
|
+
url,
|
|
1041
|
+
sizeMB,
|
|
1042
|
+
runtimes: ["python"],
|
|
1043
|
+
...files ? { files } : {}
|
|
1044
|
+
};
|
|
1045
|
+
};
|
|
782
1046
|
var MLPACKAGE_FILES = [
|
|
783
1047
|
"Manifest.json",
|
|
784
1048
|
"Data/com.apple.CoreML/model.mlmodel",
|
|
@@ -807,11 +1071,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
807
1071
|
files: [...MLPACKAGE_FILES],
|
|
808
1072
|
runtimes: ["python"]
|
|
809
1073
|
},
|
|
810
|
-
openvino:
|
|
811
|
-
url: hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"),
|
|
812
|
-
sizeMB: 6,
|
|
813
|
-
runtimes: ["python"]
|
|
814
|
-
}
|
|
1074
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9t.xml"), 6)
|
|
815
1075
|
}
|
|
816
1076
|
},
|
|
817
1077
|
{
|
|
@@ -836,11 +1096,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
836
1096
|
files: [...MLPACKAGE_FILES],
|
|
837
1097
|
runtimes: ["python"]
|
|
838
1098
|
},
|
|
839
|
-
openvino:
|
|
840
|
-
url: hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"),
|
|
841
|
-
sizeMB: 16,
|
|
842
|
-
runtimes: ["python"]
|
|
843
|
-
}
|
|
1099
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9s.xml"), 16)
|
|
844
1100
|
}
|
|
845
1101
|
},
|
|
846
1102
|
{
|
|
@@ -865,11 +1121,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
865
1121
|
files: [...MLPACKAGE_FILES],
|
|
866
1122
|
runtimes: ["python"]
|
|
867
1123
|
},
|
|
868
|
-
openvino:
|
|
869
|
-
url: hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"),
|
|
870
|
-
sizeMB: 49,
|
|
871
|
-
runtimes: ["python"]
|
|
872
|
-
}
|
|
1124
|
+
openvino: ovFormat(hf("objectDetection/yolov9/openvino/camstack-yolov9c.xml"), 49)
|
|
873
1125
|
}
|
|
874
1126
|
},
|
|
875
1127
|
{
|
|
@@ -894,11 +1146,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
894
1146
|
files: [...MLPACKAGE_FILES],
|
|
895
1147
|
runtimes: ["python"]
|
|
896
1148
|
},
|
|
897
|
-
openvino:
|
|
898
|
-
url: hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"),
|
|
899
|
-
sizeMB: 9,
|
|
900
|
-
runtimes: ["python"]
|
|
901
|
-
}
|
|
1149
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26n.xml"), 9)
|
|
902
1150
|
}
|
|
903
1151
|
},
|
|
904
1152
|
{
|
|
@@ -923,11 +1171,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
923
1171
|
files: [...MLPACKAGE_FILES],
|
|
924
1172
|
runtimes: ["python"]
|
|
925
1173
|
},
|
|
926
|
-
openvino:
|
|
927
|
-
url: hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"),
|
|
928
|
-
sizeMB: 36,
|
|
929
|
-
runtimes: ["python"]
|
|
930
|
-
}
|
|
1174
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26s.xml"), 36)
|
|
931
1175
|
}
|
|
932
1176
|
},
|
|
933
1177
|
{
|
|
@@ -952,11 +1196,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
952
1196
|
files: [...MLPACKAGE_FILES],
|
|
953
1197
|
runtimes: ["python"]
|
|
954
1198
|
},
|
|
955
|
-
openvino:
|
|
956
|
-
url: hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"),
|
|
957
|
-
sizeMB: 78,
|
|
958
|
-
runtimes: ["python"]
|
|
959
|
-
}
|
|
1199
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26m.xml"), 78)
|
|
960
1200
|
}
|
|
961
1201
|
},
|
|
962
1202
|
{
|
|
@@ -981,11 +1221,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
981
1221
|
files: [...MLPACKAGE_FILES],
|
|
982
1222
|
runtimes: ["python"]
|
|
983
1223
|
},
|
|
984
|
-
openvino:
|
|
985
|
-
url: hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"),
|
|
986
|
-
sizeMB: 95,
|
|
987
|
-
runtimes: ["python"]
|
|
988
|
-
}
|
|
1224
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26l.xml"), 95)
|
|
989
1225
|
}
|
|
990
1226
|
},
|
|
991
1227
|
{
|
|
@@ -1010,11 +1246,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1010
1246
|
files: [...MLPACKAGE_FILES],
|
|
1011
1247
|
runtimes: ["python"]
|
|
1012
1248
|
},
|
|
1013
|
-
openvino:
|
|
1014
|
-
url: hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"),
|
|
1015
|
-
sizeMB: 213,
|
|
1016
|
-
runtimes: ["python"]
|
|
1017
|
-
}
|
|
1249
|
+
openvino: ovFormat(hf("objectDetection/yolo26/openvino/camstack-yolo26x.xml"), 213)
|
|
1018
1250
|
}
|
|
1019
1251
|
},
|
|
1020
1252
|
{
|
|
@@ -1039,11 +1271,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1039
1271
|
files: [...MLPACKAGE_FILES],
|
|
1040
1272
|
runtimes: ["python"]
|
|
1041
1273
|
},
|
|
1042
|
-
openvino:
|
|
1043
|
-
url: hfScrypted("openvino/scrypted_yolov9t_relu/best.xml"),
|
|
1044
|
-
sizeMB: 6,
|
|
1045
|
-
runtimes: ["python"]
|
|
1046
|
-
}
|
|
1274
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9t_relu.xml"), 6)
|
|
1047
1275
|
}
|
|
1048
1276
|
},
|
|
1049
1277
|
{
|
|
@@ -1068,11 +1296,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1068
1296
|
files: [...MLPACKAGE_FILES],
|
|
1069
1297
|
runtimes: ["python"]
|
|
1070
1298
|
},
|
|
1071
|
-
openvino:
|
|
1072
|
-
url: hfScrypted("openvino/scrypted_yolov9s_relu/best.xml"),
|
|
1073
|
-
sizeMB: 16,
|
|
1074
|
-
runtimes: ["python"]
|
|
1075
|
-
}
|
|
1299
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9s_relu.xml"), 16)
|
|
1076
1300
|
}
|
|
1077
1301
|
},
|
|
1078
1302
|
{
|
|
@@ -1097,11 +1321,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1097
1321
|
files: [...MLPACKAGE_FILES],
|
|
1098
1322
|
runtimes: ["python"]
|
|
1099
1323
|
},
|
|
1100
|
-
openvino:
|
|
1101
|
-
url: hfScrypted("openvino/scrypted_yolov9c_relu/best.xml"),
|
|
1102
|
-
sizeMB: 49,
|
|
1103
|
-
runtimes: ["python"]
|
|
1104
|
-
}
|
|
1324
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9c_relu.xml"), 49)
|
|
1105
1325
|
}
|
|
1106
1326
|
},
|
|
1107
1327
|
{
|
|
@@ -1126,11 +1346,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1126
1346
|
files: [...MLPACKAGE_FILES],
|
|
1127
1347
|
runtimes: ["python"]
|
|
1128
1348
|
},
|
|
1129
|
-
openvino:
|
|
1130
|
-
url: hfScrypted("openvino/scrypted_yolov9m_relu/best.xml"),
|
|
1131
|
-
sizeMB: 38,
|
|
1132
|
-
runtimes: ["python"]
|
|
1133
|
-
}
|
|
1349
|
+
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1134
1350
|
}
|
|
1135
1351
|
}
|
|
1136
1352
|
];
|
|
@@ -1159,11 +1375,7 @@ var FACE_DETECTION_MODELS = [{
|
|
|
1159
1375
|
files: [...MLPACKAGE_FILES],
|
|
1160
1376
|
runtimes: ["python"]
|
|
1161
1377
|
},
|
|
1162
|
-
openvino:
|
|
1163
|
-
url: hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"),
|
|
1164
|
-
sizeMB: 1.8,
|
|
1165
|
-
runtimes: ["python"]
|
|
1166
|
-
}
|
|
1378
|
+
openvino: ovFormat(hf("faceDetection/scrfd/openvino/camstack-scrfd-2.5g.xml"), 1.8)
|
|
1167
1379
|
}
|
|
1168
1380
|
}, {
|
|
1169
1381
|
id: "scrypted-yolov9t-face",
|
|
@@ -1190,11 +1402,7 @@ var FACE_DETECTION_MODELS = [{
|
|
|
1190
1402
|
files: [...MLPACKAGE_FILES],
|
|
1191
1403
|
runtimes: ["python"]
|
|
1192
1404
|
},
|
|
1193
|
-
openvino:
|
|
1194
|
-
url: hfScrypted("openvino/scrypted_yolov9t_relu_face/best.xml"),
|
|
1195
|
-
sizeMB: 6,
|
|
1196
|
-
runtimes: ["python"]
|
|
1197
|
-
}
|
|
1405
|
+
openvino: ovFormat(hf("faceDetection/scrypted-yolov9-face/openvino/scrypted_yolov9t_relu_face.xml"), 6)
|
|
1198
1406
|
}
|
|
1199
1407
|
}];
|
|
1200
1408
|
var FACE_EMBEDDING_MODELS = [{
|
|
@@ -1224,11 +1432,7 @@ var FACE_EMBEDDING_MODELS = [{
|
|
|
1224
1432
|
files: [...MLPACKAGE_FILES],
|
|
1225
1433
|
runtimes: ["python"]
|
|
1226
1434
|
},
|
|
1227
|
-
openvino:
|
|
1228
|
-
url: hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"),
|
|
1229
|
-
sizeMB: 65,
|
|
1230
|
-
runtimes: ["python"]
|
|
1231
|
-
}
|
|
1435
|
+
openvino: ovFormat(hf("faceRecognition/arcface/openvino/camstack-arcface-r100.xml"), 65)
|
|
1232
1436
|
}
|
|
1233
1437
|
}, {
|
|
1234
1438
|
id: "inception-resnet-v1",
|
|
@@ -1255,11 +1459,7 @@ var FACE_EMBEDDING_MODELS = [{
|
|
|
1255
1459
|
files: [...MLPACKAGE_FILES],
|
|
1256
1460
|
runtimes: ["python"]
|
|
1257
1461
|
},
|
|
1258
|
-
openvino:
|
|
1259
|
-
url: hfScrypted("openvino/inception_resnet_v1/best.xml"),
|
|
1260
|
-
sizeMB: 45,
|
|
1261
|
-
runtimes: ["python"]
|
|
1262
|
-
}
|
|
1462
|
+
openvino: ovFormat(hf("faceRecognition/inception-resnet-v1/openvino/camstack-inception-resnet-v1.xml"), 45)
|
|
1263
1463
|
}
|
|
1264
1464
|
}];
|
|
1265
1465
|
var PLATE_DETECTION_MODELS = [{
|
|
@@ -1287,11 +1487,7 @@ var PLATE_DETECTION_MODELS = [{
|
|
|
1287
1487
|
files: [...MLPACKAGE_FILES],
|
|
1288
1488
|
runtimes: ["python"]
|
|
1289
1489
|
},
|
|
1290
|
-
openvino:
|
|
1291
|
-
url: hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"),
|
|
1292
|
-
sizeMB: 6.1,
|
|
1293
|
-
runtimes: ["python"]
|
|
1294
|
-
}
|
|
1490
|
+
openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
|
|
1295
1491
|
}
|
|
1296
1492
|
}];
|
|
1297
1493
|
var PLATE_OCR_MODELS = [{
|
|
@@ -1319,11 +1515,7 @@ var PLATE_OCR_MODELS = [{
|
|
|
1319
1515
|
files: [...MLPACKAGE_FILES],
|
|
1320
1516
|
runtimes: ["python"]
|
|
1321
1517
|
},
|
|
1322
|
-
openvino:
|
|
1323
|
-
url: hfScrypted("openvino/vgg_english_g2/best.xml"),
|
|
1324
|
-
sizeMB: 7.2,
|
|
1325
|
-
runtimes: ["python"]
|
|
1326
|
-
}
|
|
1518
|
+
openvino: ovFormat(hf("plateRecognition/vgg_english_g2/openvino/vgg_english_g2.xml"), 7.2)
|
|
1327
1519
|
}
|
|
1328
1520
|
}];
|
|
1329
1521
|
var ANIMAL_CLASSIFIER_MODELS = [{
|
|
@@ -1352,11 +1544,7 @@ var ANIMAL_CLASSIFIER_MODELS = [{
|
|
|
1352
1544
|
files: [...MLPACKAGE_FILES],
|
|
1353
1545
|
runtimes: ["python"]
|
|
1354
1546
|
},
|
|
1355
|
-
openvino:
|
|
1356
|
-
url: hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"),
|
|
1357
|
-
sizeMB: 164,
|
|
1358
|
-
runtimes: ["python"]
|
|
1359
|
-
}
|
|
1547
|
+
openvino: ovFormat(hf("animalClassification/animals-10/openvino/camstack-animals-10.xml"), 164)
|
|
1360
1548
|
}
|
|
1361
1549
|
}];
|
|
1362
1550
|
var BIRD_CLASSIFIER_MODELS = [{
|
|
@@ -1385,11 +1573,7 @@ var BIRD_CLASSIFIER_MODELS = [{
|
|
|
1385
1573
|
files: [...MLPACKAGE_FILES],
|
|
1386
1574
|
runtimes: ["python"]
|
|
1387
1575
|
},
|
|
1388
|
-
openvino:
|
|
1389
|
-
url: hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"),
|
|
1390
|
-
sizeMB: 47,
|
|
1391
|
-
runtimes: ["python"]
|
|
1392
|
-
}
|
|
1576
|
+
openvino: ovFormat(hf("animalClassification/bird-nabirds/openvino/camstack-bird-nabirds-404.xml"), 47)
|
|
1393
1577
|
},
|
|
1394
1578
|
extraFiles: [{
|
|
1395
1579
|
url: hf("animalClassification/bird-nabirds/onnx/camstack-bird-nabirds-404-labels.json"),
|
|
@@ -1423,11 +1607,7 @@ var VEHICLE_CLASSIFIER_MODELS = [{
|
|
|
1423
1607
|
files: [...MLPACKAGE_FILES],
|
|
1424
1608
|
runtimes: ["python"]
|
|
1425
1609
|
},
|
|
1426
|
-
openvino:
|
|
1427
|
-
url: hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"),
|
|
1428
|
-
sizeMB: 68,
|
|
1429
|
-
runtimes: ["python"]
|
|
1430
|
-
}
|
|
1610
|
+
openvino: ovFormat(hf("vehicleClassification/efficientnet/openvino/camstack-vehicle-type-efficientnet.xml"), 68)
|
|
1431
1611
|
},
|
|
1432
1612
|
extraFiles: [{
|
|
1433
1613
|
url: hf("vehicleClassification/efficientnet/camstack-vehicle-type-labels.json"),
|
|
@@ -1460,11 +1640,7 @@ var SEGMENTATION_REFINER_MODELS = [{
|
|
|
1460
1640
|
files: [...MLPACKAGE_FILES],
|
|
1461
1641
|
runtimes: ["python"]
|
|
1462
1642
|
},
|
|
1463
|
-
openvino:
|
|
1464
|
-
url: hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"),
|
|
1465
|
-
sizeMB: 2.5,
|
|
1466
|
-
runtimes: ["python"]
|
|
1467
|
-
}
|
|
1643
|
+
openvino: ovFormat(hf("segmentationRefiner/u2netp/openvino/camstack-u2netp.xml"), 2.5)
|
|
1468
1644
|
}
|
|
1469
1645
|
}];
|
|
1470
1646
|
var INSTANCE_SEGMENTATION_MODELS = [
|
|
@@ -1490,11 +1666,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
|
|
|
1490
1666
|
files: [...MLPACKAGE_FILES],
|
|
1491
1667
|
runtimes: ["python"]
|
|
1492
1668
|
},
|
|
1493
|
-
openvino:
|
|
1494
|
-
url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"),
|
|
1495
|
-
sizeMB: 11,
|
|
1496
|
-
runtimes: ["python"]
|
|
1497
|
-
}
|
|
1669
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26n-seg.xml"), 11)
|
|
1498
1670
|
}
|
|
1499
1671
|
},
|
|
1500
1672
|
{
|
|
@@ -1519,11 +1691,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
|
|
|
1519
1691
|
files: [...MLPACKAGE_FILES],
|
|
1520
1692
|
runtimes: ["python"]
|
|
1521
1693
|
},
|
|
1522
|
-
openvino:
|
|
1523
|
-
url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"),
|
|
1524
|
-
sizeMB: 40,
|
|
1525
|
-
runtimes: ["python"]
|
|
1526
|
-
}
|
|
1694
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26s-seg.xml"), 40)
|
|
1527
1695
|
}
|
|
1528
1696
|
},
|
|
1529
1697
|
{
|
|
@@ -1548,11 +1716,7 @@ var INSTANCE_SEGMENTATION_MODELS = [
|
|
|
1548
1716
|
files: [...MLPACKAGE_FILES],
|
|
1549
1717
|
runtimes: ["python"]
|
|
1550
1718
|
},
|
|
1551
|
-
openvino:
|
|
1552
|
-
url: hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"),
|
|
1553
|
-
sizeMB: 90,
|
|
1554
|
-
runtimes: ["python"]
|
|
1555
|
-
}
|
|
1719
|
+
openvino: ovFormat(hf("segmentation/yolo26-seg/openvino/camstack-yolo26m-seg.xml"), 90)
|
|
1556
1720
|
}
|
|
1557
1721
|
}
|
|
1558
1722
|
];
|
|
@@ -1568,16 +1732,12 @@ var AUDIO_CLASSIFIER_MODELS = [{
|
|
|
1568
1732
|
preprocessMode: "resize",
|
|
1569
1733
|
formats: {
|
|
1570
1734
|
onnx: {
|
|
1571
|
-
url: hf("
|
|
1735
|
+
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1572
1736
|
sizeMB: 3.2
|
|
1573
1737
|
},
|
|
1574
|
-
openvino:
|
|
1575
|
-
url: hf("audioClassifier/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1576
|
-
sizeMB: 3.2,
|
|
1577
|
-
runtimes: ["python"]
|
|
1578
|
-
},
|
|
1738
|
+
openvino: ovFormat(hf("audioClassification/yamnet/openvino/camstack-yamnet.xml"), 3.2),
|
|
1579
1739
|
coreml: {
|
|
1580
|
-
url: hf("
|
|
1740
|
+
url: hf("audioClassification/yamnet/onnx/camstack-yamnet.onnx"),
|
|
1581
1741
|
sizeMB: 3.2,
|
|
1582
1742
|
runtimes: ["python"]
|
|
1583
1743
|
}
|
|
@@ -2165,71 +2325,174 @@ var EngineFactory = class {
|
|
|
2165
2325
|
}
|
|
2166
2326
|
};
|
|
2167
2327
|
//#endregion
|
|
2168
|
-
//#region src/detection-pipeline/
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2328
|
+
//#region src/detection-pipeline/engine-provisioner.ts
|
|
2329
|
+
/** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
|
|
2330
|
+
var BACKOFF_SCHEDULE_MS = [
|
|
2331
|
+
5e3,
|
|
2332
|
+
15e3,
|
|
2333
|
+
3e4,
|
|
2334
|
+
6e4,
|
|
2335
|
+
12e4,
|
|
2336
|
+
3e5
|
|
2337
|
+
];
|
|
2338
|
+
var IDLE_STATE = {
|
|
2339
|
+
runtimeId: null,
|
|
2340
|
+
device: null,
|
|
2341
|
+
state: "idle"
|
|
2342
|
+
};
|
|
2343
|
+
var EngineProvisioner = class {
|
|
2344
|
+
fx;
|
|
2345
|
+
current = IDLE_STATE;
|
|
2346
|
+
/** Bumped on every select/dispose — stale async results (old generation) are ignored. */
|
|
2347
|
+
generation = 0;
|
|
2348
|
+
cancelTimer = null;
|
|
2349
|
+
retryIndex = 0;
|
|
2350
|
+
constructor(fx) {
|
|
2351
|
+
this.fx = fx;
|
|
2352
|
+
}
|
|
2353
|
+
get state() {
|
|
2354
|
+
return this.current;
|
|
2355
|
+
}
|
|
2356
|
+
isReady() {
|
|
2357
|
+
return this.current.state === "ready";
|
|
2358
|
+
}
|
|
2359
|
+
select(runtimeId, device) {
|
|
2360
|
+
this.generation++;
|
|
2361
|
+
this.retryIndex = 0;
|
|
2362
|
+
this.clearTimer();
|
|
2363
|
+
this.transition({
|
|
2364
|
+
runtimeId,
|
|
2365
|
+
device,
|
|
2366
|
+
state: "installing",
|
|
2367
|
+
progress: 0
|
|
2368
|
+
});
|
|
2369
|
+
this.provision(this.generation, runtimeId, device);
|
|
2370
|
+
}
|
|
2371
|
+
dispose() {
|
|
2372
|
+
this.generation++;
|
|
2373
|
+
this.clearTimer();
|
|
2374
|
+
}
|
|
2375
|
+
clearTimer() {
|
|
2376
|
+
if (this.cancelTimer !== null) {
|
|
2377
|
+
this.cancelTimer();
|
|
2378
|
+
this.cancelTimer = null;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
transition(next) {
|
|
2382
|
+
this.current = next;
|
|
2383
|
+
this.fx.onChange(next);
|
|
2384
|
+
}
|
|
2385
|
+
async provision(gen, runtimeId, device) {
|
|
2386
|
+
try {
|
|
2387
|
+
await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
|
|
2388
|
+
if (gen !== this.generation) return;
|
|
2389
|
+
this.transition({
|
|
2390
|
+
runtimeId,
|
|
2391
|
+
device,
|
|
2392
|
+
state: "verifying",
|
|
2393
|
+
progress: 100
|
|
2394
|
+
});
|
|
2395
|
+
await this.fx.verify(runtimeId, device);
|
|
2396
|
+
if (gen !== this.generation) return;
|
|
2397
|
+
this.retryIndex = 0;
|
|
2398
|
+
this.transition({
|
|
2399
|
+
runtimeId,
|
|
2400
|
+
device,
|
|
2401
|
+
state: "ready"
|
|
2402
|
+
});
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
if (gen !== this.generation) return;
|
|
2405
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2406
|
+
const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
|
|
2407
|
+
this.retryIndex++;
|
|
2408
|
+
const nextRetryAt = this.fx.now() + delay;
|
|
2409
|
+
this.transition({
|
|
2410
|
+
runtimeId,
|
|
2411
|
+
device,
|
|
2412
|
+
state: "failed",
|
|
2413
|
+
error: message,
|
|
2414
|
+
nextRetryAt
|
|
2415
|
+
});
|
|
2416
|
+
this.cancelTimer = this.fx.setTimer(delay, () => {
|
|
2417
|
+
if (gen !== this.generation) return;
|
|
2418
|
+
this.cancelTimer = null;
|
|
2419
|
+
this.transition({
|
|
2420
|
+
runtimeId,
|
|
2421
|
+
device,
|
|
2422
|
+
state: "installing",
|
|
2423
|
+
progress: 0
|
|
2424
|
+
});
|
|
2425
|
+
this.provision(gen, runtimeId, device);
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
//#endregion
|
|
2431
|
+
//#region src/detection-pipeline/postprocess/dispatch.ts
|
|
2432
|
+
var VALID_KINDS = new Set([
|
|
2433
|
+
"detections",
|
|
2434
|
+
"classifications",
|
|
2435
|
+
"embedding",
|
|
2436
|
+
"text",
|
|
2437
|
+
"mask"
|
|
2438
|
+
]);
|
|
2439
|
+
/**
|
|
2440
|
+
* Type guard: validates that a structured payload from the Python pool is a
|
|
2441
|
+
* well-formed StepOutput discriminated union.
|
|
2442
|
+
*
|
|
2443
|
+
* The Python inference_pool.py always sets `output.structured` with a `kind`
|
|
2444
|
+
* field. This guard narrows `Record<string, unknown>` to `StepOutput` without
|
|
2445
|
+
* resorting to double-cast.
|
|
2446
|
+
*/
|
|
2447
|
+
function isStepOutput(value) {
|
|
2448
|
+
return typeof value === "object" && value !== null && typeof value["kind"] === "string" && VALID_KINDS.has(value["kind"]);
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Return the structured payload as `StepOutput`, or null if it fails validation.
|
|
2452
|
+
* Callers must handle null by proceeding to raw-tensor postprocessing.
|
|
2453
|
+
*/
|
|
2454
|
+
function tryStructured(output) {
|
|
2455
|
+
if (!output.structured) return null;
|
|
2456
|
+
if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
|
|
2457
|
+
return output.structured;
|
|
2458
|
+
}
|
|
2459
|
+
function postprocessYolo(output, stepDef) {
|
|
2460
|
+
const structured = tryStructured(output);
|
|
2461
|
+
if (structured) return structured;
|
|
2462
|
+
const tensor = output.tensor;
|
|
2463
|
+
if (!tensor) throw new Error("YOLO postprocessor: no tensor in engine output");
|
|
2464
|
+
const labels = stepDef.labels ?? [];
|
|
2465
|
+
const numClasses = labels.length || 80;
|
|
2466
|
+
const numBoxes = tensor.length / (4 + numClasses);
|
|
2467
|
+
const letterbox = output.letterbox;
|
|
2468
|
+
const dets = [];
|
|
2469
|
+
for (let i = 0; i < numBoxes; i++) {
|
|
2470
|
+
const cx = tensor[i];
|
|
2471
|
+
const cy = tensor[1 * numBoxes + i];
|
|
2472
|
+
const w = tensor[2 * numBoxes + i];
|
|
2473
|
+
const h = tensor[3 * numBoxes + i];
|
|
2474
|
+
let bestScore = -Infinity;
|
|
2475
|
+
let bestClass = 0;
|
|
2476
|
+
for (let j = 0; j < numClasses; j++) {
|
|
2477
|
+
const score = tensor[(4 + j) * numBoxes + i];
|
|
2478
|
+
if (score > bestScore) {
|
|
2479
|
+
bestScore = score;
|
|
2480
|
+
bestClass = j;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
if (bestScore <= 0) continue;
|
|
2484
|
+
let x1 = cx - w / 2;
|
|
2485
|
+
let y1 = cy - h / 2;
|
|
2486
|
+
let x2 = cx + w / 2;
|
|
2487
|
+
let y2 = cy + h / 2;
|
|
2488
|
+
if (letterbox) {
|
|
2489
|
+
x1 = (x1 - letterbox.padX) / letterbox.scale;
|
|
2490
|
+
y1 = (y1 - letterbox.padY) / letterbox.scale;
|
|
2491
|
+
x2 = (x2 - letterbox.padX) / letterbox.scale;
|
|
2492
|
+
y2 = (y2 - letterbox.padY) / letterbox.scale;
|
|
2493
|
+
}
|
|
2494
|
+
const label = labels[bestClass] ?? String(bestClass);
|
|
2495
|
+
dets.push({
|
|
2233
2496
|
class: label,
|
|
2234
2497
|
score: bestScore,
|
|
2235
2498
|
bbox: [
|
|
@@ -2360,7 +2623,7 @@ function postprocessArcface(output, _stepDef) {
|
|
|
2360
2623
|
let sumSq = 0;
|
|
2361
2624
|
for (let i = 0; i < tensor.length; i++) sumSq += tensor[i] * tensor[i];
|
|
2362
2625
|
const norm = Math.sqrt(sumSq);
|
|
2363
|
-
const normalized =
|
|
2626
|
+
const normalized = Array.from({ length: tensor.length });
|
|
2364
2627
|
for (let i = 0; i < tensor.length; i++) normalized[i] = norm === 0 ? 0 : tensor[i] / norm;
|
|
2365
2628
|
return {
|
|
2366
2629
|
kind: "embedding",
|
|
@@ -3396,403 +3659,143 @@ var PipelineExecutor = class {
|
|
|
3396
3659
|
return output;
|
|
3397
3660
|
}
|
|
3398
3661
|
async executeChildren(children, parentDetection, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg) {
|
|
3399
|
-
for (const child of children) {
|
|
3400
|
-
if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
|
|
3401
|
-
const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
|
|
3402
|
-
if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
|
|
3403
|
-
if (isBboxDegenerate(parentDetection.bbox)) continue;
|
|
3404
|
-
try {
|
|
3405
|
-
const childStart = Date.now();
|
|
3406
|
-
const fullFrameJpeg = await fullFrameJpegProvider();
|
|
3407
|
-
const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
|
|
3408
|
-
let cropJpegBuf;
|
|
3409
|
-
let cropW;
|
|
3410
|
-
let cropH;
|
|
3411
|
-
if (modelEntry?.faceAlignment) {
|
|
3412
|
-
const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
|
|
3413
|
-
if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
|
|
3414
|
-
const lms = parentDetection.landmarks;
|
|
3415
|
-
if (lms && lms.length >= 5) {
|
|
3416
|
-
const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
|
|
3417
|
-
cropJpegBuf = aligned.jpeg;
|
|
3418
|
-
cropW = aligned.width;
|
|
3419
|
-
cropH = aligned.height;
|
|
3420
|
-
} else {
|
|
3421
|
-
const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
|
|
3422
|
-
cropJpegBuf = crop.jpeg;
|
|
3423
|
-
cropW = crop.width;
|
|
3424
|
-
cropH = crop.height;
|
|
3425
|
-
}
|
|
3426
|
-
} else {
|
|
3427
|
-
const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
|
|
3428
|
-
cropJpegBuf = crop.jpeg;
|
|
3429
|
-
cropW = crop.width;
|
|
3430
|
-
cropH = crop.height;
|
|
3431
|
-
}
|
|
3432
|
-
const childOutput = await this.executeStep(child, {
|
|
3433
|
-
kind: "jpeg",
|
|
3434
|
-
data: cropJpegBuf
|
|
3435
|
-
}, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
|
|
3436
|
-
const childMs = Date.now() - childStart;
|
|
3437
|
-
const detailsBefore = ctx.details.length;
|
|
3438
|
-
applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
|
|
3439
|
-
if (childOutput.kind === "detections" && child.children.length > 0) {
|
|
3440
|
-
const newDetails = ctx.details.slice(detailsBefore);
|
|
3441
|
-
for (const detail of newDetails) {
|
|
3442
|
-
detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
|
|
3443
|
-
if (detail.landmarks) {
|
|
3444
|
-
const [px1, py1] = parentDetection.bbox;
|
|
3445
|
-
detail.landmarks = detail.landmarks.map((l) => ({
|
|
3446
|
-
x: l.x + px1,
|
|
3447
|
-
y: l.y + py1
|
|
3448
|
-
}));
|
|
3449
|
-
}
|
|
3450
|
-
await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
|
|
3451
|
-
}
|
|
3452
|
-
}
|
|
3453
|
-
} catch {}
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
matchesInputClasses(className, inputClasses) {
|
|
3457
|
-
if (inputClasses.length === 0) return true;
|
|
3458
|
-
return inputClasses.includes(className);
|
|
3459
|
-
}
|
|
3460
|
-
/**
|
|
3461
|
-
* Apply the object-detection step's macro filter + per-macro
|
|
3462
|
-
* minConfidence sliders introduced in the Phase 6 step rework.
|
|
3463
|
-
*
|
|
3464
|
-
* Expected `settings` shape:
|
|
3465
|
-
* - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
|
|
3466
|
-
* Empty array = all allowed (legacy behaviour).
|
|
3467
|
-
* - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
|
|
3468
|
-
*/
|
|
3469
|
-
matchesMacroFilter(macroClass, score, settings) {
|
|
3470
|
-
if (!settings) return true;
|
|
3471
|
-
const enabled = settings["enabledMacroClasses"];
|
|
3472
|
-
if (Array.isArray(enabled) && enabled.length > 0) {
|
|
3473
|
-
if (!enabled.includes(macroClass)) return false;
|
|
3474
|
-
}
|
|
3475
|
-
const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
|
|
3476
|
-
if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
|
|
3477
|
-
return true;
|
|
3478
|
-
}
|
|
3479
|
-
};
|
|
3480
|
-
function capitalize(s) {
|
|
3481
|
-
if (s.length === 0) return s;
|
|
3482
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3483
|
-
}
|
|
3484
|
-
//#endregion
|
|
3485
|
-
//#region src/detection-pipeline/pipeline/tree-builder.ts
|
|
3486
|
-
/**
|
|
3487
|
-
* Build an executable tree from user config.
|
|
3488
|
-
*
|
|
3489
|
-
* @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
|
|
3490
|
-
* @param getEngine - Function that returns an IInferenceEngine for a step ID.
|
|
3491
|
-
* Throws if step not loaded.
|
|
3492
|
-
*/
|
|
3493
|
-
function buildExecutableTree(steps, getEngine) {
|
|
3494
|
-
return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
|
|
3495
|
-
}
|
|
3496
|
-
function buildNode(step, getEngine) {
|
|
3497
|
-
const definition = getStepDefinition(step.addonId);
|
|
3498
|
-
const engine = getEngine(step.addonId);
|
|
3499
|
-
const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
|
|
3500
|
-
const mergedSettings = {
|
|
3501
|
-
...collectSchemaDefaults(step.addonId),
|
|
3502
|
-
...step.settings
|
|
3503
|
-
};
|
|
3504
|
-
return {
|
|
3505
|
-
stepId: step.addonId,
|
|
3506
|
-
definition,
|
|
3507
|
-
engine,
|
|
3508
|
-
modelId: step.modelId,
|
|
3509
|
-
inputClasses: definition.inputClasses ?? [],
|
|
3510
|
-
enabled: step.enabled,
|
|
3511
|
-
children,
|
|
3512
|
-
...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
|
|
3513
|
-
};
|
|
3514
|
-
}
|
|
3515
|
-
/**
|
|
3516
|
-
* Walk the step's declared `getConfigSchema()` and collect every field
|
|
3517
|
-
* with a primitive `default` value into a plain record. Group fields
|
|
3518
|
-
* are flattened — a nested `{ type: 'group', fields: [...] }` is
|
|
3519
|
-
* entered recursively. Fields without a declared default are skipped.
|
|
3520
|
-
*/
|
|
3521
|
-
function collectSchemaDefaults(stepId) {
|
|
3522
|
-
const schema = getStep(stepId).getConfigSchema();
|
|
3523
|
-
const out = {};
|
|
3524
|
-
walkFieldsForDefaults(schema, out);
|
|
3525
|
-
return out;
|
|
3526
|
-
}
|
|
3527
|
-
function walkFieldsForDefaults(fields, out) {
|
|
3528
|
-
for (const field of fields) {
|
|
3529
|
-
if ("type" in field && field.type === "group" && "fields" in field) {
|
|
3530
|
-
walkFieldsForDefaults(field.fields, out);
|
|
3531
|
-
continue;
|
|
3532
|
-
}
|
|
3533
|
-
if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
//#endregion
|
|
3537
|
-
//#region src/detection-pipeline/runtimes.ts
|
|
3538
|
-
var AUTO = {
|
|
3539
|
-
value: "auto",
|
|
3540
|
-
label: "Auto"
|
|
3541
|
-
};
|
|
3542
|
-
var CPU = {
|
|
3543
|
-
value: "cpu",
|
|
3544
|
-
label: "CPU"
|
|
3545
|
-
};
|
|
3546
|
-
var RUNTIMES = [
|
|
3547
|
-
{
|
|
3548
|
-
id: "onnx",
|
|
3549
|
-
label: "ONNX Runtime",
|
|
3550
|
-
supports: () => true,
|
|
3551
|
-
devices: (hw) => {
|
|
3552
|
-
if (!hw) return [CPU];
|
|
3553
|
-
const out = [CPU];
|
|
3554
|
-
if (hw.gpu?.type === "nvidia") out.push({
|
|
3555
|
-
value: "cuda",
|
|
3556
|
-
label: "CUDA"
|
|
3557
|
-
});
|
|
3558
|
-
if (hw.npu?.type === "apple-ane") out.push({
|
|
3559
|
-
value: "coreml",
|
|
3560
|
-
label: "CoreML EP"
|
|
3561
|
-
});
|
|
3562
|
-
return out;
|
|
3563
|
-
},
|
|
3564
|
-
defaultDevice: "cpu",
|
|
3565
|
-
modelFormat: "onnx",
|
|
3566
|
-
pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
|
|
3567
|
-
tuning: {
|
|
3568
|
-
concurrency: 4,
|
|
3569
|
-
batchMode: "list",
|
|
3570
|
-
maxBatchSize: 8,
|
|
3571
|
-
intraOpThreads: 0
|
|
3572
|
-
}
|
|
3573
|
-
},
|
|
3574
|
-
{
|
|
3575
|
-
id: "openvino",
|
|
3576
|
-
label: "OpenVINO",
|
|
3577
|
-
supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
|
|
3578
|
-
devices: (hw) => {
|
|
3579
|
-
if (!hw) return [AUTO];
|
|
3580
|
-
const out = [AUTO, CPU];
|
|
3581
|
-
if (hw.gpu?.type === "intel") out.push({
|
|
3582
|
-
value: "gpu",
|
|
3583
|
-
label: "GPU"
|
|
3584
|
-
});
|
|
3585
|
-
if (hw.npu?.type === "intel-npu") out.push({
|
|
3586
|
-
value: "npu",
|
|
3587
|
-
label: "NPU"
|
|
3588
|
-
});
|
|
3589
|
-
return out;
|
|
3590
|
-
},
|
|
3591
|
-
defaultDevice: "auto",
|
|
3592
|
-
modelFormat: "openvino",
|
|
3593
|
-
pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
|
|
3594
|
-
tuning: {
|
|
3595
|
-
concurrency: 1,
|
|
3596
|
-
batchMode: "none",
|
|
3597
|
-
numStreams: 0
|
|
3598
|
-
}
|
|
3599
|
-
},
|
|
3600
|
-
{
|
|
3601
|
-
id: "coreml",
|
|
3602
|
-
label: "CoreML",
|
|
3603
|
-
supports: (env) => env.platform === "darwin",
|
|
3604
|
-
devices: (hw) => {
|
|
3605
|
-
const all = {
|
|
3606
|
-
value: "all",
|
|
3607
|
-
label: "All (ANE + GPU + CPU)"
|
|
3608
|
-
};
|
|
3609
|
-
if (!hw) return [all];
|
|
3610
|
-
const out = [all];
|
|
3611
|
-
if (hw.npu?.type === "apple-ane") out.push({
|
|
3612
|
-
value: "ane",
|
|
3613
|
-
label: "Apple Neural Engine"
|
|
3614
|
-
});
|
|
3615
|
-
out.push({
|
|
3616
|
-
value: "gpu",
|
|
3617
|
-
label: "GPU"
|
|
3618
|
-
}, CPU);
|
|
3619
|
-
return out;
|
|
3620
|
-
},
|
|
3621
|
-
defaultDevice: "all",
|
|
3622
|
-
modelFormat: "coreml",
|
|
3623
|
-
pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
|
|
3624
|
-
tuning: {
|
|
3625
|
-
concurrency: 1,
|
|
3626
|
-
batchMode: "none",
|
|
3627
|
-
windowMs: 8,
|
|
3628
|
-
maxBatchSize: 8,
|
|
3629
|
-
numWorkers: 1
|
|
3630
|
-
}
|
|
3631
|
-
}
|
|
3632
|
-
];
|
|
3633
|
-
function def(id) {
|
|
3634
|
-
const d = RUNTIMES.find((r) => r.id === id);
|
|
3635
|
-
if (!d) throw new Error(`Unknown runtime: ${id}`);
|
|
3636
|
-
return d;
|
|
3637
|
-
}
|
|
3638
|
-
function supportedRuntimes(env) {
|
|
3639
|
-
return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
|
|
3640
|
-
}
|
|
3641
|
-
function runtimeDevices(id, hardware) {
|
|
3642
|
-
return def(id).devices(hardware);
|
|
3643
|
-
}
|
|
3644
|
-
function defaultDeviceFor(id) {
|
|
3645
|
-
return def(id).defaultDevice;
|
|
3646
|
-
}
|
|
3647
|
-
function pythonRequirementsFor(id) {
|
|
3648
|
-
return def(id).pythonRequirements;
|
|
3649
|
-
}
|
|
3650
|
-
function modelFormatFor(id) {
|
|
3651
|
-
return def(id).modelFormat;
|
|
3652
|
-
}
|
|
3653
|
-
function tuningFor(id) {
|
|
3654
|
-
return def(id).tuning;
|
|
3655
|
-
}
|
|
3656
|
-
function runtimeLabel(id) {
|
|
3657
|
-
return def(id).label;
|
|
3658
|
-
}
|
|
3659
|
-
/**
|
|
3660
|
-
* Proactive-install hint kept for back-compat (re-exported from index.ts).
|
|
3661
|
-
* Intel on Linux warrants installing openvino; coremltools covers macOS;
|
|
3662
|
-
* openvino has no AMD/NVIDIA backend.
|
|
3663
|
-
*/
|
|
3664
|
-
function shouldInstallOpenvino(env) {
|
|
3665
|
-
if (env.platform === "darwin") return false;
|
|
3666
|
-
return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
|
|
3667
|
-
}
|
|
3668
|
-
//#endregion
|
|
3669
|
-
//#region src/detection-pipeline/engine-provisioner.ts
|
|
3670
|
-
/** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
|
|
3671
|
-
var BACKOFF_SCHEDULE_MS = [
|
|
3672
|
-
5e3,
|
|
3673
|
-
15e3,
|
|
3674
|
-
3e4,
|
|
3675
|
-
6e4,
|
|
3676
|
-
12e4,
|
|
3677
|
-
3e5
|
|
3678
|
-
];
|
|
3679
|
-
var IDLE_STATE = {
|
|
3680
|
-
runtimeId: null,
|
|
3681
|
-
device: null,
|
|
3682
|
-
state: "idle"
|
|
3683
|
-
};
|
|
3684
|
-
var EngineProvisioner = class {
|
|
3685
|
-
fx;
|
|
3686
|
-
current = IDLE_STATE;
|
|
3687
|
-
/** Bumped on every select/dispose — stale async results (old generation) are ignored. */
|
|
3688
|
-
generation = 0;
|
|
3689
|
-
cancelTimer = null;
|
|
3690
|
-
retryIndex = 0;
|
|
3691
|
-
constructor(fx) {
|
|
3692
|
-
this.fx = fx;
|
|
3693
|
-
}
|
|
3694
|
-
get state() {
|
|
3695
|
-
return this.current;
|
|
3696
|
-
}
|
|
3697
|
-
isReady() {
|
|
3698
|
-
return this.current.state === "ready";
|
|
3699
|
-
}
|
|
3700
|
-
select(runtimeId, device) {
|
|
3701
|
-
this.generation++;
|
|
3702
|
-
this.retryIndex = 0;
|
|
3703
|
-
this.clearTimer();
|
|
3704
|
-
this.transition({
|
|
3705
|
-
runtimeId,
|
|
3706
|
-
device,
|
|
3707
|
-
state: "installing",
|
|
3708
|
-
progress: 0
|
|
3709
|
-
});
|
|
3710
|
-
this.provision(this.generation, runtimeId, device);
|
|
3711
|
-
}
|
|
3712
|
-
dispose() {
|
|
3713
|
-
this.generation++;
|
|
3714
|
-
this.clearTimer();
|
|
3715
|
-
}
|
|
3716
|
-
clearTimer() {
|
|
3717
|
-
if (this.cancelTimer !== null) {
|
|
3718
|
-
this.cancelTimer();
|
|
3719
|
-
this.cancelTimer = null;
|
|
3662
|
+
for (const child of children) {
|
|
3663
|
+
if (!this.matchesInputClasses(parentDetection.macroClass, child.inputClasses)) continue;
|
|
3664
|
+
const minParentScore = child.settings?.minParentScore ?? child.definition?.defaultMinParentScore;
|
|
3665
|
+
if (typeof minParentScore === "number" && parentDetection.score < minParentScore) continue;
|
|
3666
|
+
if (isBboxDegenerate(parentDetection.bbox)) continue;
|
|
3667
|
+
try {
|
|
3668
|
+
const childStart = Date.now();
|
|
3669
|
+
const fullFrameJpeg = await fullFrameJpegProvider();
|
|
3670
|
+
const modelEntry = child.definition.models.find((m) => m.id === child.modelId);
|
|
3671
|
+
let cropJpegBuf;
|
|
3672
|
+
let cropW;
|
|
3673
|
+
let cropH;
|
|
3674
|
+
if (modelEntry?.faceAlignment) {
|
|
3675
|
+
const minFaceSize = typeof child.settings?.["minFaceSize"] === "number" ? child.settings["minFaceSize"] : DEFAULT_MIN_FACE_SIZE_PX;
|
|
3676
|
+
if (bboxShortSide(parentDetection.bbox) < minFaceSize) continue;
|
|
3677
|
+
const lms = parentDetection.landmarks;
|
|
3678
|
+
if (lms && lms.length >= 5) {
|
|
3679
|
+
const aligned = await alignFaceCrop(fullFrameJpeg, parentDetection.bbox, lms, imageWidth, imageHeight, { outSize: modelEntry.inputSize.width });
|
|
3680
|
+
cropJpegBuf = aligned.jpeg;
|
|
3681
|
+
cropW = aligned.width;
|
|
3682
|
+
cropH = aligned.height;
|
|
3683
|
+
} else {
|
|
3684
|
+
const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
|
|
3685
|
+
cropJpegBuf = crop.jpeg;
|
|
3686
|
+
cropW = crop.width;
|
|
3687
|
+
cropH = crop.height;
|
|
3688
|
+
}
|
|
3689
|
+
} else {
|
|
3690
|
+
const crop = await cropJpeg(fullFrameJpeg, parentDetection.bbox, imageWidth, imageHeight);
|
|
3691
|
+
cropJpegBuf = crop.jpeg;
|
|
3692
|
+
cropW = crop.width;
|
|
3693
|
+
cropH = crop.height;
|
|
3694
|
+
}
|
|
3695
|
+
const childOutput = await this.executeStep(child, {
|
|
3696
|
+
kind: "jpeg",
|
|
3697
|
+
data: cropJpegBuf
|
|
3698
|
+
}, cropW, cropH, "crop-roi", parentDetection.bbox, parentDetection.macroClass, traceBuilder, stepTimings, poolAgg);
|
|
3699
|
+
const childMs = Date.now() - childStart;
|
|
3700
|
+
const detailsBefore = ctx.details.length;
|
|
3701
|
+
applyChildOutput(parentDetection, child, childOutput, childMs, ctx);
|
|
3702
|
+
if (childOutput.kind === "detections" && child.children.length > 0) {
|
|
3703
|
+
const newDetails = ctx.details.slice(detailsBefore);
|
|
3704
|
+
for (const detail of newDetails) {
|
|
3705
|
+
detail.bbox = transformBboxToImageSpace(detail.bbox, parentDetection.bbox);
|
|
3706
|
+
if (detail.landmarks) {
|
|
3707
|
+
const [px1, py1] = parentDetection.bbox;
|
|
3708
|
+
detail.landmarks = detail.landmarks.map((l) => ({
|
|
3709
|
+
x: l.x + px1,
|
|
3710
|
+
y: l.y + py1
|
|
3711
|
+
}));
|
|
3712
|
+
}
|
|
3713
|
+
await this.executeChildren(child.children, detail, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
} catch {}
|
|
3720
3717
|
}
|
|
3721
3718
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3719
|
+
matchesInputClasses(className, inputClasses) {
|
|
3720
|
+
if (inputClasses.length === 0) return true;
|
|
3721
|
+
return inputClasses.includes(className);
|
|
3725
3722
|
}
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
runtimeId,
|
|
3741
|
-
device,
|
|
3742
|
-
state: "ready"
|
|
3743
|
-
});
|
|
3744
|
-
} catch (err) {
|
|
3745
|
-
if (gen !== this.generation) return;
|
|
3746
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3747
|
-
const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
|
|
3748
|
-
this.retryIndex++;
|
|
3749
|
-
const nextRetryAt = this.fx.now() + delay;
|
|
3750
|
-
this.transition({
|
|
3751
|
-
runtimeId,
|
|
3752
|
-
device,
|
|
3753
|
-
state: "failed",
|
|
3754
|
-
error: message,
|
|
3755
|
-
nextRetryAt
|
|
3756
|
-
});
|
|
3757
|
-
this.cancelTimer = this.fx.setTimer(delay, () => {
|
|
3758
|
-
if (gen !== this.generation) return;
|
|
3759
|
-
this.cancelTimer = null;
|
|
3760
|
-
this.transition({
|
|
3761
|
-
runtimeId,
|
|
3762
|
-
device,
|
|
3763
|
-
state: "installing",
|
|
3764
|
-
progress: 0
|
|
3765
|
-
});
|
|
3766
|
-
this.provision(gen, runtimeId, device);
|
|
3767
|
-
});
|
|
3723
|
+
/**
|
|
3724
|
+
* Apply the object-detection step's macro filter + per-macro
|
|
3725
|
+
* minConfidence sliders introduced in the Phase 6 step rework.
|
|
3726
|
+
*
|
|
3727
|
+
* Expected `settings` shape:
|
|
3728
|
+
* - `enabledMacroClasses`: readonly string[] (e.g. ['person','vehicle','animal']).
|
|
3729
|
+
* Empty array = all allowed (legacy behaviour).
|
|
3730
|
+
* - `minConfidence<Macro>`: number (e.g. `minConfidencePerson: 0.5`)
|
|
3731
|
+
*/
|
|
3732
|
+
matchesMacroFilter(macroClass, score, settings) {
|
|
3733
|
+
if (!settings) return true;
|
|
3734
|
+
const enabled = settings["enabledMacroClasses"];
|
|
3735
|
+
if (Array.isArray(enabled) && enabled.length > 0) {
|
|
3736
|
+
if (!enabled.includes(macroClass)) return false;
|
|
3768
3737
|
}
|
|
3738
|
+
const perMacroThreshold = settings[`minConfidence${capitalize(macroClass)}`];
|
|
3739
|
+
if (typeof perMacroThreshold === "number" && score < perMacroThreshold) return false;
|
|
3740
|
+
return true;
|
|
3769
3741
|
}
|
|
3770
3742
|
};
|
|
3743
|
+
function capitalize(s) {
|
|
3744
|
+
if (s.length === 0) return s;
|
|
3745
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3746
|
+
}
|
|
3771
3747
|
//#endregion
|
|
3772
|
-
//#region src/detection-pipeline/
|
|
3773
|
-
var PREFERENCE = [
|
|
3774
|
-
"coreml",
|
|
3775
|
-
"openvino",
|
|
3776
|
-
"onnx"
|
|
3777
|
-
];
|
|
3748
|
+
//#region src/detection-pipeline/pipeline/tree-builder.ts
|
|
3778
3749
|
/**
|
|
3779
|
-
*
|
|
3780
|
-
*
|
|
3781
|
-
* Logic:
|
|
3782
|
-
* 1. If `bestBackendHint` is in the supported set, use it.
|
|
3783
|
-
* 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
|
|
3784
|
-
* 3. Floor to `'onnx'` (always supported).
|
|
3750
|
+
* Build an executable tree from user config.
|
|
3785
3751
|
*
|
|
3786
|
-
*
|
|
3752
|
+
* @param steps - User-configured pipeline steps (from PipelineDefaultStep[])
|
|
3753
|
+
* @param getEngine - Function that returns an IInferenceEngine for a step ID.
|
|
3754
|
+
* Throws if step not loaded.
|
|
3787
3755
|
*/
|
|
3788
|
-
function
|
|
3789
|
-
|
|
3790
|
-
|
|
3756
|
+
function buildExecutableTree(steps, getEngine) {
|
|
3757
|
+
return { roots: steps.filter((s) => s.enabled).filter((s) => s.slot !== "audio-classifier").map((s) => buildNode(s, getEngine)) };
|
|
3758
|
+
}
|
|
3759
|
+
function buildNode(step, getEngine) {
|
|
3760
|
+
const definition = getStepDefinition(step.addonId);
|
|
3761
|
+
const engine = getEngine(step.addonId);
|
|
3762
|
+
const children = (step.children ?? []).filter((c) => c.enabled).map((c) => buildNode(c, getEngine));
|
|
3763
|
+
const mergedSettings = {
|
|
3764
|
+
...collectSchemaDefaults(step.addonId),
|
|
3765
|
+
...step.settings
|
|
3766
|
+
};
|
|
3791
3767
|
return {
|
|
3792
|
-
|
|
3793
|
-
|
|
3768
|
+
stepId: step.addonId,
|
|
3769
|
+
definition,
|
|
3770
|
+
engine,
|
|
3771
|
+
modelId: step.modelId,
|
|
3772
|
+
inputClasses: definition.inputClasses ?? [],
|
|
3773
|
+
enabled: step.enabled,
|
|
3774
|
+
children,
|
|
3775
|
+
...Object.keys(mergedSettings).length > 0 ? { settings: mergedSettings } : {}
|
|
3794
3776
|
};
|
|
3795
3777
|
}
|
|
3778
|
+
/**
|
|
3779
|
+
* Walk the step's declared `getConfigSchema()` and collect every field
|
|
3780
|
+
* with a primitive `default` value into a plain record. Group fields
|
|
3781
|
+
* are flattened — a nested `{ type: 'group', fields: [...] }` is
|
|
3782
|
+
* entered recursively. Fields without a declared default are skipped.
|
|
3783
|
+
*/
|
|
3784
|
+
function collectSchemaDefaults(stepId) {
|
|
3785
|
+
const schema = getStep(stepId).getConfigSchema();
|
|
3786
|
+
const out = {};
|
|
3787
|
+
walkFieldsForDefaults(schema, out);
|
|
3788
|
+
return out;
|
|
3789
|
+
}
|
|
3790
|
+
function walkFieldsForDefaults(fields, out) {
|
|
3791
|
+
for (const field of fields) {
|
|
3792
|
+
if ("type" in field && field.type === "group" && "fields" in field) {
|
|
3793
|
+
walkFieldsForDefaults(field.fields, out);
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3796
|
+
if ("key" in field && "default" in field && field.default !== void 0) out[field.key] = field.default;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3796
3799
|
//#endregion
|
|
3797
3800
|
//#region src/detection-pipeline/provider.ts
|
|
3798
3801
|
/**
|
|
@@ -3995,6 +3998,27 @@ function toProbedHardware(hw) {
|
|
|
3995
3998
|
gpu: hw.gpu ? { type: hw.gpu.type } : null
|
|
3996
3999
|
};
|
|
3997
4000
|
}
|
|
4001
|
+
var ONNX_FLOOR = {
|
|
4002
|
+
runtime: "python",
|
|
4003
|
+
backend: "onnx",
|
|
4004
|
+
format: "onnx",
|
|
4005
|
+
device: "cpu"
|
|
4006
|
+
};
|
|
4007
|
+
/**
|
|
4008
|
+
* Build the onnx-cpu floor pick using `pickBestRuntime` with a null hardware
|
|
4009
|
+
* env. Used wherever the old `detectBestEngine()` sync probe fell back — the
|
|
4010
|
+
* result is identical (onnx / cpu) but is now derived through the shared rules
|
|
4011
|
+
* instead of duplicated inline.
|
|
4012
|
+
*/
|
|
4013
|
+
function onnxFloorPick() {
|
|
4014
|
+
const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
|
|
4015
|
+
return {
|
|
4016
|
+
runtime: "python",
|
|
4017
|
+
backend: pick.runtimeId,
|
|
4018
|
+
format: modelFormatFor(pick.runtimeId),
|
|
4019
|
+
device: pick.device
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
3998
4022
|
var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
3999
4023
|
modelsDir;
|
|
4000
4024
|
eventBus;
|
|
@@ -4053,6 +4077,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4053
4077
|
*/
|
|
4054
4078
|
needsAutoPick = false;
|
|
4055
4079
|
/**
|
|
4080
|
+
* Unsubscribe handle for the deferred-auto-pick `platform-probe` ready
|
|
4081
|
+
* listener (armed in `setApi` when the probe isn't ready yet). Cleared once
|
|
4082
|
+
* the engine is resolved — either by the listener firing or by the boot
|
|
4083
|
+
* safety-net `ensureBootEngineProvisioned`.
|
|
4084
|
+
*/
|
|
4085
|
+
deferredAutoPickUnsub = null;
|
|
4086
|
+
/**
|
|
4056
4087
|
* Warm cache for benchmark engine-override runs.
|
|
4057
4088
|
*
|
|
4058
4089
|
* Each override rebuild costs a full Python pool spin-up (~300-500ms)
|
|
@@ -4084,8 +4115,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4084
4115
|
this.readStore = () => settings.readAddonStore();
|
|
4085
4116
|
this.writeStore = (patch) => settings.writeAddonStore(patch);
|
|
4086
4117
|
this.readDeviceStore = settings.readDeviceStore ?? (async () => ({}));
|
|
4087
|
-
this.currentEngine =
|
|
4088
|
-
this.log.info("Engine
|
|
4118
|
+
this.currentEngine = ONNX_FLOOR;
|
|
4119
|
+
this.log.info("Engine pick pending (placeholder until probe / persisted selection)", { meta: {
|
|
4089
4120
|
runtime: this.currentEngine.runtime,
|
|
4090
4121
|
backend: this.currentEngine.backend,
|
|
4091
4122
|
format: this.currentEngine.format
|
|
@@ -4150,69 +4181,78 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4150
4181
|
isReady() {
|
|
4151
4182
|
return this.ready;
|
|
4152
4183
|
}
|
|
4153
|
-
/** Detect the best inference engine for this platform. */
|
|
4154
|
-
static detectBestEngine() {
|
|
4155
|
-
const platform = process.platform;
|
|
4156
|
-
const arch = process.arch;
|
|
4157
|
-
if (platform === "darwin" && arch === "arm64") return {
|
|
4158
|
-
runtime: "python",
|
|
4159
|
-
backend: "coreml",
|
|
4160
|
-
format: "coreml",
|
|
4161
|
-
device: "all"
|
|
4162
|
-
};
|
|
4163
|
-
if (platform === "darwin") return {
|
|
4164
|
-
runtime: "python",
|
|
4165
|
-
backend: "coreml",
|
|
4166
|
-
format: "coreml",
|
|
4167
|
-
device: "gpu"
|
|
4168
|
-
};
|
|
4169
|
-
try {
|
|
4170
|
-
const { execFileSync } = require("node:child_process");
|
|
4171
|
-
execFileSync("nvidia-smi", ["--query-gpu=name", "--format=csv,noheader"], {
|
|
4172
|
-
timeout: 3e3,
|
|
4173
|
-
stdio: "pipe"
|
|
4174
|
-
});
|
|
4175
|
-
return {
|
|
4176
|
-
runtime: "python",
|
|
4177
|
-
backend: "onnx",
|
|
4178
|
-
format: "onnx",
|
|
4179
|
-
device: "cuda"
|
|
4180
|
-
};
|
|
4181
|
-
} catch {}
|
|
4182
|
-
try {
|
|
4183
|
-
const fsmod = require("node:fs");
|
|
4184
|
-
const isIntel = require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
|
|
4185
|
-
const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
|
|
4186
|
-
const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
|
|
4187
|
-
if (isIntel && (hasIgpu || hasNpu)) return {
|
|
4188
|
-
runtime: "python",
|
|
4189
|
-
backend: "openvino",
|
|
4190
|
-
format: "openvino",
|
|
4191
|
-
device: "auto"
|
|
4192
|
-
};
|
|
4193
|
-
} catch {}
|
|
4194
|
-
return {
|
|
4195
|
-
runtime: "python",
|
|
4196
|
-
backend: "onnx",
|
|
4197
|
-
format: "onnx",
|
|
4198
|
-
device: "cpu"
|
|
4199
|
-
};
|
|
4200
|
-
}
|
|
4201
4184
|
/** Store the addon context. ctx.api is a lazy getter resolved at call time. */
|
|
4202
4185
|
async setApi(addonCtx) {
|
|
4203
4186
|
this.addonCtx = addonCtx;
|
|
4204
|
-
if (this.needsAutoPick) {
|
|
4187
|
+
if (this.needsAutoPick) if (this.addonCtx.useCapability("platform-probe").isReady) {
|
|
4205
4188
|
await this.autoPickAndPersist();
|
|
4206
4189
|
this.needsAutoPick = false;
|
|
4190
|
+
this.startProvisioningForCurrentEngine();
|
|
4191
|
+
} else {
|
|
4192
|
+
const unsubscribe = this.addonCtx.onCapabilityStateChange("platform-probe", { type: "global" }, (state) => {
|
|
4193
|
+
if (state !== "ready") return;
|
|
4194
|
+
this.cancelDeferredAutoPick();
|
|
4195
|
+
if (!this.needsAutoPick) return;
|
|
4196
|
+
this.autoPickAndPersist().then(() => {
|
|
4197
|
+
this.needsAutoPick = false;
|
|
4198
|
+
this.startProvisioningForCurrentEngine();
|
|
4199
|
+
});
|
|
4200
|
+
});
|
|
4201
|
+
this.deferredAutoPickUnsub = unsubscribe;
|
|
4202
|
+
this.addonCtx.addDisposer(() => this.cancelDeferredAutoPick());
|
|
4207
4203
|
}
|
|
4204
|
+
else this.startProvisioningForCurrentEngine();
|
|
4205
|
+
}
|
|
4206
|
+
/** Tear down the deferred-auto-pick probe listener, if still armed. */
|
|
4207
|
+
cancelDeferredAutoPick() {
|
|
4208
|
+
if (this.deferredAutoPickUnsub) {
|
|
4209
|
+
this.deferredAutoPickUnsub();
|
|
4210
|
+
this.deferredAutoPickUnsub = null;
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* Boot safety-net: deterministically provision the hardware-probed engine.
|
|
4215
|
+
*
|
|
4216
|
+
* Called from the addon's `onInitialize` right after `reprobeEngine` has
|
|
4217
|
+
* written this node's probe-driven selection (e.g. `engineBackend=openvino`)
|
|
4218
|
+
* to the store. When first-boot auto-pick was DEFERRED (probe not ready at
|
|
4219
|
+
* `setApi`), the engine would otherwise stay `idle` — selected but never
|
|
4220
|
+
* provisioned — because the `onCapabilityStateChange('platform-probe')` ready
|
|
4221
|
+
* edge can be missed when the cap is already 'ready' by the time we subscribe
|
|
4222
|
+
* (the old eager-onnx-floor masked this; removing it surfaced a node that
|
|
4223
|
+
* boots with no engine at all). This loads the now-persisted selection and
|
|
4224
|
+
* starts provisioning it, so the node reliably comes up on its real best
|
|
4225
|
+
* engine (openvino on Intel) with no onnx floor and no missed boot. No-op
|
|
4226
|
+
* when provisioning already started (persisted-engine path / listener fired)
|
|
4227
|
+
* or when nothing has been selected yet.
|
|
4228
|
+
*/
|
|
4229
|
+
async ensureBootEngineProvisioned() {
|
|
4230
|
+
if (this.getEngineProvisioning().state !== "idle") return;
|
|
4231
|
+
const stored = await this.loadEngine();
|
|
4232
|
+
if (!stored) return;
|
|
4233
|
+
this.currentEngine = stored;
|
|
4234
|
+
this.needsAutoPick = false;
|
|
4235
|
+
this.cancelDeferredAutoPick();
|
|
4236
|
+
this.log.info("Boot engine provisioning from probed selection", { meta: {
|
|
4237
|
+
runtime: stored.runtime,
|
|
4238
|
+
backend: stored.backend,
|
|
4239
|
+
device: stored.device ?? null
|
|
4240
|
+
} });
|
|
4208
4241
|
this.startProvisioningForCurrentEngine();
|
|
4209
4242
|
}
|
|
4210
4243
|
/**
|
|
4211
4244
|
* Auto-pick the best supported runtime at first boot (no stored engine).
|
|
4212
4245
|
* Uses the platform-probe cap's hardware + bestScore hint when available;
|
|
4213
4246
|
* falls back to platform/arch when the probe cap is not yet reachable.
|
|
4247
|
+
*
|
|
4214
4248
|
* Persists the selection as `engineBackend` + `engineDevice` so subsequent
|
|
4215
|
-
* boots load it via `loadEngine()` and skip this path
|
|
4249
|
+
* boots load it via `loadEngine()` and skip this path — but ONLY when the
|
|
4250
|
+
* probe actually answered (real `hardware` or a `bestScore` hint). If the
|
|
4251
|
+
* probe query failed (cold-start race, cap momentarily unreachable), the
|
|
4252
|
+
* pick floors to onnx purely for lack of information; persisting that would
|
|
4253
|
+
* LOCK onnx and skip auto-pick on every future boot even after the
|
|
4254
|
+
* accelerator surfaces. In that case we set the in-memory floor for liveness
|
|
4255
|
+
* but leave the store untouched so the next boot re-attempts the pick.
|
|
4216
4256
|
*/
|
|
4217
4257
|
async autoPickAndPersist() {
|
|
4218
4258
|
let hardware = null;
|
|
@@ -4220,7 +4260,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4220
4260
|
try {
|
|
4221
4261
|
const api = this.addonCtx?.api;
|
|
4222
4262
|
if (api) {
|
|
4223
|
-
const caps = await api.platformProbe.getCapabilities.query();
|
|
4263
|
+
const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
|
|
4224
4264
|
hardware = caps?.hardware ?? null;
|
|
4225
4265
|
const bs = caps?.bestScore;
|
|
4226
4266
|
if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
|
|
@@ -4234,9 +4274,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4234
4274
|
device: pick.device
|
|
4235
4275
|
};
|
|
4236
4276
|
this.currentEngine = engine;
|
|
4277
|
+
if (!(hardware !== null || bestBackendHint !== null)) {
|
|
4278
|
+
this.log.warn("Auto-pick: probe returned no hardware/hint — using onnx floor WITHOUT persisting", { meta: {
|
|
4279
|
+
backend: pick.runtimeId,
|
|
4280
|
+
device: pick.device
|
|
4281
|
+
} });
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
const apNode = this.localProbeNodeId();
|
|
4237
4285
|
await this.writeStore({
|
|
4238
|
-
engineBackend: pick.runtimeId,
|
|
4239
|
-
engineDevice: pick.device
|
|
4286
|
+
[nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
|
|
4287
|
+
[nodeEngineKey("engineDevice", apNode)]: pick.device
|
|
4240
4288
|
});
|
|
4241
4289
|
this.log.info("Auto-picked engine at first boot", { meta: {
|
|
4242
4290
|
backend: pick.runtimeId,
|
|
@@ -4353,7 +4401,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4353
4401
|
if (!step.enabled) continue;
|
|
4354
4402
|
const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
|
|
4355
4403
|
if (!modelEntry) continue;
|
|
4356
|
-
if (
|
|
4404
|
+
if (require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
|
|
4357
4405
|
await this.downloadWithRetry(modelEntry, format, 3);
|
|
4358
4406
|
}
|
|
4359
4407
|
}
|
|
@@ -4409,11 +4457,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4409
4457
|
* callers fall back to the registry's safe minimum. The engine OFFER derives
|
|
4410
4458
|
* from hardware ONLY — install state (probe `scores`) never gates it.
|
|
4411
4459
|
*/
|
|
4460
|
+
/**
|
|
4461
|
+
* The local Moleculer node id (child-suffix stripped). MUST be passed to every
|
|
4462
|
+
* `platformProbe.getCapabilities` query: the cap is a singleton and a query
|
|
4463
|
+
* with no nodeId resolves to the HUB's probe — so on a remote agent the engine
|
|
4464
|
+
* decision would use the HUB's hardware (e.g. an Intel NPU the agent doesn't
|
|
4465
|
+
* have) and pin a device the node can't run, breaking provisioning.
|
|
4466
|
+
*/
|
|
4467
|
+
localProbeNodeId() {
|
|
4468
|
+
const raw = this.addonCtx?.kernel?.localNodeId ?? "hub";
|
|
4469
|
+
return raw.includes("/") ? raw.split("/")[0] : raw;
|
|
4470
|
+
}
|
|
4412
4471
|
async fetchProbeGatingData() {
|
|
4413
4472
|
try {
|
|
4414
4473
|
const api = this.addonCtx?.api;
|
|
4415
4474
|
if (!api) return { hardware: null };
|
|
4416
|
-
return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
|
|
4475
|
+
return { hardware: (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null };
|
|
4417
4476
|
} catch {
|
|
4418
4477
|
return { hardware: null };
|
|
4419
4478
|
}
|
|
@@ -4550,7 +4609,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4550
4609
|
const formats = {};
|
|
4551
4610
|
for (const [formatKey, entry] of Object.entries(m.formats)) {
|
|
4552
4611
|
if (!entry) continue;
|
|
4553
|
-
const downloaded =
|
|
4612
|
+
const downloaded = require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, m, formatKey);
|
|
4554
4613
|
formats[formatKey] = {
|
|
4555
4614
|
url: entry.url,
|
|
4556
4615
|
sizeMB: entry.sizeMB,
|
|
@@ -4673,7 +4732,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4673
4732
|
const { modelId, format, addonId } = input;
|
|
4674
4733
|
const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
|
|
4675
4734
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
|
|
4676
|
-
if (!
|
|
4735
|
+
if (!require_model_download_service_RxAOiYvX.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
|
|
4677
4736
|
this.log.info("Model deleted from disk", { meta: {
|
|
4678
4737
|
modelId,
|
|
4679
4738
|
format
|
|
@@ -5128,7 +5187,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5128
5187
|
const format = this.currentEngine?.format ?? "onnx";
|
|
5129
5188
|
for (const step of needed) {
|
|
5130
5189
|
const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
|
|
5131
|
-
if (modelEntry && !
|
|
5190
|
+
if (modelEntry && !require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, modelEntry, format)) {
|
|
5132
5191
|
this.log.info("Downloading model for step", { meta: {
|
|
5133
5192
|
modelId: step.modelId,
|
|
5134
5193
|
format,
|
|
@@ -5153,7 +5212,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5153
5212
|
/** Download a model with retry + exponential backoff */
|
|
5154
5213
|
async downloadWithRetry(entry, format, maxRetries, onProgress) {
|
|
5155
5214
|
for (let attempt = 1; attempt <= maxRetries; attempt++) try {
|
|
5156
|
-
await
|
|
5215
|
+
await require_model_download_service_RxAOiYvX.ensureModel(this.modelsDir, entry, format, onProgress);
|
|
5157
5216
|
this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
|
|
5158
5217
|
return;
|
|
5159
5218
|
} catch (err) {
|
|
@@ -5478,7 +5537,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5478
5537
|
this.engineFactory = null;
|
|
5479
5538
|
this.executor = null;
|
|
5480
5539
|
}
|
|
5481
|
-
for (const id of
|
|
5540
|
+
for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
|
|
5482
5541
|
}
|
|
5483
5542
|
/**
|
|
5484
5543
|
* Resolve and cache a {@link DeviceProxy} for the given camera. Pins
|
|
@@ -5630,11 +5689,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5630
5689
|
} });
|
|
5631
5690
|
continue;
|
|
5632
5691
|
}
|
|
5633
|
-
if (!
|
|
5692
|
+
if (!require_model_download_service_RxAOiYvX.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
|
|
5634
5693
|
modelId: step.modelId,
|
|
5635
5694
|
format
|
|
5636
5695
|
} });
|
|
5637
|
-
downloads.push(
|
|
5696
|
+
downloads.push(require_model_download_service_RxAOiYvX.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
|
|
5638
5697
|
}
|
|
5639
5698
|
await Promise.all(downloads);
|
|
5640
5699
|
await this.ensureBackendDeps(this.currentEngine);
|
|
@@ -5662,25 +5721,27 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5662
5721
|
* fields (`engineRuntime`, `engineBackend`, `engineDevice`). Fallback
|
|
5663
5722
|
* to the legacy `KEY_ENGINE` JSON blob for pre-migration stores.
|
|
5664
5723
|
* Returns null when neither source has anything; the caller keeps
|
|
5665
|
-
*
|
|
5724
|
+
* the onnx floor set at construction until autoPickAndPersist() runs.
|
|
5666
5725
|
*/
|
|
5667
5726
|
async loadEngine() {
|
|
5668
5727
|
const store = await this.readStore();
|
|
5669
5728
|
const storedRuntime = store["engineRuntime"];
|
|
5670
|
-
const
|
|
5729
|
+
const node = this.localProbeNodeId();
|
|
5730
|
+
const storedBackend = readNodeEngineValue(store, "engineBackend", node);
|
|
5671
5731
|
if (typeof storedBackend === "string" && storedBackend.length > 0) {
|
|
5672
5732
|
const backend = storedBackend;
|
|
5673
|
-
const
|
|
5674
|
-
const
|
|
5733
|
+
const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
|
|
5734
|
+
const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
|
|
5735
|
+
const floor = onnxFloorPick();
|
|
5675
5736
|
const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
|
|
5676
5737
|
if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
|
|
5677
|
-
this.log.warn("Stored engine backend unavailable on this node — falling back to
|
|
5738
|
+
this.log.warn("Stored engine backend unavailable on this node — falling back to onnx floor", { meta: {
|
|
5678
5739
|
stored: migratedBackend,
|
|
5679
|
-
fallback: `${
|
|
5740
|
+
fallback: `${floor.backend}`
|
|
5680
5741
|
} });
|
|
5681
|
-
return
|
|
5742
|
+
return floor;
|
|
5682
5743
|
}
|
|
5683
|
-
const device = storedDevice ||
|
|
5744
|
+
const device = storedDevice || floor.device;
|
|
5684
5745
|
return {
|
|
5685
5746
|
runtime: "python",
|
|
5686
5747
|
backend: migratedBackend,
|
|
@@ -5852,18 +5913,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5852
5913
|
const api = this.addonCtx?.api;
|
|
5853
5914
|
let best;
|
|
5854
5915
|
if (api) try {
|
|
5855
|
-
const caps = await api.platformProbe.getCapabilities.query();
|
|
5916
|
+
const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
|
|
5856
5917
|
const bs = caps?.bestScore;
|
|
5857
5918
|
if (bs && bs.runtime === "python") {
|
|
5858
5919
|
const probeBackend = bs.backend;
|
|
5859
5920
|
const probeDevice = (() => {
|
|
5860
5921
|
const hw = caps.hardware;
|
|
5861
|
-
if (probeBackend === "
|
|
5862
|
-
if (probeBackend === "
|
|
5863
|
-
if (hw?.npu?.type === "intel-npu") return "npu";
|
|
5864
|
-
if (hw?.gpu?.type === "intel") return "gpu";
|
|
5865
|
-
return "auto";
|
|
5866
|
-
}
|
|
5922
|
+
if (probeBackend === "openvino") return defaultDeviceFor("openvino");
|
|
5923
|
+
if (probeBackend === "coreml") return defaultDeviceFor("coreml");
|
|
5867
5924
|
if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
|
|
5868
5925
|
return "cpu";
|
|
5869
5926
|
})();
|
|
@@ -5873,16 +5930,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5873
5930
|
format: backendToFormat(probeBackend),
|
|
5874
5931
|
device: probeDevice
|
|
5875
5932
|
};
|
|
5876
|
-
} else best =
|
|
5933
|
+
} else best = onnxFloorPick();
|
|
5877
5934
|
} catch {
|
|
5878
|
-
best =
|
|
5935
|
+
best = onnxFloorPick();
|
|
5879
5936
|
}
|
|
5880
|
-
else best =
|
|
5937
|
+
else best = onnxFloorPick();
|
|
5881
5938
|
const probedLabel = `${best.backend}/${best.device ?? "default"}`;
|
|
5939
|
+
const rpNode = this.localProbeNodeId();
|
|
5882
5940
|
await this.writeStore({
|
|
5883
|
-
probedBestEngine: probedLabel,
|
|
5884
|
-
engineBackend: best.backend,
|
|
5885
|
-
engineDevice: best.device ?? "cpu"
|
|
5941
|
+
[nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
|
|
5942
|
+
[nodeEngineKey("engineBackend", rpNode)]: best.backend,
|
|
5943
|
+
[nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
|
|
5886
5944
|
});
|
|
5887
5945
|
this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
|
|
5888
5946
|
backend: best.backend,
|
|
@@ -6046,7 +6104,7 @@ function buildSchemaSlots(format, modelsDir) {
|
|
|
6046
6104
|
id: m.id,
|
|
6047
6105
|
name: m.name,
|
|
6048
6106
|
formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
|
|
6049
|
-
downloaded:
|
|
6107
|
+
downloaded: require_model_download_service_RxAOiYvX.isModelDownloaded(modelsDir, m, f),
|
|
6050
6108
|
sizeMB: entry.sizeMB
|
|
6051
6109
|
}]))
|
|
6052
6110
|
})),
|
|
@@ -6305,7 +6363,8 @@ var DEFAULT_CONFIG = {
|
|
|
6305
6363
|
engineRuntime: "python",
|
|
6306
6364
|
engineBackend: "onnx",
|
|
6307
6365
|
engineDevice: "cpu",
|
|
6308
|
-
probedBestEngine: ""
|
|
6366
|
+
probedBestEngine: "",
|
|
6367
|
+
activeEngine: ""
|
|
6309
6368
|
};
|
|
6310
6369
|
/** Derive the model-format from a backend value. Called by the provider. */
|
|
6311
6370
|
function backendToFormat(backend) {
|
|
@@ -6348,6 +6407,15 @@ var POOL_BOUND_KEYS = [
|
|
|
6348
6407
|
];
|
|
6349
6408
|
var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
6350
6409
|
provider = null;
|
|
6410
|
+
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
6411
|
+
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
6412
|
+
* node because the hub addon probes other nodes when serving their config. */
|
|
6413
|
+
lastGoodHardwareByNode = /* @__PURE__ */ new Map();
|
|
6414
|
+
/** This node's effective engine selection, cached from the node-scoped store
|
|
6415
|
+
* so the synchronous `resolveBackendTuning` and the reprobe gate don't read
|
|
6416
|
+
* the cluster-shared bare keys. Refreshed at init + on every config change. */
|
|
6417
|
+
nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
|
|
6418
|
+
nodeProbedBestEngine = "";
|
|
6351
6419
|
engineMetricsTimer = null;
|
|
6352
6420
|
/** Snapshot-equality cache for engine-metrics emit. Most ticks
|
|
6353
6421
|
* the engine inventory is unchanged (no model load/unload), so
|
|
@@ -6402,6 +6470,14 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6402
6470
|
tooltip: "Re-probe engine"
|
|
6403
6471
|
}]
|
|
6404
6472
|
}),
|
|
6473
|
+
this.field({
|
|
6474
|
+
type: "text",
|
|
6475
|
+
key: "activeEngine",
|
|
6476
|
+
label: "Active engine",
|
|
6477
|
+
description: "The runtime/device the inference pool ACTUALLY loaded on this node, with its provisioning state (format: provider/device (state)). If this differs from the selected provider below — e.g. \"onnx/cpu (ready)\" while OpenVINO is selected — the chosen accelerator could not load on this host and the engine fell back to the ONNX-CPU baseline.",
|
|
6478
|
+
readonlyField: true,
|
|
6479
|
+
default: ""
|
|
6480
|
+
}),
|
|
6405
6481
|
this.field({
|
|
6406
6482
|
type: "select",
|
|
6407
6483
|
key: "engineBackend",
|
|
@@ -6538,14 +6614,18 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6538
6614
|
* probe is unreachable). Stored backend / device snap back to the registry
|
|
6539
6615
|
* floor / default when they fall outside the offered set.
|
|
6540
6616
|
*/
|
|
6541
|
-
async getGlobalSettings(overlay) {
|
|
6542
|
-
const
|
|
6543
|
-
const
|
|
6617
|
+
async getGlobalSettings(overlay, _cap, nodeId) {
|
|
6618
|
+
const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
|
|
6619
|
+
const rawStored = await this.resolveUiSettingsStore();
|
|
6620
|
+
if (rawStored === null) return null;
|
|
6621
|
+
const stored = projectNodeEngine(rawStored, targetNode);
|
|
6622
|
+
const storedBackendRaw = stored["engineBackend"];
|
|
6623
|
+
if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
|
|
6544
6624
|
const merged = overlay ? {
|
|
6545
6625
|
...stored,
|
|
6546
6626
|
...overlay
|
|
6547
6627
|
} : stored;
|
|
6548
|
-
const env = await this.probeHardwareEnv();
|
|
6628
|
+
const env = await this.probeHardwareEnv(targetNode);
|
|
6549
6629
|
const hardware = env.hardware;
|
|
6550
6630
|
const offered = supportedRuntimes(env);
|
|
6551
6631
|
const runtimeBackends = offered.map((id) => ({
|
|
@@ -6553,14 +6633,22 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6553
6633
|
label: runtimeLabel(id)
|
|
6554
6634
|
}));
|
|
6555
6635
|
const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
|
|
6556
|
-
const backend =
|
|
6636
|
+
const backend = (() => {
|
|
6637
|
+
const rid = toRuntimeId(storedBackend);
|
|
6638
|
+
if (rid === "onnx") return "onnx";
|
|
6639
|
+
if (offered.includes(rid)) return rid;
|
|
6640
|
+
return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
|
|
6641
|
+
})();
|
|
6557
6642
|
const deviceOptions = runtimeDevices(backend, hardware);
|
|
6558
6643
|
const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
|
|
6559
6644
|
const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
|
|
6645
|
+
const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
|
|
6646
|
+
const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
|
|
6560
6647
|
const raw = {
|
|
6561
6648
|
...merged,
|
|
6562
6649
|
engineBackend: backend,
|
|
6563
|
-
engineDevice: device
|
|
6650
|
+
engineDevice: device,
|
|
6651
|
+
activeEngine
|
|
6564
6652
|
};
|
|
6565
6653
|
const schema = this.globalSettingsSchema();
|
|
6566
6654
|
if (!schema) return { sections: [] };
|
|
@@ -6591,15 +6679,15 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6591
6679
|
if (field.type === "slider" && "key" in field) {
|
|
6592
6680
|
const tuned = tuning[field.key];
|
|
6593
6681
|
const sliderField = field;
|
|
6594
|
-
let
|
|
6682
|
+
let patchedField = typeof tuned === "number" ? {
|
|
6595
6683
|
...sliderField,
|
|
6596
6684
|
default: tuned
|
|
6597
6685
|
} : sliderField;
|
|
6598
|
-
if (sliderField.key === "concurrency" && backend === "coreml")
|
|
6599
|
-
...
|
|
6686
|
+
if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
|
|
6687
|
+
...patchedField,
|
|
6600
6688
|
max: 4
|
|
6601
6689
|
};
|
|
6602
|
-
return
|
|
6690
|
+
return patchedField;
|
|
6603
6691
|
}
|
|
6604
6692
|
return field;
|
|
6605
6693
|
})
|
|
@@ -6616,23 +6704,41 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6616
6704
|
* Protected seam — overridable by tests (canned hardware) and reused by the
|
|
6617
6705
|
* Phase 2 auto-pick path. NEVER reads install state.
|
|
6618
6706
|
*/
|
|
6619
|
-
|
|
6620
|
-
|
|
6707
|
+
/**
|
|
6708
|
+
* Resolve the persisted UI settings store, or `null` when it can't be read
|
|
6709
|
+
* right now (ctx/settings not wired — mid-restart). A readable-but-empty
|
|
6710
|
+
* store (genuine first boot) returns `{}`, never null. `getGlobalSettings`
|
|
6711
|
+
* uses the null signal to avoid serving fabricated empty-store defaults
|
|
6712
|
+
* during the restart window. Seam kept protected so tests can drive the
|
|
6713
|
+
* unreadable vs empty vs populated cases without faking a full AddonContext.
|
|
6714
|
+
*/
|
|
6715
|
+
async resolveUiSettingsStore() {
|
|
6716
|
+
const settings = this.ctxIfReady?.settings;
|
|
6717
|
+
if (!settings) return null;
|
|
6718
|
+
return await settings.readAddonStore() ?? {};
|
|
6719
|
+
}
|
|
6720
|
+
async probeHardwareEnv(nodeId) {
|
|
6721
|
+
const node = nodeId ?? this.localNodeId();
|
|
6722
|
+
const hardware = await this.resolveProbeHardware(node);
|
|
6723
|
+
if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
|
|
6724
|
+
const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
|
|
6621
6725
|
return {
|
|
6622
6726
|
platform: process.platform,
|
|
6623
6727
|
arch: process.arch,
|
|
6624
|
-
hardware
|
|
6728
|
+
hardware: effective
|
|
6625
6729
|
};
|
|
6626
6730
|
}
|
|
6627
6731
|
/**
|
|
6628
|
-
* Fetch the probed hardware from the platform-probe cap
|
|
6629
|
-
* the cap is not reachable (caller falls back to
|
|
6732
|
+
* Fetch the probed hardware from the platform-probe cap for `nodeId` (default
|
|
6733
|
+
* = self). Returns null when the cap is not reachable (caller falls back to
|
|
6734
|
+
* the registry's safe minimum).
|
|
6630
6735
|
*/
|
|
6631
|
-
async resolveProbeHardware() {
|
|
6736
|
+
async resolveProbeHardware(nodeId) {
|
|
6632
6737
|
try {
|
|
6633
6738
|
const api = this.ctxIfReady?.api;
|
|
6634
6739
|
if (!api) return null;
|
|
6635
|
-
const
|
|
6740
|
+
const node = nodeId ?? this.localNodeId();
|
|
6741
|
+
const hw = (node === this.localNodeId() ? await api.platformProbe.getCapabilities.query() : await api.platformProbe.getCapabilities.query(void 0, require_dist.nodePin(node)))?.hardware;
|
|
6636
6742
|
if (!hw) return null;
|
|
6637
6743
|
return {
|
|
6638
6744
|
npu: hw.npu ? { type: hw.npu.type } : null,
|
|
@@ -6643,6 +6749,61 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6643
6749
|
}
|
|
6644
6750
|
}
|
|
6645
6751
|
/**
|
|
6752
|
+
* Bare node id used to scope the engine cascade in the shared store. MUST
|
|
6753
|
+
* match the provider's `localProbeNodeId()` (kernel.localNodeId, default
|
|
6754
|
+
* 'hub') so the UI write path (this) and the provider read/write path
|
|
6755
|
+
* (loadEngine / autoPick / reprobe) target the SAME `<key>@<nodeId>`. The old
|
|
6756
|
+
* `?? this.ctx.id` fallback resolved to the ADDON id ('detection-pipeline')
|
|
6757
|
+
* when kernel.localNodeId was absent, so UI saves landed on a key the
|
|
6758
|
+
* provider never read — silently losing the per-node selection.
|
|
6759
|
+
*/
|
|
6760
|
+
localNodeId() {
|
|
6761
|
+
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
6762
|
+
}
|
|
6763
|
+
/**
|
|
6764
|
+
* Refresh this node's cached engine selection from the node-scoped store.
|
|
6765
|
+
* `resolveBackendTuning` is synchronous and the reprobe gate runs before the
|
|
6766
|
+
* provider exists, so both read these cached fields instead of the
|
|
6767
|
+
* cluster-shared bare keys (which belong to no single node). Best-effort: a
|
|
6768
|
+
* transiently-unreadable store leaves the last cached values in place.
|
|
6769
|
+
*/
|
|
6770
|
+
async refreshNodeEngineFromStore() {
|
|
6771
|
+
const store = await this.resolveUiSettingsStore();
|
|
6772
|
+
if (store === null) return;
|
|
6773
|
+
const node = this.localNodeId();
|
|
6774
|
+
const backend = readNodeEngineValue(store, "engineBackend", node);
|
|
6775
|
+
if (typeof backend === "string" && backend !== "") this.nodeEngineBackend = backend;
|
|
6776
|
+
const probed = readNodeEngineValue(store, "probedBestEngine", node);
|
|
6777
|
+
this.nodeProbedBestEngine = typeof probed === "string" ? probed : "";
|
|
6778
|
+
}
|
|
6779
|
+
/**
|
|
6780
|
+
* Persist a settings patch, mirroring the engine cascade fields to the TARGET
|
|
6781
|
+
* node's scoped keys so each node keeps an INDEPENDENT engine selection in the
|
|
6782
|
+
* cluster-central store. The hub addon serves writes for every node, so it
|
|
6783
|
+
* scopes by the requested `nodeId` (default self).
|
|
6784
|
+
*
|
|
6785
|
+
* When the target IS this node, `super.updateGlobalSettings` drives the normal
|
|
6786
|
+
* apply path (`resolveConfig` / `onConfigChanged` / `requiresRestart` restart),
|
|
6787
|
+
* and the bare engine keys it writes are shadowed by the node-scoped keys on
|
|
6788
|
+
* read. When the target is a SIBLING node, we persist the scoped engine keys +
|
|
6789
|
+
* the non-engine bare keys but DON'T run this node's restart/reprovision — the
|
|
6790
|
+
* owning node applies its own engine selection on its next (re)start.
|
|
6791
|
+
*/
|
|
6792
|
+
async updateGlobalSettings(patch, nodeId) {
|
|
6793
|
+
const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
|
|
6794
|
+
const patchRecord = patch;
|
|
6795
|
+
const scopedEngine = {};
|
|
6796
|
+
for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
|
|
6797
|
+
if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
|
|
6798
|
+
if (targetNode === this.localNodeId()) {
|
|
6799
|
+
await super.updateGlobalSettings(patch, nodeId);
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
6802
|
+
const sharedPatch = {};
|
|
6803
|
+
for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
|
|
6804
|
+
if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
|
|
6805
|
+
}
|
|
6806
|
+
/**
|
|
6646
6807
|
* Resolve the effective pool tuning for the configured backend.
|
|
6647
6808
|
*
|
|
6648
6809
|
* Reads the registry's `tuningFor(backend)` and ignores any persisted
|
|
@@ -6656,7 +6817,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6656
6817
|
* a reason to disagree.
|
|
6657
6818
|
*/
|
|
6658
6819
|
resolveBackendTuning() {
|
|
6659
|
-
const t = tuningFor(toRuntimeId(this.
|
|
6820
|
+
const t = tuningFor(toRuntimeId(this.nodeEngineBackend ?? DEFAULT_CONFIG.engineBackend));
|
|
6660
6821
|
const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
|
|
6661
6822
|
const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
|
|
6662
6823
|
return {
|
|
@@ -6675,6 +6836,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6675
6836
|
relativePath: ""
|
|
6676
6837
|
}).catch(() => "camstack-data/models");
|
|
6677
6838
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
6839
|
+
await this.refreshNodeEngineFromStore();
|
|
6678
6840
|
this.pythonAddonDir = resolveAddonPythonDir();
|
|
6679
6841
|
const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
|
|
6680
6842
|
if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
|
|
@@ -6696,9 +6858,12 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6696
6858
|
});
|
|
6697
6859
|
await this.provider.init();
|
|
6698
6860
|
await this.provider.setApi(this.ctx);
|
|
6699
|
-
if (!this.
|
|
6861
|
+
if (!this.nodeProbedBestEngine) await this.provider.reprobeEngine().catch((err) => {
|
|
6700
6862
|
this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6701
6863
|
});
|
|
6864
|
+
await this.provider.ensureBootEngineProvisioned().catch((err) => {
|
|
6865
|
+
this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6866
|
+
});
|
|
6702
6867
|
await this.provider.warmPool();
|
|
6703
6868
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
6704
6869
|
this.lastAppliedPoolConfig = this.snapshotPoolConfig();
|
|
@@ -6748,8 +6913,8 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6748
6913
|
/**
|
|
6749
6914
|
* Proactively install the OpenVINO Python package when Intel hardware
|
|
6750
6915
|
* (iGPU or NPU) is detected. Called once from `onInitialize`, before the
|
|
6751
|
-
* provider is constructed, so the module is
|
|
6752
|
-
*
|
|
6916
|
+
* provider is constructed, so the module is RUNNABLE when the engine is
|
|
6917
|
+
* first loaded (gated by `loadEngine`'s `isPythonBackendAvailable` check).
|
|
6753
6918
|
*
|
|
6754
6919
|
* Failure is non-fatal: a warning is logged and the addon continues with
|
|
6755
6920
|
* the onnx-cpu baseline. The hardware query itself is also best-effort —
|
|
@@ -6818,6 +6983,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6818
6983
|
* lifecycle (engineFactory rebuild on next runPipeline).
|
|
6819
6984
|
*/
|
|
6820
6985
|
async onConfigChanged() {
|
|
6986
|
+
await this.refreshNodeEngineFromStore();
|
|
6821
6987
|
if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
|
|
6822
6988
|
this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6823
6989
|
});
|