@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-
|
|
3
|
-
import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-
|
|
2
|
+
import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-BWc-HYQz.mjs";
|
|
3
|
+
import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-3Mmeob3l.mjs";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import * as os from "node:os";
|
|
@@ -114,7 +114,6 @@ function npuInfoFrom(hw) {
|
|
|
114
114
|
return { type };
|
|
115
115
|
}
|
|
116
116
|
function envToHardwareInfo(env) {
|
|
117
|
-
if (!env.hardware) return null;
|
|
118
117
|
return {
|
|
119
118
|
platform: toKnownPlatform(env.platform),
|
|
120
119
|
arch: toKnownArch(env.arch),
|
|
@@ -122,8 +121,8 @@ function envToHardwareInfo(env) {
|
|
|
122
121
|
cpuCores: 0,
|
|
123
122
|
totalRAM_MB: 0,
|
|
124
123
|
availableRAM_MB: 0,
|
|
125
|
-
gpu: gpuInfoFrom(env.hardware),
|
|
126
|
-
npu: npuInfoFrom(env.hardware)
|
|
124
|
+
gpu: env.hardware ? gpuInfoFrom(env.hardware) : null,
|
|
125
|
+
npu: env.hardware ? npuInfoFrom(env.hardware) : null
|
|
127
126
|
};
|
|
128
127
|
}
|
|
129
128
|
function probedToHardwareInfo(hw) {
|
|
@@ -1036,6 +1035,26 @@ var ovFormat = (url, sizeMB) => {
|
|
|
1036
1035
|
...files ? { files } : {}
|
|
1037
1036
|
};
|
|
1038
1037
|
};
|
|
1038
|
+
/**
|
|
1039
|
+
* Build a precision-variant catalog entry (OpenVINO-only) derived from a base
|
|
1040
|
+
* detection model. fp16 halves the weights (Intel iGPU/NPU sweet spot); int8 is
|
|
1041
|
+
* NNCF post-training-quantized (~4× smaller, fastest on CPU/iGPU at a small
|
|
1042
|
+
* accuracy cost). The IRs live next to the base `.xml` on HF as
|
|
1043
|
+
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
1044
|
+
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
1045
|
+
*/
|
|
1046
|
+
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
|
|
1047
|
+
id: `${baseId}-${precision}`,
|
|
1048
|
+
name: `${baseName} (${precision.toUpperCase()})`,
|
|
1049
|
+
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
1050
|
+
inputSize: {
|
|
1051
|
+
width: 640,
|
|
1052
|
+
height: 640
|
|
1053
|
+
},
|
|
1054
|
+
labels: [],
|
|
1055
|
+
preprocessMode: "letterbox",
|
|
1056
|
+
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) }
|
|
1057
|
+
});
|
|
1039
1058
|
var MLPACKAGE_FILES = [
|
|
1040
1059
|
"Manifest.json",
|
|
1041
1060
|
"Data/com.apple.CoreML/model.mlmodel",
|
|
@@ -1341,7 +1360,21 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1341
1360
|
},
|
|
1342
1361
|
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1343
1362
|
}
|
|
1344
|
-
}
|
|
1363
|
+
},
|
|
1364
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5),
|
|
1365
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3),
|
|
1366
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15),
|
|
1367
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8),
|
|
1368
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5),
|
|
1369
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
1370
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19),
|
|
1371
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
1372
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41),
|
|
1373
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
1374
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50),
|
|
1375
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
1376
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112),
|
|
1377
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56)
|
|
1345
1378
|
];
|
|
1346
1379
|
var FACE_DETECTION_MODELS = [{
|
|
1347
1380
|
id: "scrfd-2.5g",
|
|
@@ -1788,7 +1821,7 @@ var ObjectDetectionStep = class {
|
|
|
1788
1821
|
"animal"
|
|
1789
1822
|
],
|
|
1790
1823
|
models: [...OBJECT_DETECTION_MODELS],
|
|
1791
|
-
defaultModelId: "
|
|
1824
|
+
defaultModelId: "yolo26n",
|
|
1792
1825
|
defaultConfidence: .5,
|
|
1793
1826
|
labels: COCO_80_LABELS.map((l) => l.id),
|
|
1794
1827
|
classMap: COCO_TO_MACRO
|
|
@@ -2037,9 +2070,9 @@ var STEP_VEHICLE_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
|
2037
2070
|
enabledByDefault: false,
|
|
2038
2071
|
defaultConfidence: .3
|
|
2039
2072
|
});
|
|
2040
|
-
var
|
|
2041
|
-
id: "segmentation
|
|
2042
|
-
name: "
|
|
2073
|
+
var STEP_SEGMENTATION = new PipelineStepBase({
|
|
2074
|
+
id: "segmentation",
|
|
2075
|
+
name: "Segmentation",
|
|
2043
2076
|
slot: "refiner",
|
|
2044
2077
|
postprocessor: "saliency",
|
|
2045
2078
|
extractMode: "crop-roi",
|
|
@@ -2051,7 +2084,7 @@ var STEP_SEGMENTATION_REFINER = new PipelineStepBase({
|
|
|
2051
2084
|
defaultConfidence: 0,
|
|
2052
2085
|
group: "Segmentation"
|
|
2053
2086
|
});
|
|
2054
|
-
|
|
2087
|
+
new PipelineStepBase({
|
|
2055
2088
|
id: "instance-segmentation",
|
|
2056
2089
|
name: "Instance Segmentation",
|
|
2057
2090
|
slot: "refiner",
|
|
@@ -2078,8 +2111,7 @@ var ALL_PIPELINE_STEPS = [
|
|
|
2078
2111
|
new AnimalClassifierStep(),
|
|
2079
2112
|
STEP_BIRD_CLASSIFIER,
|
|
2080
2113
|
STEP_VEHICLE_CLASSIFIER,
|
|
2081
|
-
|
|
2082
|
-
STEP_INSTANCE_SEGMENTATION,
|
|
2114
|
+
STEP_SEGMENTATION,
|
|
2083
2115
|
STEP_AUDIO_CLASSIFIER_INSTANCE
|
|
2084
2116
|
];
|
|
2085
2117
|
/** Compat: flat array of StepDefinition for existing consumers */
|
|
@@ -3250,6 +3282,18 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
|
|
|
3250
3282
|
parent.mask = output.mask;
|
|
3251
3283
|
parent.maskWidth = output.maskWidth;
|
|
3252
3284
|
parent.maskHeight = output.maskHeight;
|
|
3285
|
+
if (output.maskBbox !== void 0 && output.maskWidth > 0 && output.maskHeight > 0) {
|
|
3286
|
+
const [px1, py1, px2, py2] = parent.bbox;
|
|
3287
|
+
const parentW = px2 - px1;
|
|
3288
|
+
const parentH = py2 - py1;
|
|
3289
|
+
const [mbx, mby, mbw, mbh] = output.maskBbox;
|
|
3290
|
+
parent.refinedBbox = [
|
|
3291
|
+
px1 + mbx / output.maskWidth * parentW,
|
|
3292
|
+
py1 + mby / output.maskHeight * parentH,
|
|
3293
|
+
px1 + (mbx + mbw) / output.maskWidth * parentW,
|
|
3294
|
+
py1 + (mby + mbh) / output.maskHeight * parentH
|
|
3295
|
+
];
|
|
3296
|
+
}
|
|
3253
3297
|
break;
|
|
3254
3298
|
}
|
|
3255
3299
|
}
|
|
@@ -3300,6 +3344,12 @@ function buildFrameResult(input) {
|
|
|
3300
3344
|
macroClass: m.macroClass,
|
|
3301
3345
|
score: m.score,
|
|
3302
3346
|
bbox,
|
|
3347
|
+
...m.refinedBbox !== void 0 ? { refinedBbox: bboxTupleToRect(m.refinedBbox) } : {},
|
|
3348
|
+
...m.mask !== void 0 && m.maskWidth !== void 0 && m.maskHeight !== void 0 ? {
|
|
3349
|
+
mask: m.mask,
|
|
3350
|
+
maskWidth: m.maskWidth,
|
|
3351
|
+
maskHeight: m.maskHeight
|
|
3352
|
+
} : {},
|
|
3303
3353
|
labels,
|
|
3304
3354
|
...m.parentId !== void 0 ? { parentId: m.parentId } : {},
|
|
3305
3355
|
...embedding !== void 0 ? {
|
|
@@ -3790,6 +3840,40 @@ function walkFieldsForDefaults(fields, out) {
|
|
|
3790
3840
|
}
|
|
3791
3841
|
}
|
|
3792
3842
|
//#endregion
|
|
3843
|
+
//#region src/detection-pipeline/registry/custom-models.ts
|
|
3844
|
+
/**
|
|
3845
|
+
* Group a flat list of custom-model descriptors (as returned by the
|
|
3846
|
+
* `custom-model-registry` collection cap) into a `stepId → entries` map for
|
|
3847
|
+
* the picker / resolution union. Pure; order within a step preserved.
|
|
3848
|
+
*/
|
|
3849
|
+
function groupCustomModelsByStep(descriptors) {
|
|
3850
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
3851
|
+
for (const d of descriptors) {
|
|
3852
|
+
const arr = byStep.get(d.stepId) ?? [];
|
|
3853
|
+
arr.push(d.entry);
|
|
3854
|
+
byStep.set(d.stepId, arr);
|
|
3855
|
+
}
|
|
3856
|
+
return byStep;
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Union a step's static catalog models with operator-registered custom
|
|
3860
|
+
* models. On an `id` collision the static catalog entry wins — a custom
|
|
3861
|
+
* model can never shadow a built-in one.
|
|
3862
|
+
*
|
|
3863
|
+
* Pure + side-effect-free so it can be unit-tested in isolation and called
|
|
3864
|
+
* from the (free) `buildSchemaSlots` builder without any addon context.
|
|
3865
|
+
*/
|
|
3866
|
+
function mergeCustomModels(staticModels, customModels) {
|
|
3867
|
+
const seen = new Set(staticModels.map((m) => m.id));
|
|
3868
|
+
const merged = [...staticModels];
|
|
3869
|
+
for (const m of customModels) {
|
|
3870
|
+
if (seen.has(m.id)) continue;
|
|
3871
|
+
seen.add(m.id);
|
|
3872
|
+
merged.push(m);
|
|
3873
|
+
}
|
|
3874
|
+
return merged;
|
|
3875
|
+
}
|
|
3876
|
+
//#endregion
|
|
3793
3877
|
//#region src/detection-pipeline/provider.ts
|
|
3794
3878
|
/**
|
|
3795
3879
|
* DetectionPipelineProvider — implements IPipelineExecutorProvider.
|
|
@@ -4004,6 +4088,17 @@ var ONNX_FLOOR = {
|
|
|
4004
4088
|
* instead of duplicated inline.
|
|
4005
4089
|
*/
|
|
4006
4090
|
function onnxFloorPick() {
|
|
4091
|
+
return ONNX_FLOOR;
|
|
4092
|
+
}
|
|
4093
|
+
/**
|
|
4094
|
+
* The platform-deterministic engine pick computed SYNCHRONOUSLY from this node's
|
|
4095
|
+
* own `process.platform`/`arch` alone (no probe): darwin → coreml, else → onnx
|
|
4096
|
+
* (gpu-dependent openvino/cuda need the probe and converge via the auto-pick).
|
|
4097
|
+
* Used as the ENGINE fallback when a persisted selection is unsupported on this
|
|
4098
|
+
* node — so a stale GLOBAL engine config (e.g. the cluster's OpenVINO choice)
|
|
4099
|
+
* can never force an impossible engine onto a node whose platform rejects it.
|
|
4100
|
+
*/
|
|
4101
|
+
function platformDefaultPick() {
|
|
4007
4102
|
const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
|
|
4008
4103
|
return {
|
|
4009
4104
|
runtime: "python",
|
|
@@ -4012,6 +4107,19 @@ function onnxFloorPick() {
|
|
|
4012
4107
|
device: pick.device
|
|
4013
4108
|
};
|
|
4014
4109
|
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Is `backend` even POSSIBLE on this node's OS/arch (ignoring gpu detail)?
|
|
4112
|
+
* coreml ⇒ darwin only; openvino ⇒ x64 non-darwin only; onnx ⇒ anywhere. Used to
|
|
4113
|
+
* reject a persisted/global engine choice that the node's PLATFORM fundamentally
|
|
4114
|
+
* cannot run (e.g. the cluster's OpenVINO default landing on a Mac) — distinct
|
|
4115
|
+
* from the gpu-dependent support (linux without a probed Intel iGPU still keeps
|
|
4116
|
+
* openvino as a valid platform choice; the device falls back to cpu).
|
|
4117
|
+
*/
|
|
4118
|
+
function backendPossibleOnPlatform(backend) {
|
|
4119
|
+
if (backend === "coreml") return process.platform === "darwin";
|
|
4120
|
+
if (backend === "openvino") return process.arch === "x64" && process.platform !== "darwin";
|
|
4121
|
+
return true;
|
|
4122
|
+
}
|
|
4015
4123
|
var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
4016
4124
|
modelsDir;
|
|
4017
4125
|
eventBus;
|
|
@@ -4025,6 +4133,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4025
4133
|
/** Addon context — ctx.api resolves lazily (direct caller created after boot) */
|
|
4026
4134
|
addonCtx = null;
|
|
4027
4135
|
/**
|
|
4136
|
+
* Short-lived cache of custom models pulled from the `custom-model-registry`
|
|
4137
|
+
* collection cap, grouped by step id. TTL-bounded so the picker / resolution
|
|
4138
|
+
* paths don't issue a cap round-trip on every call. Empty map = no provider
|
|
4139
|
+
* (or a query failure) → behaviour identical to the static-catalog-only path.
|
|
4140
|
+
*/
|
|
4141
|
+
customModelsCache = null;
|
|
4142
|
+
/**
|
|
4028
4143
|
* Per-device {@link DeviceProxy} cache used for zone gating at the
|
|
4029
4144
|
* runtime path. Reads `state.zones.value` + `state.zoneRules.value`
|
|
4030
4145
|
* synchronously per frame so detections inside an `exclude` zone
|
|
@@ -4249,17 +4364,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4249
4364
|
*/
|
|
4250
4365
|
async autoPickAndPersist() {
|
|
4251
4366
|
let hardware = null;
|
|
4252
|
-
let bestBackendHint = null;
|
|
4253
4367
|
try {
|
|
4254
4368
|
const api = this.addonCtx?.api;
|
|
4255
|
-
if (api) {
|
|
4256
|
-
const caps = await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() });
|
|
4257
|
-
hardware = caps?.hardware ?? null;
|
|
4258
|
-
const bs = caps?.bestScore;
|
|
4259
|
-
if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
|
|
4260
|
-
}
|
|
4369
|
+
if (api) hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
|
|
4261
4370
|
} catch {}
|
|
4262
|
-
const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)),
|
|
4371
|
+
const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
|
|
4263
4372
|
const engine = {
|
|
4264
4373
|
runtime: "python",
|
|
4265
4374
|
backend: pick.runtimeId,
|
|
@@ -4267,8 +4376,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4267
4376
|
device: pick.device
|
|
4268
4377
|
};
|
|
4269
4378
|
this.currentEngine = engine;
|
|
4270
|
-
if (!(
|
|
4271
|
-
this.log.
|
|
4379
|
+
if (!(pick.runtimeId !== "onnx" || hardware !== null)) {
|
|
4380
|
+
this.log.info("Auto-pick: onnx floor pending gpu probe — NOT persisting (re-pick on done)", { meta: {
|
|
4272
4381
|
backend: pick.runtimeId,
|
|
4273
4382
|
device: pick.device
|
|
4274
4383
|
} });
|
|
@@ -4279,10 +4388,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4279
4388
|
[nodeEngineKey("engineBackend", apNode)]: pick.runtimeId,
|
|
4280
4389
|
[nodeEngineKey("engineDevice", apNode)]: pick.device
|
|
4281
4390
|
});
|
|
4282
|
-
this.log.info("Auto-picked engine
|
|
4391
|
+
this.log.info("Auto-picked engine (platform-deterministic)", { meta: {
|
|
4283
4392
|
backend: pick.runtimeId,
|
|
4284
4393
|
device: pick.device,
|
|
4285
|
-
|
|
4394
|
+
hadProbeHardware: hardware !== null
|
|
4286
4395
|
} });
|
|
4287
4396
|
}
|
|
4288
4397
|
/** Map a backend string to a known RuntimeId, flooring to onnx. */
|
|
@@ -4342,7 +4451,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4342
4451
|
const runtimeId = this.toRuntimeId(engine.backend);
|
|
4343
4452
|
const device = engine.device ?? "cpu";
|
|
4344
4453
|
const snapshot = this.provisioner.state;
|
|
4345
|
-
|
|
4454
|
+
const sameSelection = snapshot.runtimeId === runtimeId && snapshot.device === device;
|
|
4455
|
+
if (sameSelection && snapshot.state !== "idle") return;
|
|
4456
|
+
if (!sameSelection) this.currentSteps = null;
|
|
4346
4457
|
this.provisioner.select(runtimeId, device);
|
|
4347
4458
|
}
|
|
4348
4459
|
/**
|
|
@@ -4353,8 +4464,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4353
4464
|
* runtime through installing → verifying → ready.
|
|
4354
4465
|
*/
|
|
4355
4466
|
async onEngineSelectionChanged() {
|
|
4467
|
+
const prev = this.currentEngine;
|
|
4356
4468
|
const stored = await this.loadEngine();
|
|
4357
4469
|
if (stored) this.currentEngine = stored;
|
|
4470
|
+
if ((prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "")) && this.engineFactory) {
|
|
4471
|
+
this.log.info("engine selection changed — rebuilding pool in place", { meta: {
|
|
4472
|
+
from: `${prev.backend}/${prev.device ?? "default"}`,
|
|
4473
|
+
to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
|
|
4474
|
+
} });
|
|
4475
|
+
try {
|
|
4476
|
+
await this.engineFactory.dispose();
|
|
4477
|
+
} catch {}
|
|
4478
|
+
this.engineFactory = null;
|
|
4479
|
+
this.executor = null;
|
|
4480
|
+
}
|
|
4358
4481
|
this.startProvisioningForCurrentEngine();
|
|
4359
4482
|
}
|
|
4360
4483
|
/**
|
|
@@ -4392,7 +4515,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4392
4515
|
if (!steps || steps.length === 0) return;
|
|
4393
4516
|
for (const step of flattenSteps(steps)) {
|
|
4394
4517
|
if (!step.enabled) continue;
|
|
4395
|
-
const modelEntry =
|
|
4518
|
+
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
4396
4519
|
if (!modelEntry) continue;
|
|
4397
4520
|
if (isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
|
|
4398
4521
|
await this.downloadWithRetry(modelEntry, format, 3);
|
|
@@ -4470,10 +4593,44 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4470
4593
|
return { hardware: null };
|
|
4471
4594
|
}
|
|
4472
4595
|
}
|
|
4596
|
+
/**
|
|
4597
|
+
* Pull custom models from the `custom-model-registry` collection cap,
|
|
4598
|
+
* grouped by step id. 5s TTL cache. Fully graceful: when no provider is
|
|
4599
|
+
* registered (or the query fails) it returns an empty map and logs at most
|
|
4600
|
+
* a warning — callers then behave exactly like the static-catalog path.
|
|
4601
|
+
*/
|
|
4602
|
+
async getCustomModels() {
|
|
4603
|
+
const now = Date.now();
|
|
4604
|
+
if (this.customModelsCache && now - this.customModelsCache.at < 5e3) return this.customModelsCache.byStep;
|
|
4605
|
+
let byStep = /* @__PURE__ */ new Map();
|
|
4606
|
+
try {
|
|
4607
|
+
const api = this.addonCtx?.api;
|
|
4608
|
+
if (api) {
|
|
4609
|
+
if ((await api.addons.listCapabilityProviders.query({ capName: "custom-model-registry" })).some((p) => p.isActive)) byStep = groupCustomModelsByStep(await api.customModelRegistry.listModels.query());
|
|
4610
|
+
}
|
|
4611
|
+
} catch (err) {
|
|
4612
|
+
this.log.warn("custom-model-registry query failed — using static catalog only", { meta: { error: errMsg(err) } });
|
|
4613
|
+
}
|
|
4614
|
+
this.customModelsCache = {
|
|
4615
|
+
at: now,
|
|
4616
|
+
byStep
|
|
4617
|
+
};
|
|
4618
|
+
return byStep;
|
|
4619
|
+
}
|
|
4620
|
+
/**
|
|
4621
|
+
* Resolve a model id within a step to a catalog entry — static catalog
|
|
4622
|
+
* first, then the custom registry. Returns undefined if neither has it.
|
|
4623
|
+
*/
|
|
4624
|
+
async resolveModelEntry(addonId, modelId) {
|
|
4625
|
+
const fromCatalog = getStepDefinition(addonId).models.find((m) => m.id === modelId);
|
|
4626
|
+
if (fromCatalog) return fromCatalog;
|
|
4627
|
+
return (await this.getCustomModels()).get(addonId)?.find((m) => m.id === modelId);
|
|
4628
|
+
}
|
|
4473
4629
|
async getSchema(engine) {
|
|
4474
4630
|
if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
|
|
4475
4631
|
const format = engine.format;
|
|
4476
|
-
const
|
|
4632
|
+
const customByStep = await this.getCustomModels();
|
|
4633
|
+
const slots = buildSchemaSlots(format, this.modelsDir, customByStep);
|
|
4477
4634
|
const { hardware } = await this.fetchProbeGatingData();
|
|
4478
4635
|
const env = runtimeEnvFromProcess(toProbedHardware(hardware));
|
|
4479
4636
|
return {
|
|
@@ -4532,13 +4689,52 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4532
4689
|
async getDefaultSteps(engine) {
|
|
4533
4690
|
return buildDefaultStepTree(engine.format);
|
|
4534
4691
|
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Substitute, AT RUNTIME ONLY (never persisted), any step whose configured
|
|
4694
|
+
* model lacks a build for that step's active engine format. A GLOBAL pipeline
|
|
4695
|
+
* config can pin a model that's valid on the cluster's Intel nodes (e.g. the
|
|
4696
|
+
* OpenVINO-only `yolov9t-int8`) but impossible on a CoreML/ONNX node — without
|
|
4697
|
+
* this the node crash-loops `Model "X" has no <format> format`. The operator's
|
|
4698
|
+
* choice stays in the persisted config; this node just runs the smallest
|
|
4699
|
+
* catalog model that DOES have its format. Mirrors the per-node engine
|
|
4700
|
+
* fallback in `loadEngine`. Catalog models only — a custom model that lacks
|
|
4701
|
+
* the format is left as-is (operator's responsibility).
|
|
4702
|
+
*/
|
|
4703
|
+
substituteIncompatibleModels(steps) {
|
|
4704
|
+
const fix = (step) => {
|
|
4705
|
+
const format = step.engine?.format ?? this.currentEngine.format;
|
|
4706
|
+
let modelId = step.modelId;
|
|
4707
|
+
try {
|
|
4708
|
+
const entry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
|
|
4709
|
+
if (entry && !entry.formats[format]) {
|
|
4710
|
+
const fallback = getDefaultModelForFormat(step.addonId, format);
|
|
4711
|
+
if (fallback !== step.modelId) {
|
|
4712
|
+
this.log.info("Step model lacks engine format — substituting format default (runtime)", { meta: {
|
|
4713
|
+
step: step.addonId,
|
|
4714
|
+
configured: step.modelId,
|
|
4715
|
+
substitute: fallback,
|
|
4716
|
+
format
|
|
4717
|
+
} });
|
|
4718
|
+
modelId = fallback;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
} catch {}
|
|
4722
|
+
const children = step.children?.length ? step.children.map(fix) : step.children;
|
|
4723
|
+
return modelId === step.modelId && children === step.children ? step : {
|
|
4724
|
+
...step,
|
|
4725
|
+
modelId,
|
|
4726
|
+
...children ? { children } : {}
|
|
4727
|
+
};
|
|
4728
|
+
};
|
|
4729
|
+
return steps.map(fix);
|
|
4730
|
+
}
|
|
4535
4731
|
async getGlobalSteps() {
|
|
4536
4732
|
if (this.currentSteps) return this.currentSteps;
|
|
4537
4733
|
const raw = (await this.readStore())[KEY_STEPS];
|
|
4538
4734
|
if (!raw) {
|
|
4539
4735
|
const defaults = buildDefaultStepTree(this.currentEngine.format);
|
|
4540
4736
|
if (defaults.length === 0) return null;
|
|
4541
|
-
this.currentSteps = defaults;
|
|
4737
|
+
this.currentSteps = this.substituteIncompatibleModels(defaults);
|
|
4542
4738
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
4543
4739
|
this.log.info("Bootstrapped default pipeline — object-detection + face + plate recognition enabled by default", { meta: { rootSteps: defaults.length } });
|
|
4544
4740
|
return this.currentSteps;
|
|
@@ -4576,7 +4772,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4576
4772
|
this.log.info("Migration: added audio-classifier step to persisted pipeline config");
|
|
4577
4773
|
}
|
|
4578
4774
|
}
|
|
4579
|
-
this.currentSteps = steps;
|
|
4775
|
+
this.currentSteps = this.substituteIncompatibleModels(steps);
|
|
4580
4776
|
return this.currentSteps;
|
|
4581
4777
|
} catch {
|
|
4582
4778
|
throw new Error(`Failed to parse persisted pipeline steps: corrupt data in key "${KEY_STEPS}"`);
|
|
@@ -4660,7 +4856,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4660
4856
|
}
|
|
4661
4857
|
async downloadModel(input) {
|
|
4662
4858
|
const { modelId, format, addonId } = input;
|
|
4663
|
-
const modelEntry =
|
|
4859
|
+
const modelEntry = await this.resolveModelEntry(addonId, modelId);
|
|
4664
4860
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
|
|
4665
4861
|
const formatEntry = modelEntry.formats[format];
|
|
4666
4862
|
if (!formatEntry) throw new Error(`Model "${modelId}" has no ${format} format. Available: ${Object.keys(modelEntry.formats).join(", ")}`);
|
|
@@ -4723,7 +4919,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4723
4919
|
}
|
|
4724
4920
|
async deleteModel(input) {
|
|
4725
4921
|
const { modelId, format, addonId } = input;
|
|
4726
|
-
const modelEntry =
|
|
4922
|
+
const modelEntry = await this.resolveModelEntry(addonId, modelId);
|
|
4727
4923
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
|
|
4728
4924
|
if (!deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
|
|
4729
4925
|
this.log.info("Model deleted from disk", { meta: {
|
|
@@ -5179,7 +5375,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5179
5375
|
const work = (async () => {
|
|
5180
5376
|
const format = this.currentEngine?.format ?? "onnx";
|
|
5181
5377
|
for (const step of needed) {
|
|
5182
|
-
const modelEntry =
|
|
5378
|
+
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
5183
5379
|
if (modelEntry && !isModelDownloaded(this.modelsDir, modelEntry, format)) {
|
|
5184
5380
|
this.log.info("Downloading model for step", { meta: {
|
|
5185
5381
|
modelId: step.modelId,
|
|
@@ -5674,7 +5870,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5674
5870
|
const format = this.currentEngine.format;
|
|
5675
5871
|
const downloads = [];
|
|
5676
5872
|
for (const step of flattenSteps(steps)) {
|
|
5677
|
-
const modelEntry =
|
|
5873
|
+
const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
|
|
5678
5874
|
if (!modelEntry) {
|
|
5679
5875
|
this.log.warn("Model not found in step catalog — skipping download", { meta: {
|
|
5680
5876
|
modelId: step.modelId,
|
|
@@ -5727,12 +5923,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5727
5923
|
const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
|
|
5728
5924
|
const floor = onnxFloorPick();
|
|
5729
5925
|
const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
|
|
5730
|
-
if (!
|
|
5731
|
-
|
|
5926
|
+
if (!backendPossibleOnPlatform(migratedBackend)) {
|
|
5927
|
+
const platformDefault = platformDefaultPick();
|
|
5928
|
+
this.log.warn("Stored engine backend impossible on this platform — using platform default", { meta: {
|
|
5732
5929
|
stored: migratedBackend,
|
|
5733
|
-
fallback:
|
|
5930
|
+
fallback: platformDefault.backend
|
|
5734
5931
|
} });
|
|
5735
|
-
return
|
|
5932
|
+
return platformDefault;
|
|
5736
5933
|
}
|
|
5737
5934
|
const device = storedDevice || floor.device;
|
|
5738
5935
|
return {
|
|
@@ -5904,44 +6101,61 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5904
6101
|
}
|
|
5905
6102
|
async reprobeEngine() {
|
|
5906
6103
|
const api = this.addonCtx?.api;
|
|
5907
|
-
let
|
|
6104
|
+
let hardware = null;
|
|
5908
6105
|
if (api) try {
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
return "cpu";
|
|
5919
|
-
})();
|
|
5920
|
-
best = {
|
|
5921
|
-
runtime: "python",
|
|
5922
|
-
backend: probeBackend,
|
|
5923
|
-
format: backendToFormat(probeBackend),
|
|
5924
|
-
device: probeDevice
|
|
5925
|
-
};
|
|
5926
|
-
} else best = onnxFloorPick();
|
|
5927
|
-
} catch {
|
|
5928
|
-
best = onnxFloorPick();
|
|
5929
|
-
}
|
|
5930
|
-
else best = onnxFloorPick();
|
|
6106
|
+
hardware = (await api.platformProbe.getCapabilities.query({ nodeId: this.localProbeNodeId() }))?.hardware ?? null;
|
|
6107
|
+
} catch {}
|
|
6108
|
+
const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), null);
|
|
6109
|
+
const best = {
|
|
6110
|
+
runtime: "python",
|
|
6111
|
+
backend: pick.runtimeId,
|
|
6112
|
+
format: modelFormatFor(pick.runtimeId),
|
|
6113
|
+
device: pick.device
|
|
6114
|
+
};
|
|
5931
6115
|
const probedLabel = `${best.backend}/${best.device ?? "default"}`;
|
|
5932
6116
|
const rpNode = this.localProbeNodeId();
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
6117
|
+
if (pick.runtimeId !== "onnx" || hardware !== null) {
|
|
6118
|
+
await this.writeStore({
|
|
6119
|
+
[nodeEngineKey("probedBestEngine", rpNode)]: probedLabel,
|
|
6120
|
+
[nodeEngineKey("engineBackend", rpNode)]: best.backend,
|
|
6121
|
+
[nodeEngineKey("engineDevice", rpNode)]: best.device ?? "cpu"
|
|
6122
|
+
});
|
|
6123
|
+
this.log.info("Re-probed engine (platform-deterministic) — wrote back", { meta: {
|
|
6124
|
+
backend: best.backend,
|
|
6125
|
+
device: best.device ?? null,
|
|
6126
|
+
probedBestEngine: probedLabel
|
|
6127
|
+
} });
|
|
6128
|
+
} else this.log.info("Re-probe: onnx floor pending gpu probe — NOT persisting (re-pick on done)", { meta: {
|
|
5939
6129
|
backend: best.backend,
|
|
5940
|
-
device: best.device ?? null
|
|
5941
|
-
probedBestEngine: probedLabel
|
|
6130
|
+
device: best.device ?? null
|
|
5942
6131
|
} });
|
|
5943
6132
|
return best;
|
|
5944
6133
|
}
|
|
6134
|
+
/**
|
|
6135
|
+
* Re-pick the engine when the platform-probe finishes its async hardware +
|
|
6136
|
+
* Python detection (the `platform-probe.phase` `done` event). At boot the
|
|
6137
|
+
* probe's accelerator result may not be ready yet, so the engine floored to
|
|
6138
|
+
* onnx; once the probe answers (e.g. a Mac's CoreML/ANE surfaces after the
|
|
6139
|
+
* embedded Python is installed) this re-runs the probe-driven pick and
|
|
6140
|
+
* re-provisions. Idempotent: `startProvisioningForCurrentEngine` skips a
|
|
6141
|
+
* no-op when the selection is unchanged.
|
|
6142
|
+
*/
|
|
6143
|
+
async repickEngineOnProbeReady() {
|
|
6144
|
+
const before = `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`;
|
|
6145
|
+
await this.reprobeEngine();
|
|
6146
|
+
const stored = await this.loadEngine();
|
|
6147
|
+
if (stored) {
|
|
6148
|
+
this.currentEngine = stored;
|
|
6149
|
+
this.needsAutoPick = false;
|
|
6150
|
+
this.cancelDeferredAutoPick();
|
|
6151
|
+
}
|
|
6152
|
+
const after = `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`;
|
|
6153
|
+
if (before !== after) this.log.info("Engine re-picked after platform-probe completed", { meta: {
|
|
6154
|
+
before,
|
|
6155
|
+
after
|
|
6156
|
+
} });
|
|
6157
|
+
this.startProvisioningForCurrentEngine();
|
|
6158
|
+
}
|
|
5945
6159
|
async getReferenceAudioFiles() {
|
|
5946
6160
|
const dir = resolveReferenceAudioDir();
|
|
5947
6161
|
if (!dir) return [];
|
|
@@ -6077,11 +6291,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6077
6291
|
}
|
|
6078
6292
|
}
|
|
6079
6293
|
};
|
|
6080
|
-
function buildSchemaSlots(format, modelsDir) {
|
|
6294
|
+
function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
6081
6295
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6082
6296
|
for (const pipelineStep of ALL_PIPELINE_STEPS) {
|
|
6083
6297
|
const step = pipelineStep.definition;
|
|
6084
|
-
const availableModels = step.models.filter((m) => m.formats[format]);
|
|
6298
|
+
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format]);
|
|
6085
6299
|
if (availableModels.length === 0) continue;
|
|
6086
6300
|
const slot = step.slot;
|
|
6087
6301
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
@@ -6186,8 +6400,7 @@ function buildDefaultStepTree(format) {
|
|
|
6186
6400
|
makeStep("animal-classifier", [], { enabled: false }),
|
|
6187
6401
|
makeStep("bird-classifier", [], { enabled: false }),
|
|
6188
6402
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
6189
|
-
makeStep("segmentation
|
|
6190
|
-
makeStep("instance-segmentation", [], { enabled: false })
|
|
6403
|
+
makeStep("segmentation", [], { enabled: false })
|
|
6191
6404
|
].filter((s) => s !== null));
|
|
6192
6405
|
const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
|
|
6193
6406
|
runtime: "python",
|
|
@@ -6410,6 +6623,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6410
6623
|
nodeEngineBackend = DEFAULT_CONFIG.engineBackend;
|
|
6411
6624
|
nodeProbedBestEngine = "";
|
|
6412
6625
|
engineMetricsTimer = null;
|
|
6626
|
+
probePhaseUnsub = null;
|
|
6413
6627
|
/** Snapshot-equality cache for engine-metrics emit. Most ticks
|
|
6414
6628
|
* the engine inventory is unchanged (no model load/unload), so
|
|
6415
6629
|
* we skip the bus emit and let the heartbeat re-emit at
|
|
@@ -6477,8 +6691,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6477
6691
|
label: "Execution provider",
|
|
6478
6692
|
options: [...STATIC_BACKEND_OPTIONS],
|
|
6479
6693
|
default: DEFAULT_CONFIG.engineBackend,
|
|
6480
|
-
immediate: true
|
|
6481
|
-
requiresRestart: true
|
|
6694
|
+
immediate: true
|
|
6482
6695
|
}),
|
|
6483
6696
|
this.field({
|
|
6484
6697
|
type: "select",
|
|
@@ -6486,8 +6699,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6486
6699
|
label: "Hardware device",
|
|
6487
6700
|
options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
|
|
6488
6701
|
default: DEFAULT_CONFIG.engineDevice,
|
|
6489
|
-
immediate: true
|
|
6490
|
-
requiresRestart: true
|
|
6702
|
+
immediate: true
|
|
6491
6703
|
})
|
|
6492
6704
|
]
|
|
6493
6705
|
}, {
|
|
@@ -6526,8 +6738,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6526
6738
|
"onnx",
|
|
6527
6739
|
"cpu"
|
|
6528
6740
|
]
|
|
6529
|
-
}
|
|
6530
|
-
requiresRestart: true
|
|
6741
|
+
}
|
|
6531
6742
|
}),
|
|
6532
6743
|
this.field({
|
|
6533
6744
|
type: "slider",
|
|
@@ -6544,8 +6755,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6544
6755
|
showWhen: {
|
|
6545
6756
|
field: "batchMode",
|
|
6546
6757
|
notEquals: "none"
|
|
6547
|
-
}
|
|
6548
|
-
requiresRestart: true
|
|
6758
|
+
}
|
|
6549
6759
|
}),
|
|
6550
6760
|
this.field({
|
|
6551
6761
|
type: "slider",
|
|
@@ -6562,8 +6772,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6562
6772
|
showWhen: {
|
|
6563
6773
|
field: "batchMode",
|
|
6564
6774
|
notEquals: "none"
|
|
6565
|
-
}
|
|
6566
|
-
requiresRestart: true
|
|
6775
|
+
}
|
|
6567
6776
|
}),
|
|
6568
6777
|
this.field({
|
|
6569
6778
|
type: "slider",
|
|
@@ -6576,8 +6785,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6576
6785
|
default: void 0,
|
|
6577
6786
|
showValue: true,
|
|
6578
6787
|
nullable: true,
|
|
6579
|
-
nullLabel: "Auto"
|
|
6580
|
-
requiresRestart: true
|
|
6788
|
+
nullLabel: "Auto"
|
|
6581
6789
|
}),
|
|
6582
6790
|
this.field({
|
|
6583
6791
|
type: "slider",
|
|
@@ -6590,8 +6798,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6590
6798
|
default: void 0,
|
|
6591
6799
|
showValue: true,
|
|
6592
6800
|
nullable: true,
|
|
6593
|
-
nullLabel: "Auto"
|
|
6594
|
-
requiresRestart: true
|
|
6801
|
+
nullLabel: "Auto"
|
|
6595
6802
|
})
|
|
6596
6803
|
]
|
|
6597
6804
|
}] });
|
|
@@ -6823,11 +7030,35 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6823
7030
|
numWorkers: num(t["numWorkers"], 1)
|
|
6824
7031
|
};
|
|
6825
7032
|
}
|
|
6826
|
-
|
|
6827
|
-
|
|
7033
|
+
/**
|
|
7034
|
+
* Resolve the directory models are downloaded into, resilient across nodes.
|
|
7035
|
+
* The `models` storage location is GLOBAL (hub-seeded) and can resolve to a
|
|
7036
|
+
* path that exists only on the hub — e.g. Docker's `/data/models`, which an
|
|
7037
|
+
* agent on a different filesystem (a Mac) cannot create (`ENOENT mkdir
|
|
7038
|
+
* /data/models`). Verify the resolved dir is creatable; otherwise fall back to
|
|
7039
|
+
* this node's LOCAL addon data-dir so models always land on writable disk.
|
|
7040
|
+
*/
|
|
7041
|
+
async resolveModelsDir() {
|
|
7042
|
+
const fallback = path$1.join(this.ctx.dataDir, "models");
|
|
7043
|
+
const candidate = await this.ctx.api.storage.resolve.query({
|
|
6828
7044
|
location: "models",
|
|
6829
7045
|
relativePath: ""
|
|
6830
|
-
}).catch(() =>
|
|
7046
|
+
}).catch(() => null) ?? fallback;
|
|
7047
|
+
try {
|
|
7048
|
+
await fs.promises.mkdir(candidate, { recursive: true });
|
|
7049
|
+
return candidate;
|
|
7050
|
+
} catch (err) {
|
|
7051
|
+
this.ctx.logger.warn("models dir not creatable on this node — using local data-dir", { meta: {
|
|
7052
|
+
resolved: candidate,
|
|
7053
|
+
fallback,
|
|
7054
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7055
|
+
} });
|
|
7056
|
+
await fs.promises.mkdir(fallback, { recursive: true });
|
|
7057
|
+
return fallback;
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
async onInitialize() {
|
|
7061
|
+
const modelsDir = await this.resolveModelsDir();
|
|
6831
7062
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
6832
7063
|
await this.refreshNodeEngineFromStore();
|
|
6833
7064
|
this.pythonAddonDir = resolveAddonPythonDir();
|
|
@@ -6857,6 +7088,12 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6857
7088
|
await this.provider.ensureBootEngineProvisioned().catch((err) => {
|
|
6858
7089
|
this.ctx.logger.warn("ensureBootEngineProvisioned failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6859
7090
|
});
|
|
7091
|
+
this.probePhaseUnsub = this.ctx.eventBus?.subscribe({ category: EventCategory.PlatformProbePhase }, (event) => {
|
|
7092
|
+
if (event.data?.phase !== "done") return;
|
|
7093
|
+
this.provider.repickEngineOnProbeReady().catch((err) => {
|
|
7094
|
+
this.ctx.logger.warn("repick on platform-probe done failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7095
|
+
});
|
|
7096
|
+
}) ?? null;
|
|
6860
7097
|
await this.provider.warmPool();
|
|
6861
7098
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
6862
7099
|
this.lastAppliedPoolConfig = this.snapshotPoolConfig();
|
|
@@ -6930,6 +7167,10 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6930
7167
|
}
|
|
6931
7168
|
}
|
|
6932
7169
|
async onShutdown() {
|
|
7170
|
+
if (this.probePhaseUnsub) {
|
|
7171
|
+
this.probePhaseUnsub();
|
|
7172
|
+
this.probePhaseUnsub = null;
|
|
7173
|
+
}
|
|
6933
7174
|
if (this.engineMetricsTimer) {
|
|
6934
7175
|
clearInterval(this.engineMetricsTimer);
|
|
6935
7176
|
this.engineMetricsTimer = null;
|
|
@@ -6964,16 +7205,17 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6964
7205
|
return false;
|
|
6965
7206
|
}
|
|
6966
7207
|
/**
|
|
6967
|
-
* BaseAddon calls `onConfigChanged` after every settings write.
|
|
6968
|
-
*
|
|
6969
|
-
*
|
|
6970
|
-
*
|
|
6971
|
-
*
|
|
6972
|
-
* respawn the
|
|
6973
|
-
*
|
|
6974
|
-
*
|
|
6975
|
-
*
|
|
6976
|
-
*
|
|
7208
|
+
* BaseAddon calls `onConfigChanged` after every settings write. This is the
|
|
7209
|
+
* SOLE apply path for engine + tuning changes — none of those fields declare
|
|
7210
|
+
* `requiresRestart` anymore (an addon restart re-probes hardware + reloads
|
|
7211
|
+
* every model on every set, which is exactly what we want to avoid). Instead:
|
|
7212
|
+
* - Pool-bound tuning (`concurrency`/`batchMode`/`windowMs`/…) → in-place
|
|
7213
|
+
* pool respawn when the snapshot flips (`poolConfigChanged` below).
|
|
7214
|
+
* - Engine cascade (`engineBackend`/`engineDevice`) → the provider's
|
|
7215
|
+
* `onEngineSelectionChanged` disposes the device-bound factory so the next
|
|
7216
|
+
* runPipeline rebuilds the pool on the new selection, in place.
|
|
7217
|
+
* Both apply optimistically — the inference gate stays closed for the short
|
|
7218
|
+
* re-spin, frames are dropped (never crashed), no addon bounce.
|
|
6977
7219
|
*/
|
|
6978
7220
|
async onConfigChanged() {
|
|
6979
7221
|
await this.refreshNodeEngineFromStore();
|
|
@@ -6994,10 +7236,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6994
7236
|
} catch (err) {
|
|
6995
7237
|
this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6996
7238
|
}
|
|
6997
|
-
const modelsDir = await this.
|
|
6998
|
-
location: "models",
|
|
6999
|
-
relativePath: ""
|
|
7000
|
-
}).catch(() => "camstack-data/models");
|
|
7239
|
+
const modelsDir = await this.resolveModelsDir();
|
|
7001
7240
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available during respawn");
|
|
7002
7241
|
const effectiveTuning = this.resolveBackendTuning();
|
|
7003
7242
|
this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
|