@camstack/addon-pipeline 1.1.0 → 1.1.2
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 +104 -29
- package/dist/audio-analyzer/index.mjs +100 -25
- package/dist/audio-codec-nodeav/index.js +1 -1
- package/dist/audio-codec-nodeav/index.mjs +1 -1
- package/dist/decoder-nodeav/index.js +1 -1
- package/dist/decoder-nodeav/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +355 -116
- package/dist/detection-pipeline/index.mjs +343 -104
- package/dist/{dist-BA6DR_jV.mjs → dist-BWc-HYQz.mjs} +194 -1
- package/dist/{dist-BLcTVvol.js → dist-DnD2tm7T.js} +194 -1
- package/dist/{model-download-service-RxAOiYvX-CMAvhgO7.mjs → model-download-service-C-IHWnXx-3Mmeob3l.mjs} +1 -1
- package/dist/{model-download-service-RxAOiYvX-C8rTRJy_.js → model-download-service-C-IHWnXx-BnQ_awK4.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +14 -10
- package/dist/pipeline-runner/index.mjs +14 -10
- package/dist/recorder/index.js +4 -4
- package/dist/recorder/index.mjs +2 -2
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DrohyZ5L.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DLgk22-S.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-FRD2eBuz.mjs +26 -0
- package/dist/stream-broker/{hostInit-zLZbYJcg.mjs → hostInit-pRSjUAJj.mjs} +3 -3
- package/dist/stream-broker/index.js +8 -8
- package/dist/stream-broker/index.mjs +2 -2
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DJ7ztnFv.js → MaskShapeCanvas-DI4BY7W2-5UPreLSr.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CQzEnQoq.js → MotionZonesSettings-NcxxQN8r-Bxqs-CpZ.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-Cl0eOy_U.js → PrivacyMaskSettings-APgPLF7p-BDMPeMJd.js} +1 -1
- package/embed-dist/assets/{index-CSuLwWK-.js → index-BgGwqHYl.js} +9 -9
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/postprocessors/saliency.py +47 -1
- package/python/postprocessors/test_saliency.py +23 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-COa17XL2.mjs +0 -26
|
@@ -2,17 +2,17 @@ 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_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
|
|
6
|
+
const require_dist = require("../dist-DnD2tm7T.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
|
-
node_fs =
|
|
8
|
+
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
9
9
|
let node_path = require("node:path");
|
|
10
|
-
node_path =
|
|
10
|
+
node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
|
|
11
11
|
let node_os = require("node:os");
|
|
12
|
-
node_os =
|
|
12
|
+
node_os = require_model_download_service_C_IHWnXx.__toESM(node_os);
|
|
13
13
|
let node_child_process = require("node:child_process");
|
|
14
14
|
let sharp = require("sharp");
|
|
15
|
-
sharp =
|
|
15
|
+
sharp = require_model_download_service_C_IHWnXx.__toESM(sharp);
|
|
16
16
|
//#region src/detection-pipeline/engine-store-keys.ts
|
|
17
17
|
/**
|
|
18
18
|
* Per-node scoping for the detection-pipeline engine cascade.
|
|
@@ -121,7 +121,6 @@ function npuInfoFrom(hw) {
|
|
|
121
121
|
return { type };
|
|
122
122
|
}
|
|
123
123
|
function envToHardwareInfo(env) {
|
|
124
|
-
if (!env.hardware) return null;
|
|
125
124
|
return {
|
|
126
125
|
platform: toKnownPlatform(env.platform),
|
|
127
126
|
arch: toKnownArch(env.arch),
|
|
@@ -129,8 +128,8 @@ function envToHardwareInfo(env) {
|
|
|
129
128
|
cpuCores: 0,
|
|
130
129
|
totalRAM_MB: 0,
|
|
131
130
|
availableRAM_MB: 0,
|
|
132
|
-
gpu: gpuInfoFrom(env.hardware),
|
|
133
|
-
npu: npuInfoFrom(env.hardware)
|
|
131
|
+
gpu: env.hardware ? gpuInfoFrom(env.hardware) : null,
|
|
132
|
+
npu: env.hardware ? npuInfoFrom(env.hardware) : null
|
|
134
133
|
};
|
|
135
134
|
}
|
|
136
135
|
function probedToHardwareInfo(hw) {
|
|
@@ -1043,6 +1042,26 @@ var ovFormat = (url, sizeMB) => {
|
|
|
1043
1042
|
...files ? { files } : {}
|
|
1044
1043
|
};
|
|
1045
1044
|
};
|
|
1045
|
+
/**
|
|
1046
|
+
* Build a precision-variant catalog entry (OpenVINO-only) derived from a base
|
|
1047
|
+
* detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
|
|
1048
|
+
* NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
|
|
1049
|
+
* accuracy cost). The IRs live next to the base `.xml` on HF as
|
|
1050
|
+
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
1051
|
+
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
1052
|
+
*/
|
|
1053
|
+
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
|
|
1054
|
+
id: `${baseId}-${precision}`,
|
|
1055
|
+
name: `${baseName} (${precision.toUpperCase()})`,
|
|
1056
|
+
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
1057
|
+
inputSize: {
|
|
1058
|
+
width: 640,
|
|
1059
|
+
height: 640
|
|
1060
|
+
},
|
|
1061
|
+
labels: [],
|
|
1062
|
+
preprocessMode: "letterbox",
|
|
1063
|
+
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) }
|
|
1064
|
+
});
|
|
1046
1065
|
var MLPACKAGE_FILES = [
|
|
1047
1066
|
"Manifest.json",
|
|
1048
1067
|
"Data/com.apple.CoreML/model.mlmodel",
|
|
@@ -1348,7 +1367,21 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1348
1367
|
},
|
|
1349
1368
|
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1350
1369
|
}
|
|
1351
|
-
}
|
|
1370
|
+
},
|
|
1371
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5),
|
|
1372
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3),
|
|
1373
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15),
|
|
1374
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8),
|
|
1375
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5),
|
|
1376
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
1377
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19),
|
|
1378
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
1379
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41),
|
|
1380
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
1381
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50),
|
|
1382
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
1383
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112),
|
|
1384
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56)
|
|
1352
1385
|
];
|
|
1353
1386
|
var FACE_DETECTION_MODELS = [{
|
|
1354
1387
|
id: "scrfd-2.5g",
|
|
@@ -1795,7 +1828,7 @@ var ObjectDetectionStep = class {
|
|
|
1795
1828
|
"animal"
|
|
1796
1829
|
],
|
|
1797
1830
|
models: [...OBJECT_DETECTION_MODELS],
|
|
1798
|
-
defaultModelId: "
|
|
1831
|
+
defaultModelId: "yolo26n",
|
|
1799
1832
|
defaultConfidence: .5,
|
|
1800
1833
|
labels: require_dist.COCO_80_LABELS.map((l) => l.id),
|
|
1801
1834
|
classMap: require_dist.COCO_TO_MACRO
|
|
@@ -2044,9 +2077,9 @@ var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
|
2044
2077
|
enabledByDefault: false,
|
|
2045
2078
|
defaultConfidence: .3
|
|
2046
2079
|
});
|
|
2047
|
-
var
|
|
2048
|
-
id: "segmentation
|
|
2049
|
-
name: "
|
|
2080
|
+
var STEP_SEGMENTATION = new PipelineStepBase({
|
|
2081
|
+
id: "segmentation",
|
|
2082
|
+
name: "Segmentation",
|
|
2050
2083
|
slot: "refiner",
|
|
2051
2084
|
postprocessor: "saliency",
|
|
2052
2085
|
extractMode: "crop-roi",
|
|
@@ -2058,7 +2091,7 @@ var STEP_SEGMENTATION_REFINER = new PipelineStepBase({
|
|
|
2058
2091
|
defaultConfidence: 0,
|
|
2059
2092
|
group: "Segmentation"
|
|
2060
2093
|
});
|
|
2061
|
-
|
|
2094
|
+
new PipelineStepBase({
|
|
2062
2095
|
id: "instance-segmentation",
|
|
2063
2096
|
name: "Instance Segmentation",
|
|
2064
2097
|
slot: "refiner",
|
|
@@ -2085,8 +2118,7 @@ var ALL_PIPELINE_STEPS = [
|
|
|
2085
2118
|
new AnimalClassifierStep(),
|
|
2086
2119
|
STEP_BIRD_CLASSIFIER,
|
|
2087
2120
|
STEP_VEHICLE_CLASSIFIER,
|
|
2088
|
-
|
|
2089
|
-
STEP_INSTANCE_SEGMENTATION,
|
|
2121
|
+
STEP_SEGMENTATION,
|
|
2090
2122
|
STEP_AUDIO_CLASSIFIER_INSTANCE
|
|
2091
2123
|
];
|
|
2092
2124
|
/** Compat: flat array of StepDefinition for existing consumers */
|
|
@@ -3257,6 +3289,18 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
|
|
|
3257
3289
|
parent.mask = output.mask;
|
|
3258
3290
|
parent.maskWidth = output.maskWidth;
|
|
3259
3291
|
parent.maskHeight = output.maskHeight;
|
|
3292
|
+
if (output.maskBbox !== void 0 && output.maskWidth > 0 && output.maskHeight > 0) {
|
|
3293
|
+
const [px1, py1, px2, py2] = parent.bbox;
|
|
3294
|
+
const parentW = px2 - px1;
|
|
3295
|
+
const parentH = py2 - py1;
|
|
3296
|
+
const [mbx, mby, mbw, mbh] = output.maskBbox;
|
|
3297
|
+
parent.refinedBbox = [
|
|
3298
|
+
px1 + mbx / output.maskWidth * parentW,
|
|
3299
|
+
py1 + mby / output.maskHeight * parentH,
|
|
3300
|
+
px1 + (mbx + mbw) / output.maskWidth * parentW,
|
|
3301
|
+
py1 + (mby + mbh) / output.maskHeight * parentH
|
|
3302
|
+
];
|
|
3303
|
+
}
|
|
3260
3304
|
break;
|
|
3261
3305
|
}
|
|
3262
3306
|
}
|
|
@@ -3307,6 +3351,12 @@ function buildFrameResult(input) {
|
|
|
3307
3351
|
macroClass: m.macroClass,
|
|
3308
3352
|
score: m.score,
|
|
3309
3353
|
bbox,
|
|
3354
|
+
...m.refinedBbox !== void 0 ? { refinedBbox: bboxTupleToRect(m.refinedBbox) } : {},
|
|
3355
|
+
...m.mask !== void 0 && m.maskWidth !== void 0 && m.maskHeight !== void 0 ? {
|
|
3356
|
+
mask: m.mask,
|
|
3357
|
+
maskWidth: m.maskWidth,
|
|
3358
|
+
maskHeight: m.maskHeight
|
|
3359
|
+
} : {},
|
|
3310
3360
|
labels,
|
|
3311
3361
|
...m.parentId !== void 0 ? { parentId: m.parentId } : {},
|
|
3312
3362
|
...embedding !== void 0 ? {
|
|
@@ -3797,6 +3847,40 @@ function walkFieldsForDefaults(fields, out) {
|
|
|
3797
3847
|
}
|
|
3798
3848
|
}
|
|
3799
3849
|
//#endregion
|
|
3850
|
+
//#region src/detection-pipeline/registry/custom-models.ts
|
|
3851
|
+
/**
|
|
3852
|
+
* Group a flat list of custom-model descriptors (as returned by the
|
|
3853
|
+
* `custom-model-registry` collection cap) into a `stepId → entries` map for
|
|
3854
|
+
* the picker / resolution union. Pure; order within a step preserved.
|
|
3855
|
+
*/
|
|
3856
|
+
function groupCustomModelsByStep(descriptors) {
|
|
3857
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
3858
|
+
for (const d of descriptors) {
|
|
3859
|
+
const arr = byStep.get(d.stepId) ?? [];
|
|
3860
|
+
arr.push(d.entry);
|
|
3861
|
+
byStep.set(d.stepId, arr);
|
|
3862
|
+
}
|
|
3863
|
+
return byStep;
|
|
3864
|
+
}
|
|
3865
|
+
/**
|
|
3866
|
+
* Union a step's static catalog models with operator-registered custom
|
|
3867
|
+
* models. On an `id` collision the static catalog entry wins — a custom
|
|
3868
|
+
* model can never shadow a built-in one.
|
|
3869
|
+
*
|
|
3870
|
+
* Pure + side-effect-free so it can be unit-tested in isolation and called
|
|
3871
|
+
* from the (free) `buildSchemaSlots` builder without any addon context.
|
|
3872
|
+
*/
|
|
3873
|
+
function mergeCustomModels(staticModels, customModels) {
|
|
3874
|
+
const seen = new Set(staticModels.map((m) => m.id));
|
|
3875
|
+
const merged = [...staticModels];
|
|
3876
|
+
for (const m of customModels) {
|
|
3877
|
+
if (seen.has(m.id)) continue;
|
|
3878
|
+
seen.add(m.id);
|
|
3879
|
+
merged.push(m);
|
|
3880
|
+
}
|
|
3881
|
+
return merged;
|
|
3882
|
+
}
|
|
3883
|
+
//#endregion
|
|
3800
3884
|
//#region src/detection-pipeline/provider.ts
|
|
3801
3885
|
/**
|
|
3802
3886
|
* DetectionPipelineProvider — implements IPipelineExecutorProvider.
|
|
@@ -4011,6 +4095,17 @@ var ONNX_FLOOR = {
|
|
|
4011
4095
|
* instead of duplicated inline.
|
|
4012
4096
|
*/
|
|
4013
4097
|
function onnxFloorPick() {
|
|
4098
|
+
return ONNX_FLOOR;
|
|
4099
|
+
}
|
|
4100
|
+
/**
|
|
4101
|
+
* The platform-deterministic engine pick computed SYNCHRONOUSLY from this node's
|
|
4102
|
+
* own `process.platform`/`arch` alone (no probe): darwin → coreml, else → onnx
|
|
4103
|
+
* (gpu-dependent openvino/cuda need the probe and converge via the auto-pick).
|
|
4104
|
+
* Used as the ENGINE fallback when a persisted selection is unsupported on this
|
|
4105
|
+
* node — so a stale GLOBAL engine config (e.g. the cluster's OpenVINO choice)
|
|
4106
|
+
* can never force an impossible engine onto a node whose platform rejects it.
|
|
4107
|
+
*/
|
|
4108
|
+
function platformDefaultPick() {
|
|
4014
4109
|
const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
|
|
4015
4110
|
return {
|
|
4016
4111
|
runtime: "python",
|
|
@@ -4019,6 +4114,19 @@ function onnxFloorPick() {
|
|
|
4019
4114
|
device: pick.device
|
|
4020
4115
|
};
|
|
4021
4116
|
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Is `backend` even POSSIBLE on this node's OS/arch (ignoring gpu detail)?
|
|
4119
|
+
* coreml ⇒ darwin only; openvino ⇒ x64 non-darwin only; onnx ⇒ anywhere. Used to
|
|
4120
|
+
* reject a persisted/global engine choice that the node's PLATFORM fundamentally
|
|
4121
|
+
* cannot run (e.g. the cluster's OpenVINO default landing on a Mac) — distinct
|
|
4122
|
+
* from the gpu-dependent support (linux without a probed Intel iGPU still keeps
|
|
4123
|
+
* openvino as a valid platform choice; the device falls back to cpu).
|
|
4124
|
+
*/
|
|
4125
|
+
function backendPossibleOnPlatform(backend) {
|
|
4126
|
+
if (backend === "coreml") return process.platform === "darwin";
|
|
4127
|
+
if (backend === "openvino") return process.arch === "x64" && process.platform !== "darwin";
|
|
4128
|
+
return true;
|
|
4129
|
+
}
|
|
4022
4130
|
var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
4023
4131
|
modelsDir;
|
|
4024
4132
|
eventBus;
|
|
@@ -4032,6 +4140,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4032
4140
|
/** Addon context — ctx.api resolves lazily (direct caller created after boot) */
|
|
4033
4141
|
addonCtx = null;
|
|
4034
4142
|
/**
|
|
4143
|
+
* Short-lived cache of custom models pulled from the `custom-model-registry`
|
|
4144
|
+
* collection cap, grouped by step id. TTL-bounded so the picker / resolution
|
|
4145
|
+
* paths don't issue a cap round-trip on every call. Empty map = no provider
|
|
4146
|
+
* (or a query failure) → behaviour identical to the static-catalog-only path.
|
|
4147
|
+
*/
|
|
4148
|
+
customModelsCache = null;
|
|
4149
|
+
/**
|
|
4035
4150
|
* Per-device {@link DeviceProxy} cache used for zone gating at the
|
|
4036
4151
|
* runtime path. Reads `state.zones.value` + `state.zoneRules.value`
|
|
4037
4152
|
* synchronously per frame so detections inside an `exclude` zone
|
|
@@ -4256,17 +4371,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4256
4371
|
*/
|
|
4257
4372
|
async autoPickAndPersist() {
|
|
4258
4373
|
let hardware = null;
|
|
4259
|
-
let bestBackendHint = null;
|
|
4260
4374
|
try {
|
|
4261
4375
|
const api = this.addonCtx?.api;
|
|
4262
|
-
if (api) {
|
|
4263
|
-
const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
|
|
4264
|
-
hardware = caps?.hardware ?? null;
|
|
4265
|
-
const bs = caps?.bestScore;
|
|
4266
|
-
if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
|
|
4267
|
-
}
|
|
4376
|
+
if (api) hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
|
|
4268
4377
|
} catch {}
|
|
4269
|
-
const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)),
|
|
4378
|
+
const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
|
|
4270
4379
|
const engine = {
|
|
4271
4380
|
runtime: "python",
|
|
4272
4381
|
backend: pick.runtimeId,
|
|
@@ -4274,8 +4383,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4274
4383
|
device: pick.device
|
|
4275
4384
|
};
|
|
4276
4385
|
this.currentEngine = engine;
|
|
4277
|
-
if (!(
|
|
4278
|
-
this.log.
|
|
4386
|
+
if (!(pick.runtimeId !== "onnx" || hardware !== null)) {
|
|
4387
|
+
this.log.info("Auto-pick: onnx floor pending gpu probe — NOT persisting (re-pick on done)", { meta: {
|
|
4279
4388
|
backend: pick.runtimeId,
|
|
4280
4389
|
device: pick.device
|
|
4281
4390
|
} });
|
|
@@ -4286,10 +4395,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4286
4395
|
[nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
|
|
4287
4396
|
[nodeEngineKey("engineDevice", apNode)]: pick.device
|
|
4288
4397
|
});
|
|
4289
|
-
this.log.info("Auto-picked engine
|
|
4398
|
+
this.log.info("Auto-picked engine (platform-deterministic)", { meta: {
|
|
4290
4399
|
backend: pick.runtimeId,
|
|
4291
4400
|
device: pick.device,
|
|
4292
|
-
|
|
4401
|
+
hadProbeHardware: hardware !== null
|
|
4293
4402
|
} });
|
|
4294
4403
|
}
|
|
4295
4404
|
/** Map a backend string to a known RuntimeId, flooring to onnx. */
|
|
@@ -4349,7 +4458,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4349
4458
|
const runtimeId = this.toRuntimeId(engine.backend);
|
|
4350
4459
|
const device = engine.device ?? "cpu";
|
|
4351
4460
|
const snapshot = this.provisioner.state;
|
|
4352
|
-
|
|
4461
|
+
const sameSelection = snapshot.runtimeId === runtimeId && snapshot.device === device;
|
|
4462
|
+
if (sameSelection && snapshot.state !== "idle") return;
|
|
4463
|
+
if (!sameSelection) this.currentSteps = null;
|
|
4353
4464
|
this.provisioner.select(runtimeId, device);
|
|
4354
4465
|
}
|
|
4355
4466
|
/**
|
|
@@ -4360,8 +4471,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4360
4471
|
* runtime through installing → verifying → ready.
|
|
4361
4472
|
*/
|
|
4362
4473
|
async onEngineSelectionChanged() {
|
|
4474
|
+
const prev = this.currentEngine;
|
|
4363
4475
|
const stored = await this.loadEngine();
|
|
4364
4476
|
if (stored) this.currentEngine = stored;
|
|
4477
|
+
if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
|
|
4478
|
+
this.log.info("engine selection changed — rebuilding pool in place", { meta: {
|
|
4479
|
+
from: `${prev.backend}/${prev.device ?? "default"}`,
|
|
4480
|
+
to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
|
|
4481
|
+
} });
|
|
4482
|
+
try {
|
|
4483
|
+
await this.engineFactory.dispose();
|
|
4484
|
+
} catch {}
|
|
4485
|
+
this.engineFactory = null;
|
|
4486
|
+
this.executor = null;
|
|
4487
|
+
}
|
|
4365
4488
|
this.startProvisioningForCurrentEngine();
|
|
4366
4489
|
}
|
|
4367
4490
|
/**
|
|
@@ -4399,9 +4522,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4399
4522
|
if (!steps || steps.length === 0) return;
|
|
4400
4523
|
for (const step of flattenSteps(steps)) {
|
|
4401
4524
|
if (!step.enabled) continue;
|
|
4402
|
-
const modelEntry =
|
|
4525
|
+
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
4403
4526
|
if (!modelEntry) continue;
|
|
4404
|
-
if (
|
|
4527
|
+
if (require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
|
|
4405
4528
|
await this.downloadWithRetry(modelEntry, format, 3);
|
|
4406
4529
|
}
|
|
4407
4530
|
}
|
|
@@ -4477,10 +4600,44 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4477
4600
|
return { hardware: null };
|
|
4478
4601
|
}
|
|
4479
4602
|
}
|
|
4603
|
+
/**
|
|
4604
|
+
* Pull custom models from the `custom-model-registry` collection cap,
|
|
4605
|
+
* grouped by step id. 5s TTL cache. Fully graceful: when no provider is
|
|
4606
|
+
* registered (or the query fails) it returns an empty map and logs at most
|
|
4607
|
+
* a warning — callers then behave exactly like the static-catalog path.
|
|
4608
|
+
*/
|
|
4609
|
+
async getCustomModels() {
|
|
4610
|
+
const now = Date.now();
|
|
4611
|
+
if (this.customModelsCache && now - this.customModelsCache.at < 5e3) return this.customModelsCache.byStep;
|
|
4612
|
+
let byStep = /* @__PURE__ */ new Map();
|
|
4613
|
+
try {
|
|
4614
|
+
const api = this.addonCtx?.api;
|
|
4615
|
+
if (api) {
|
|
4616
|
+
if ((await api.addons.listCapabilityProviders.query({ capName: "custom-model-registry" })).some((p) => p.isActive)) byStep = groupCustomModelsByStep(await api.customModelRegistry.listModels.query());
|
|
4617
|
+
}
|
|
4618
|
+
} catch (err) {
|
|
4619
|
+
this.log.warn("custom-model-registry query failed — using static catalog only", { meta: { error: require_dist.errMsg(err) } });
|
|
4620
|
+
}
|
|
4621
|
+
this.customModelsCache = {
|
|
4622
|
+
at: now,
|
|
4623
|
+
byStep
|
|
4624
|
+
};
|
|
4625
|
+
return byStep;
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* Resolve a model id within a step to a catalog entry — static catalog
|
|
4629
|
+
* first, then the custom registry. Returns undefined if neither has it.
|
|
4630
|
+
*/
|
|
4631
|
+
async resolveModelEntry(addonId, modelId) {
|
|
4632
|
+
const fromCatalog = getStepDefinition(addonId).models.find((m) => m.id === modelId);
|
|
4633
|
+
if (fromCatalog) return fromCatalog;
|
|
4634
|
+
return (await this.getCustomModels()).get(addonId)?.find((m) => m.id === modelId);
|
|
4635
|
+
}
|
|
4480
4636
|
async getSchema(engine) {
|
|
4481
4637
|
if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
|
|
4482
4638
|
const format = engine.format;
|
|
4483
|
-
const
|
|
4639
|
+
const customByStep = await this.getCustomModels();
|
|
4640
|
+
const slots = buildSchemaSlots(format, this.modelsDir, customByStep);
|
|
4484
4641
|
const { hardware } = await this.fetchProbeGatingData();
|
|
4485
4642
|
const env = runtimeEnvFromProcess(toProbedHardware(hardware));
|
|
4486
4643
|
return {
|
|
@@ -4539,13 +4696,52 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4539
4696
|
async getDefaultSteps(engine) {
|
|
4540
4697
|
return buildDefaultStepTree(engine.format);
|
|
4541
4698
|
}
|
|
4699
|
+
/**
|
|
4700
|
+
* Substitute, AT RUNTIME ONLY (never persisted), any step whose configured
|
|
4701
|
+
* model lacks a build for that step's active engine format. A GLOBAL pipeline
|
|
4702
|
+
* config can pin a model that's valid on the cluster's Intel nodes (e.g. the
|
|
4703
|
+
* OpenVINO-only `yolov9t-int8`) but impossible on a CoreML/ONNX node — without
|
|
4704
|
+
* this the node crash-loops `Model "X" has no <format> format`. The operator's
|
|
4705
|
+
* choice stays in the persisted config; this node just runs the smallest
|
|
4706
|
+
* catalog model that DOES have its format. Mirrors the per-node engine
|
|
4707
|
+
* fallback in `loadEngine`. Catalog models only — a custom model that lacks
|
|
4708
|
+
* the format is left as-is (operator's responsibility).
|
|
4709
|
+
*/
|
|
4710
|
+
substituteIncompatibleModels(steps) {
|
|
4711
|
+
const fix = (step) => {
|
|
4712
|
+
const format = step.engine?.format ?? this.currentEngine.format;
|
|
4713
|
+
let modelId = step.modelId;
|
|
4714
|
+
try {
|
|
4715
|
+
const entry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
|
|
4716
|
+
if (entry && !entry.formats[format]) {
|
|
4717
|
+
const fallback = getDefaultModelForFormat(step.addonId, format);
|
|
4718
|
+
if (fallback !== step.modelId) {
|
|
4719
|
+
this.log.info("Step model lacks engine format — substituting format default (runtime)", { meta: {
|
|
4720
|
+
step: step.addonId,
|
|
4721
|
+
configured: step.modelId,
|
|
4722
|
+
substitute: fallback,
|
|
4723
|
+
format
|
|
4724
|
+
} });
|
|
4725
|
+
modelId = fallback;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
} catch {}
|
|
4729
|
+
const children = step.children?.length ? step.children.map(fix) : step.children;
|
|
4730
|
+
return modelId === step.modelId && children === step.children ? step : {
|
|
4731
|
+
...step,
|
|
4732
|
+
modelId,
|
|
4733
|
+
...children ? { children } : {}
|
|
4734
|
+
};
|
|
4735
|
+
};
|
|
4736
|
+
return steps.map(fix);
|
|
4737
|
+
}
|
|
4542
4738
|
async getGlobalSteps() {
|
|
4543
4739
|
if (this.currentSteps) return this.currentSteps;
|
|
4544
4740
|
const raw = (await this.readStore())[KEY_STEPS];
|
|
4545
4741
|
if (!raw) {
|
|
4546
4742
|
const defaults = buildDefaultStepTree(this.currentEngine.format);
|
|
4547
4743
|
if (defaults.length === 0) return null;
|
|
4548
|
-
this.currentSteps = defaults;
|
|
4744
|
+
this.currentSteps = this.substituteIncompatibleModels(defaults);
|
|
4549
4745
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
4550
4746
|
this.log.info("Bootstrapped default pipeline — object-detection + face + plate recognition enabled by default", { meta: { rootSteps: defaults.length } });
|
|
4551
4747
|
return this.currentSteps;
|
|
@@ -4583,7 +4779,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4583
4779
|
this.log.info("Migration: added audio-classifier step to persisted pipeline config");
|
|
4584
4780
|
}
|
|
4585
4781
|
}
|
|
4586
|
-
this.currentSteps = steps;
|
|
4782
|
+
this.currentSteps = this.substituteIncompatibleModels(steps);
|
|
4587
4783
|
return this.currentSteps;
|
|
4588
4784
|
} catch {
|
|
4589
4785
|
throw new Error(`Failed to parse persisted pipeline steps: corrupt data in key "${KEY_STEPS}"`);
|
|
@@ -4609,7 +4805,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4609
4805
|
const formats = {};
|
|
4610
4806
|
for (const [formatKey, entry] of Object.entries(m.formats)) {
|
|
4611
4807
|
if (!entry) continue;
|
|
4612
|
-
const downloaded =
|
|
4808
|
+
const downloaded = require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, m, formatKey);
|
|
4613
4809
|
formats[formatKey] = {
|
|
4614
4810
|
url: entry.url,
|
|
4615
4811
|
sizeMB: entry.sizeMB,
|
|
@@ -4667,7 +4863,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4667
4863
|
}
|
|
4668
4864
|
async downloadModel(input) {
|
|
4669
4865
|
const { modelId, format, addonId } = input;
|
|
4670
|
-
const modelEntry =
|
|
4866
|
+
const modelEntry = await this.resolveModelEntry(addonId, modelId);
|
|
4671
4867
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
|
|
4672
4868
|
const formatEntry = modelEntry.formats[format];
|
|
4673
4869
|
if (!formatEntry) throw new Error(`Model "${modelId}" has no ${format} format. Available: ${Object.keys(modelEntry.formats).join(", ")}`);
|
|
@@ -4730,9 +4926,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4730
4926
|
}
|
|
4731
4927
|
async deleteModel(input) {
|
|
4732
4928
|
const { modelId, format, addonId } = input;
|
|
4733
|
-
const modelEntry =
|
|
4929
|
+
const modelEntry = await this.resolveModelEntry(addonId, modelId);
|
|
4734
4930
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
|
|
4735
|
-
if (!
|
|
4931
|
+
if (!require_model_download_service_C_IHWnXx.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
|
|
4736
4932
|
this.log.info("Model deleted from disk", { meta: {
|
|
4737
4933
|
modelId,
|
|
4738
4934
|
format
|
|
@@ -5186,8 +5382,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5186
5382
|
const work = (async () => {
|
|
5187
5383
|
const format = this.currentEngine?.format ?? "onnx";
|
|
5188
5384
|
for (const step of needed) {
|
|
5189
|
-
const modelEntry =
|
|
5190
|
-
if (modelEntry && !
|
|
5385
|
+
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
5386
|
+
if (modelEntry && !require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) {
|
|
5191
5387
|
this.log.info("Downloading model for step", { meta: {
|
|
5192
5388
|
modelId: step.modelId,
|
|
5193
5389
|
format,
|
|
@@ -5212,7 +5408,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5212
5408
|
/** Download a model with retry + exponential backoff */
|
|
5213
5409
|
async downloadWithRetry(entry, format, maxRetries, onProgress) {
|
|
5214
5410
|
for (let attempt = 1; attempt <= maxRetries; attempt++) try {
|
|
5215
|
-
await
|
|
5411
|
+
await require_model_download_service_C_IHWnXx.ensureModel(this.modelsDir, entry, format, onProgress);
|
|
5216
5412
|
this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
|
|
5217
5413
|
return;
|
|
5218
5414
|
} catch (err) {
|
|
@@ -5681,7 +5877,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5681
5877
|
const format = this.currentEngine.format;
|
|
5682
5878
|
const downloads = [];
|
|
5683
5879
|
for (const step of flattenSteps(steps)) {
|
|
5684
|
-
const modelEntry =
|
|
5880
|
+
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
5685
5881
|
if (!modelEntry) {
|
|
5686
5882
|
this.log.warn("Model not found in step catalog — skipping download", { meta: {
|
|
5687
5883
|
modelId: step.modelId,
|
|
@@ -5689,11 +5885,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5689
5885
|
} });
|
|
5690
5886
|
continue;
|
|
5691
5887
|
}
|
|
5692
|
-
if (!
|
|
5888
|
+
if (!require_model_download_service_C_IHWnXx.isModelDownloaded(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
|
|
5693
5889
|
modelId: step.modelId,
|
|
5694
5890
|
format
|
|
5695
5891
|
} });
|
|
5696
|
-
downloads.push(
|
|
5892
|
+
downloads.push(require_model_download_service_C_IHWnXx.ensureModel(this.modelsDir, modelEntry, format).then(() => {}));
|
|
5697
5893
|
}
|
|
5698
5894
|
await Promise.all(downloads);
|
|
5699
5895
|
await this.ensureBackendDeps(this.currentEngine);
|
|
@@ -5734,12 +5930,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5734
5930
|
const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
|
|
5735
5931
|
const floor = onnxFloorPick();
|
|
5736
5932
|
const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
|
|
5737
|
-
if (!
|
|
5738
|
-
|
|
5933
|
+
if (!backendPossibleOnPlatform(migratedBackend)) {
|
|
5934
|
+
const platformDefault = platformDefaultPick();
|
|
5935
|
+
this.log.warn("Stored engine backend impossible on this platform — using platform default", { meta: {
|
|
5739
5936
|
stored: migratedBackend,
|
|
5740
|
-
fallback:
|
|
5937
|
+
fallback: platformDefault.backend
|
|
5741
5938
|
} });
|
|
5742
|
-
return
|
|
5939
|
+
return platformDefault;
|
|
5743
5940
|
}
|
|
5744
5941
|
const device = storedDevice || floor.device;
|
|
5745
5942
|
return {
|
|
@@ -5911,44 +6108,61 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5911
6108
|
}
|
|
5912
6109
|
async reprobeEngine() {
|
|
5913
6110
|
const api = this.addonCtx?.api;
|
|
5914
|
-
let
|
|
6111
|
+
let hardware = null;
|
|
5915
6112
|
if (api) try {
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
return "cpu";
|
|
5926
|
-
})();
|
|
5927
|
-
best = {
|
|
5928
|
-
runtime: "python",
|
|
5929
|
-
backend: probeBackend,
|
|
5930
|
-
format: backendToFormat(probeBackend),
|
|
5931
|
-
device: probeDevice
|
|
5932
|
-
};
|
|
5933
|
-
} else best = onnxFloorPick();
|
|
5934
|
-
} catch {
|
|
5935
|
-
best = onnxFloorPick();
|
|
5936
|
-
}
|
|
5937
|
-
else best = onnxFloorPick();
|
|
6113
|
+
hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
|
|
6114
|
+
} catch {}
|
|
6115
|
+
const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
|
|
6116
|
+
const best = {
|
|
6117
|
+
runtime: "python",
|
|
6118
|
+
backend: pick.runtimeId,
|
|
6119
|
+
format: modelFormatFor(pick.runtimeId),
|
|
6120
|
+
device: pick.device
|
|
6121
|
+
};
|
|
5938
6122
|
const probedLabel = `${best.backend}/${best.device ?? "default"}`;
|
|
5939
6123
|
const rpNode = this.localProbeNodeId();
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
6124
|
+
if (pick.runtimeId !== "onnx" || hardware !== null) {
|
|
6125
|
+
await this.writeStore({
|
|
6126
|
+
[nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
|
|
6127
|
+
[nodeEngineKey("engineBackend", rpNode)]: best.backend,
|
|
6128
|
+
[nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
|
|
6129
|
+
});
|
|
6130
|
+
this.log.info("Re-probed engine (platform-deterministic) — wrote back", { meta: {
|
|
6131
|
+
backend: best.backend,
|
|
6132
|
+
device: best.device ?? null,
|
|
6133
|
+
probedBestEngine: probedLabel
|
|
6134
|
+
} });
|
|
6135
|
+
} else this.log.info("Re-probe: onnx floor pending gpu probe — NOT persisting (re-pick on done)", { meta: {
|
|
5946
6136
|
backend: best.backend,
|
|
5947
|
-
device: best.device ?? null
|
|
5948
|
-
probedBestEngine: probedLabel
|
|
6137
|
+
device: best.device ?? null
|
|
5949
6138
|
} });
|
|
5950
6139
|
return best;
|
|
5951
6140
|
}
|
|
6141
|
+
/**
|
|
6142
|
+
* Re-pick the engine when the platform-probe finishes its async hardware +
|
|
6143
|
+
* Python detection (the `platform-probe.phase` `done` event). At boot the
|
|
6144
|
+
* probe's accelerator result may not be ready yet, so the engine floored to
|
|
6145
|
+
* onnx; once the probe answers (e.g. a Mac's CoreML/ANE surfaces after the
|
|
6146
|
+
* embedded Python is installed) this re-runs the probe-driven pick and
|
|
6147
|
+
* re-provisions. Idempotent: `startProvisioningForCurrentEngine` skips a
|
|
6148
|
+
* no-op when the selection is unchanged.
|
|
6149
|
+
*/
|
|
6150
|
+
async repickEngineOnProbeReady() {
|
|
6151
|
+
const before = `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`;
|
|
6152
|
+
await this.reprobeEngine();
|
|
6153
|
+
const stored = await this.loadEngine();
|
|
6154
|
+
if (stored) {
|
|
6155
|
+
this.currentEngine = stored;
|
|
6156
|
+
this.needsAutoPick = false;
|
|
6157
|
+
this.cancelDeferredAutoPick();
|
|
6158
|
+
}
|
|
6159
|
+
const after = `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`;
|
|
6160
|
+
if (before !== after) this.log.info("Engine re-picked after platform-probe completed", { meta: {
|
|
6161
|
+
before,
|
|
6162
|
+
after
|
|
6163
|
+
} });
|
|
6164
|
+
this.startProvisioningForCurrentEngine();
|
|
6165
|
+
}
|
|
5952
6166
|
async getReferenceAudioFiles() {
|
|
5953
6167
|
const dir = resolveReferenceAudioDir();
|
|
5954
6168
|
if (!dir) return [];
|
|
@@ -6084,11 +6298,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6084
6298
|
}
|
|
6085
6299
|
}
|
|
6086
6300
|
};
|
|
6087
|
-
function buildSchemaSlots(format, modelsDir) {
|
|
6301
|
+
function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
6088
6302
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6089
6303
|
for (const pipelineStep of ALL_PIPELINE_STEPS) {
|
|
6090
6304
|
const step = pipelineStep.definition;
|
|
6091
|
-
const availableModels = step.models.filter((m) => m.formats[format]);
|
|
6305
|
+
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format]);
|
|
6092
6306
|
if (availableModels.length === 0) continue;
|
|
6093
6307
|
const slot = step.slot;
|
|
6094
6308
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
@@ -6104,7 +6318,7 @@ function buildSchemaSlots(format, modelsDir) {
|
|
|
6104
6318
|
id: m.id,
|
|
6105
6319
|
name: m.name,
|
|
6106
6320
|
formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
|
|
6107
|
-
downloaded:
|
|
6321
|
+
downloaded: require_model_download_service_C_IHWnXx.isModelDownloaded(modelsDir, m, f),
|
|
6108
6322
|
sizeMB: entry.sizeMB
|
|
6109
6323
|
}]))
|
|
6110
6324
|
})),
|
|
@@ -6193,8 +6407,7 @@ function buildDefaultStepTree(format) {
|
|
|
6193
6407
|
makeStep("animal-classifier", [], { enabled: false }),
|
|
6194
6408
|
makeStep("bird-classifier", [], { enabled: false }),
|
|
6195
6409
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
6196
|
-
makeStep("segmentation
|
|
6197
|
-
makeStep("instance-segmentation", [], { enabled: false })
|
|
6410
|
+
makeStep("segmentation", [], { enabled: false })
|
|
6198
6411
|
].filter((s) => s !== null));
|
|
6199
6412
|
const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
|
|
6200
6413
|
runtime: "python",
|
|
@@ -6417,6 +6630,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6417
6630
|
nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
|
|
6418
6631
|
nodeProbedBestEngine = "";
|
|
6419
6632
|
engineMetricsTimer = null;
|
|
6633
|
+
probePhaseUnsub = null;
|
|
6420
6634
|
/** Snapshot-equality cache for engine-metrics emit. Most ticks
|
|
6421
6635
|
* the engine inventory is unchanged (no model load/unload), so
|
|
6422
6636
|
* we skip the bus emit and let the heartbeat re-emit at
|
|
@@ -6484,8 +6698,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6484
6698
|
label: "Execution provider",
|
|
6485
6699
|
options: [...STATIC_BACKEND_OPTIONS],
|
|
6486
6700
|
default: DEFAULT_CONFIG.engineBackend,
|
|
6487
|
-
immediate: true
|
|
6488
|
-
requiresRestart: true
|
|
6701
|
+
immediate: true
|
|
6489
6702
|
}),
|
|
6490
6703
|
this.field({
|
|
6491
6704
|
type: "select",
|
|
@@ -6493,8 +6706,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6493
6706
|
label: "Hardware device",
|
|
6494
6707
|
options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
|
|
6495
6708
|
default: DEFAULT_CONFIG.engineDevice,
|
|
6496
|
-
immediate: true
|
|
6497
|
-
requiresRestart: true
|
|
6709
|
+
immediate: true
|
|
6498
6710
|
})
|
|
6499
6711
|
]
|
|
6500
6712
|
}, {
|
|
@@ -6533,8 +6745,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6533
6745
|
"onnx",
|
|
6534
6746
|
"cpu"
|
|
6535
6747
|
]
|
|
6536
|
-
}
|
|
6537
|
-
requiresRestart: true
|
|
6748
|
+
}
|
|
6538
6749
|
}),
|
|
6539
6750
|
this.field({
|
|
6540
6751
|
type: "slider",
|
|
@@ -6551,8 +6762,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6551
6762
|
showWhen: {
|
|
6552
6763
|
field: "batchMode",
|
|
6553
6764
|
notEquals: "none"
|
|
6554
|
-
}
|
|
6555
|
-
requiresRestart: true
|
|
6765
|
+
}
|
|
6556
6766
|
}),
|
|
6557
6767
|
this.field({
|
|
6558
6768
|
type: "slider",
|
|
@@ -6569,8 +6779,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6569
6779
|
showWhen: {
|
|
6570
6780
|
field: "batchMode",
|
|
6571
6781
|
notEquals: "none"
|
|
6572
|
-
}
|
|
6573
|
-
requiresRestart: true
|
|
6782
|
+
}
|
|
6574
6783
|
}),
|
|
6575
6784
|
this.field({
|
|
6576
6785
|
type: "slider",
|
|
@@ -6583,8 +6792,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6583
6792
|
default: void 0,
|
|
6584
6793
|
showValue: true,
|
|
6585
6794
|
nullable: true,
|
|
6586
|
-
nullLabel: "Auto"
|
|
6587
|
-
requiresRestart: true
|
|
6795
|
+
nullLabel: "Auto"
|
|
6588
6796
|
}),
|
|
6589
6797
|
this.field({
|
|
6590
6798
|
type: "slider",
|
|
@@ -6597,8 +6805,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6597
6805
|
default: void 0,
|
|
6598
6806
|
showValue: true,
|
|
6599
6807
|
nullable: true,
|
|
6600
|
-
nullLabel: "Auto"
|
|
6601
|
-
requiresRestart: true
|
|
6808
|
+
nullLabel: "Auto"
|
|
6602
6809
|
})
|
|
6603
6810
|
]
|
|
6604
6811
|
}] });
|
|
@@ -6830,11 +7037,35 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6830
7037
|
numWorkers: num(t["numWorkers"], 1)
|
|
6831
7038
|
};
|
|
6832
7039
|
}
|
|
6833
|
-
|
|
6834
|
-
|
|
7040
|
+
/**
|
|
7041
|
+
* Resolve the directory models are downloaded into, resilient across nodes.
|
|
7042
|
+
* The `models` storage location is GLOBAL (hub-seeded) and can resolve to a
|
|
7043
|
+
* path that exists only on the hub — e.g. Docker's `/data/models`, which an
|
|
7044
|
+
* agent on a different filesystem (a Mac) cannot create (`ENOENT mkdir
|
|
7045
|
+
* /data/models`). Verify the resolved dir is creatable; otherwise fall back to
|
|
7046
|
+
* this node's LOCAL addon data-dir so models always land on writable disk.
|
|
7047
|
+
*/
|
|
7048
|
+
async resolveModelsDir() {
|
|
7049
|
+
const fallback = node_path.join(this.ctx.dataDir, "models");
|
|
7050
|
+
const candidate = await this.ctx.api.storage.resolve.query({
|
|
6835
7051
|
location: "models",
|
|
6836
7052
|
relativePath: ""
|
|
6837
|
-
}).catch(() =>
|
|
7053
|
+
}).catch(() => null) ?? fallback;
|
|
7054
|
+
try {
|
|
7055
|
+
await node_fs.promises.mkdir(candidate, { recursive: true });
|
|
7056
|
+
return candidate;
|
|
7057
|
+
} catch (err) {
|
|
7058
|
+
this.ctx.logger.warn("models dir not creatable on this node — using local data-dir", { meta: {
|
|
7059
|
+
resolved: candidate,
|
|
7060
|
+
fallback,
|
|
7061
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7062
|
+
} });
|
|
7063
|
+
await node_fs.promises.mkdir(fallback, { recursive: true });
|
|
7064
|
+
return fallback;
|
|
7065
|
+
}
|
|
7066
|
+
}
|
|
7067
|
+
async onInitialize() {
|
|
7068
|
+
const modelsDir = await this.resolveModelsDir();
|
|
6838
7069
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
6839
7070
|
await this.refreshNodeEngineFromStore();
|
|
6840
7071
|
this.pythonAddonDir = resolveAddonPythonDir();
|
|
@@ -6864,6 +7095,12 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6864
7095
|
await this.provider.ensureBootEngineProvisioned().catch((err) => {
|
|
6865
7096
|
this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6866
7097
|
});
|
|
7098
|
+
this.probePhaseUnsub = this.ctx.eventBus?.subscribe({ category: require_dist.EventCategory.PlatformProbePhase }, (event) => {
|
|
7099
|
+
if (event.data?.phase !== "done") return;
|
|
7100
|
+
this.provider.repickEngineOnProbeReady().catch((err) => {
|
|
7101
|
+
this.ctx.logger.warn("repick on platform-probe done failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7102
|
+
});
|
|
7103
|
+
}) ?? null;
|
|
6867
7104
|
await this.provider.warmPool();
|
|
6868
7105
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
6869
7106
|
this.lastAppliedPoolConfig = this.snapshotPoolConfig();
|
|
@@ -6937,6 +7174,10 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6937
7174
|
}
|
|
6938
7175
|
}
|
|
6939
7176
|
async onShutdown() {
|
|
7177
|
+
if (this.probePhaseUnsub) {
|
|
7178
|
+
this.probePhaseUnsub();
|
|
7179
|
+
this.probePhaseUnsub = null;
|
|
7180
|
+
}
|
|
6940
7181
|
if (this.engineMetricsTimer) {
|
|
6941
7182
|
clearInterval(this.engineMetricsTimer);
|
|
6942
7183
|
this.engineMetricsTimer = null;
|
|
@@ -6971,16 +7212,17 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6971
7212
|
return false;
|
|
6972
7213
|
}
|
|
6973
7214
|
/**
|
|
6974
|
-
* BaseAddon calls `onConfigChanged` after every settings write.
|
|
6975
|
-
*
|
|
6976
|
-
*
|
|
6977
|
-
*
|
|
6978
|
-
*
|
|
6979
|
-
* respawn the
|
|
6980
|
-
*
|
|
6981
|
-
*
|
|
6982
|
-
*
|
|
6983
|
-
*
|
|
7215
|
+
* BaseAddon calls `onConfigChanged` after every settings write. This is the
|
|
7216
|
+
* SOLE apply path for engine + tuning changes — none of those fields declare
|
|
7217
|
+
* `requiresRestart` anymore (an addon restart re-probes hardware + reloads
|
|
7218
|
+
* every model on every set, which is exactly what we want to avoid). Instead:
|
|
7219
|
+
* - Pool-bound tuning (`concurrency`/`batchMode`/`windowMs`/…) → in-place
|
|
7220
|
+
* pool respawn when the snapshot flips (`poolConfigChanged` below).
|
|
7221
|
+
* - Engine cascade (`engineBackend`/`engineDevice`) → the provider's
|
|
7222
|
+
* `onEngineSelectionChanged` disposes the device-bound factory so the next
|
|
7223
|
+
* runPipeline rebuilds the pool on the new selection, in place.
|
|
7224
|
+
* Both apply optimistically — the inference gate stays closed for the short
|
|
7225
|
+
* re-spin, frames are dropped (never crashed), no addon bounce.
|
|
6984
7226
|
*/
|
|
6985
7227
|
async onConfigChanged() {
|
|
6986
7228
|
await this.refreshNodeEngineFromStore();
|
|
@@ -7001,10 +7243,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
7001
7243
|
} catch (err) {
|
|
7002
7244
|
this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7003
7245
|
}
|
|
7004
|
-
const modelsDir = await this.
|
|
7005
|
-
location: "models",
|
|
7006
|
-
relativePath: ""
|
|
7007
|
-
}).catch(() => "camstack-data/models");
|
|
7246
|
+
const modelsDir = await this.resolveModelsDir();
|
|
7008
7247
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available during respawn");
|
|
7009
7248
|
const effectiveTuning = this.resolveBackendTuning();
|
|
7010
7249
|
this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
|