@camstack/addon-pipeline 1.1.42 → 1.1.44
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 +5 -4
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/chunk-D6vf50IK.js +28 -0
- package/dist/detection-pipeline/index.js +535 -115
- package/dist/detection-pipeline/index.mjs +529 -110
- package/dist/{dist-GFd8M6KO.mjs → dist-CgBGV988.mjs} +214 -186
- package/dist/{dist-NvwN60Fq.js → dist-CySHUkXY.js} +219 -185
- package/dist/{model-download-service-C-IHWnXx-DxM2DSns.js → model-download-service-C-IHWnXx-BLXzxpRB.js} +3 -29
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +122 -287
- package/dist/pipeline-runner/index.mjs +118 -285
- package/dist/recorder/index.js +78 -31
- package/dist/recorder/index.mjs +75 -29
- package/dist/{frame-handle-plane-E_AsR77T.mjs → remote-source-plane-B4hRwchu.mjs} +219 -2
- package/dist/{frame-handle-plane-B3Fxww8H.js → remote-source-plane-D9m0z1N9.js} +236 -1
- package/dist/session-decode/decode-worker-child.js +118 -12
- package/dist/session-decode/decode-worker-child.mjs +115 -12
- package/dist/stream-broker/_stub.js +550 -523
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-G6wtUb3G.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUpF966l.mjs +26 -0
- package/dist/stream-broker/{hostInit-DmjqskOR.mjs → hostInit-BSzP3BIj.mjs} +2 -2
- package/dist/stream-broker/index.js +840 -196
- package/dist/stream-broker/index.mjs +831 -188
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js → MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-D43fIRgB.js → MotionZonesSettings-NcxxQN8r-TTWGGaqv.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js → PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js} +1 -1
- package/embed-dist/assets/index-C1oaL9s7.js +81 -0
- package/embed-dist/assets/{index-CSFtK41z.css → index-CA4kv43T.css} +1 -1
- package/embed-dist/index.html +2 -2
- package/package.json +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-dGka8yZY.mjs +0 -26
- package/embed-dist/assets/index-CX-rILhw.js +0 -81
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { H as createEvent, I as errMsg,
|
|
2
|
+
import { H as createEvent, I as errMsg, L as BaseAddon, O as pipelineExecutorCapability, P as supportedRuntimes$1, S as hfModelUrl, U as hydrateSchema, V as EventCategory, X as sleep, a as COCO_TO_MACRO, b as detectionPipelineCapability, f as YAMNET_TO_MACRO, i as COCO_80_LABELS, j as runtimeDevices$1, q as parseJsonUnknown, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, v as defaultDeviceFor$1, x as evaluateZoneRules } from "../dist-CgBGV988.mjs";
|
|
3
3
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
4
4
|
import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
5
5
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
@@ -1175,7 +1175,7 @@ var ovFormat = (url, sizeMB) => {
|
|
|
1175
1175
|
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
1176
1176
|
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
1177
1177
|
*/
|
|
1178
|
-
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
|
|
1178
|
+
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB, legacy = false) => ({
|
|
1179
1179
|
id: `${baseId}-${precision}`,
|
|
1180
1180
|
name: `${baseName} (${precision.toUpperCase()})`,
|
|
1181
1181
|
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
@@ -1185,7 +1185,13 @@ var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
|
|
|
1185
1185
|
},
|
|
1186
1186
|
labels: [],
|
|
1187
1187
|
preprocessMode: "letterbox",
|
|
1188
|
-
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) }
|
|
1188
|
+
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) },
|
|
1189
|
+
...legacy ? { legacy: true } : {},
|
|
1190
|
+
...precision === "int8" ? { group: {
|
|
1191
|
+
family: baseId.slice(0, -1),
|
|
1192
|
+
tier: baseId.slice(-1),
|
|
1193
|
+
precision: "int8"
|
|
1194
|
+
} } : {}
|
|
1189
1195
|
});
|
|
1190
1196
|
var MLPACKAGE_FILES = [
|
|
1191
1197
|
"Manifest.json",
|
|
@@ -1195,6 +1201,7 @@ var MLPACKAGE_FILES = [
|
|
|
1195
1201
|
var OBJECT_DETECTION_MODELS = [
|
|
1196
1202
|
{
|
|
1197
1203
|
id: "yolov9t",
|
|
1204
|
+
legacy: true,
|
|
1198
1205
|
name: "YOLOv9 Tiny",
|
|
1199
1206
|
description: "YOLOv9 Tiny — ultra-lightweight next-gen detector",
|
|
1200
1207
|
inputSize: {
|
|
@@ -1220,6 +1227,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1220
1227
|
},
|
|
1221
1228
|
{
|
|
1222
1229
|
id: "yolov9s",
|
|
1230
|
+
legacy: true,
|
|
1223
1231
|
name: "YOLOv9 Small",
|
|
1224
1232
|
description: "YOLOv9 Small — improved efficiency over YOLOv8s",
|
|
1225
1233
|
inputSize: {
|
|
@@ -1245,6 +1253,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1245
1253
|
},
|
|
1246
1254
|
{
|
|
1247
1255
|
id: "yolov9c",
|
|
1256
|
+
legacy: true,
|
|
1248
1257
|
name: "YOLOv9 C",
|
|
1249
1258
|
description: "YOLOv9 C — high-accuracy compact model",
|
|
1250
1259
|
inputSize: {
|
|
@@ -1270,6 +1279,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1270
1279
|
},
|
|
1271
1280
|
{
|
|
1272
1281
|
id: "yolo26n",
|
|
1282
|
+
group: {
|
|
1283
|
+
family: "yolo26",
|
|
1284
|
+
tier: "n"
|
|
1285
|
+
},
|
|
1273
1286
|
name: "YOLO26 Nano",
|
|
1274
1287
|
description: "YOLO26 Nano — latest generation ultra-lightweight detector with attention mechanism",
|
|
1275
1288
|
inputSize: {
|
|
@@ -1295,6 +1308,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1295
1308
|
},
|
|
1296
1309
|
{
|
|
1297
1310
|
id: "yolo26s",
|
|
1311
|
+
group: {
|
|
1312
|
+
family: "yolo26",
|
|
1313
|
+
tier: "s"
|
|
1314
|
+
},
|
|
1298
1315
|
name: "YOLO26 Small",
|
|
1299
1316
|
description: "YOLO26 Small — balanced speed/accuracy with attention-based architecture",
|
|
1300
1317
|
inputSize: {
|
|
@@ -1320,6 +1337,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1320
1337
|
},
|
|
1321
1338
|
{
|
|
1322
1339
|
id: "yolo26m",
|
|
1340
|
+
group: {
|
|
1341
|
+
family: "yolo26",
|
|
1342
|
+
tier: "m"
|
|
1343
|
+
},
|
|
1323
1344
|
name: "YOLO26 Medium",
|
|
1324
1345
|
description: "YOLO26 Medium — high accuracy general-purpose detector",
|
|
1325
1346
|
inputSize: {
|
|
@@ -1345,6 +1366,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1345
1366
|
},
|
|
1346
1367
|
{
|
|
1347
1368
|
id: "yolo26l",
|
|
1369
|
+
group: {
|
|
1370
|
+
family: "yolo26",
|
|
1371
|
+
tier: "l"
|
|
1372
|
+
},
|
|
1348
1373
|
name: "YOLO26 Large",
|
|
1349
1374
|
description: "YOLO26 Large — maximum accuracy for demanding deployments",
|
|
1350
1375
|
inputSize: {
|
|
@@ -1370,6 +1395,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1370
1395
|
},
|
|
1371
1396
|
{
|
|
1372
1397
|
id: "yolo26x",
|
|
1398
|
+
legacy: true,
|
|
1373
1399
|
name: "YOLO26 XLarge",
|
|
1374
1400
|
description: "YOLO26 XLarge — highest accuracy, attention-based architecture",
|
|
1375
1401
|
inputSize: {
|
|
@@ -1395,6 +1421,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1395
1421
|
},
|
|
1396
1422
|
{
|
|
1397
1423
|
id: "scrypted-yolov9t-relu",
|
|
1424
|
+
legacy: true,
|
|
1398
1425
|
name: "YOLOv9 Tiny ReLU",
|
|
1399
1426
|
description: "YOLOv9t ReLU — optimized for int8 quantization",
|
|
1400
1427
|
inputSize: {
|
|
@@ -1420,6 +1447,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1420
1447
|
},
|
|
1421
1448
|
{
|
|
1422
1449
|
id: "scrypted-yolov9s-relu",
|
|
1450
|
+
legacy: true,
|
|
1423
1451
|
name: "YOLOv9 Small ReLU",
|
|
1424
1452
|
description: "YOLOv9s ReLU — improved efficiency, int8 ready",
|
|
1425
1453
|
inputSize: {
|
|
@@ -1445,6 +1473,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1445
1473
|
},
|
|
1446
1474
|
{
|
|
1447
1475
|
id: "scrypted-yolov9c-relu",
|
|
1476
|
+
legacy: true,
|
|
1448
1477
|
name: "YOLOv9 C ReLU",
|
|
1449
1478
|
description: "YOLOv9c ReLU — high-accuracy, int8 ready",
|
|
1450
1479
|
inputSize: {
|
|
@@ -1470,6 +1499,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1470
1499
|
},
|
|
1471
1500
|
{
|
|
1472
1501
|
id: "scrypted-yolov9m-relu",
|
|
1502
|
+
legacy: true,
|
|
1473
1503
|
name: "YOLOv9 M ReLU",
|
|
1474
1504
|
description: "YOLOv9m ReLU — medium, int8 ready",
|
|
1475
1505
|
inputSize: {
|
|
@@ -1493,20 +1523,20 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1493
1523
|
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1494
1524
|
}
|
|
1495
1525
|
},
|
|
1496
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5),
|
|
1497
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3),
|
|
1498
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15),
|
|
1499
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8),
|
|
1500
|
-
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5),
|
|
1526
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5, true),
|
|
1527
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3, true),
|
|
1528
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15, true),
|
|
1529
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8, true),
|
|
1530
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5, true),
|
|
1501
1531
|
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
1502
|
-
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19),
|
|
1532
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19, true),
|
|
1503
1533
|
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
1504
|
-
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41),
|
|
1534
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41, true),
|
|
1505
1535
|
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
1506
|
-
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50),
|
|
1536
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50, true),
|
|
1507
1537
|
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
1508
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112),
|
|
1509
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56)
|
|
1538
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112, true),
|
|
1539
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56, true)
|
|
1510
1540
|
];
|
|
1511
1541
|
var FACE_DETECTION_MODELS = [{
|
|
1512
1542
|
id: "scrfd-2.5g",
|
|
@@ -2372,17 +2402,58 @@ function getStepDefinition(stepId) {
|
|
|
2372
2402
|
}
|
|
2373
2403
|
/**
|
|
2374
2404
|
* Get the default model ID for a step given the current model format.
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2405
|
+
*
|
|
2406
|
+
* Resolution order:
|
|
2407
|
+
* 1. `def.defaultModelIdByFormat?.[format]` — the step's declared
|
|
2408
|
+
* per-format preference — if that model exists in `def.models` AND
|
|
2409
|
+
* has a build for `format`.
|
|
2410
|
+
* 2. `def.defaultModelId` — the step's plain declared default — if it
|
|
2411
|
+
* exists in `def.models` AND has a build for `format`.
|
|
2412
|
+
* 3. The smallest-by-size model among those with a `format` build
|
|
2413
|
+
* (legacy fallback, preserved for steps/formats with no declared
|
|
2414
|
+
* preference reachable).
|
|
2415
|
+
* 4. `def.defaultModelId` unchanged, when ZERO models have a `format`
|
|
2416
|
+
* build — an unloadable case flagged elsewhere, not resolved here.
|
|
2377
2417
|
*/
|
|
2378
2418
|
function getDefaultModelForFormat(stepId, format) {
|
|
2379
2419
|
const def = getStepDefinition(stepId);
|
|
2380
|
-
const
|
|
2420
|
+
const hasFormatBuild = (modelId) => def.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
|
|
2421
|
+
const declaredForFormat = def.defaultModelIdByFormat?.[format];
|
|
2422
|
+
if (declaredForFormat !== void 0 && hasFormatBuild(declaredForFormat)) return declaredForFormat;
|
|
2423
|
+
if (hasFormatBuild(def.defaultModelId)) return def.defaultModelId;
|
|
2424
|
+
const available = def.models.filter((m) => m.formats[format] && m.legacy !== true);
|
|
2381
2425
|
if (available.length === 0) return def.defaultModelId;
|
|
2382
2426
|
return [...available].toSorted((a, b) => {
|
|
2383
2427
|
return (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity);
|
|
2384
2428
|
})[0].id;
|
|
2385
2429
|
}
|
|
2430
|
+
/**
|
|
2431
|
+
* The single node-local authority for turning a CHOSEN model (or none) into a
|
|
2432
|
+
* concrete model that has a build for this node's engine `format`. Fuses the
|
|
2433
|
+
* two previously-duplicated decisions (orchestrator `pickDefaultModelId` +
|
|
2434
|
+
* provider `substituteIncompatibleModels`):
|
|
2435
|
+
*
|
|
2436
|
+
* - chosen === null → the addon's default model for `format`.
|
|
2437
|
+
* - chosen is a catalog model → keep it if it has a `format` build; else
|
|
2438
|
+
* the addon's default model for `format`
|
|
2439
|
+
* (a substitution — the caller surfaces it).
|
|
2440
|
+
* - chosen is NOT in the catalog (custom/unknown) → returned unchanged; the
|
|
2441
|
+
* operator owns a custom model's format compat.
|
|
2442
|
+
* - unknown stepId → chosen returned unchanged (never throws).
|
|
2443
|
+
*/
|
|
2444
|
+
function resolveModelForFormat(stepId, chosenModelId, format) {
|
|
2445
|
+
let def;
|
|
2446
|
+
try {
|
|
2447
|
+
def = getStepDefinition(stepId);
|
|
2448
|
+
} catch {
|
|
2449
|
+
return chosenModelId ?? "";
|
|
2450
|
+
}
|
|
2451
|
+
if (chosenModelId === null) return getDefaultModelForFormat(stepId, format);
|
|
2452
|
+
const entry = def.models.find((m) => m.id === chosenModelId);
|
|
2453
|
+
if (!entry) return chosenModelId;
|
|
2454
|
+
if (entry.formats[format]) return chosenModelId;
|
|
2455
|
+
return getDefaultModelForFormat(stepId, format);
|
|
2456
|
+
}
|
|
2386
2457
|
//#endregion
|
|
2387
2458
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
2388
2459
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
@@ -3928,12 +3999,15 @@ var PipelineExecutor = class {
|
|
|
3928
3999
|
try {
|
|
3929
4000
|
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
|
|
3930
4001
|
} catch (err) {
|
|
3931
|
-
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
4002
|
+
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
4003
|
+
tags: { deviceId },
|
|
4004
|
+
meta: {
|
|
4005
|
+
rootStepId: rootStep.stepId,
|
|
4006
|
+
parentClass: mutable.macroClass,
|
|
4007
|
+
parentScore: mutable.score,
|
|
4008
|
+
error: errMsg(err)
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
3937
4011
|
}
|
|
3938
4012
|
firstLevel.push(mutable);
|
|
3939
4013
|
}
|
|
@@ -4198,6 +4272,147 @@ function mergeCustomModels(staticModels, customModels) {
|
|
|
4198
4272
|
return merged;
|
|
4199
4273
|
}
|
|
4200
4274
|
//#endregion
|
|
4275
|
+
//#region src/detection-pipeline/step-resolution.ts
|
|
4276
|
+
/**
|
|
4277
|
+
* Walk `steps` (recursing into children) and return exactly the
|
|
4278
|
+
* substitutions in effect for `format` — one entry per step whose chosen
|
|
4279
|
+
* model differs from its resolved model. Pure and deterministic: the same
|
|
4280
|
+
* `(steps, format)` always yields the same result, with no entries left over
|
|
4281
|
+
* from a prior format. Callers that cache substitutions (e.g. for a status
|
|
4282
|
+
* surface) should REBUILD their cache from this result on every resolve
|
|
4283
|
+
* pass rather than merging into it, so a step that becomes compatible again
|
|
4284
|
+
* (chosen === resolved) is dropped instead of going stale.
|
|
4285
|
+
*/
|
|
4286
|
+
function collectSubstitutions(steps, format) {
|
|
4287
|
+
const result = [];
|
|
4288
|
+
for (const step of steps) {
|
|
4289
|
+
const chosen = step.modelId;
|
|
4290
|
+
if (chosen != null) {
|
|
4291
|
+
const running = resolveModelForFormat(step.addonId, chosen, format);
|
|
4292
|
+
if (running !== chosen) result.push({
|
|
4293
|
+
addonId: step.addonId,
|
|
4294
|
+
chosen,
|
|
4295
|
+
running,
|
|
4296
|
+
format
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
if (step.children?.length) result.push(...collectSubstitutions(step.children, format));
|
|
4300
|
+
}
|
|
4301
|
+
return result;
|
|
4302
|
+
}
|
|
4303
|
+
/**
|
|
4304
|
+
* Return one {@link ZeroBuildIssue} per step in `steps` whose catalog entry
|
|
4305
|
+
* (via `getStepDef`) has ZERO models with a build for `format`. `steps` is
|
|
4306
|
+
* expected to already be flattened + filtered to the set the caller cares
|
|
4307
|
+
* about (e.g. enabled video steps) — this does not recurse into children or
|
|
4308
|
+
* filter by `enabled`, unlike {@link collectSubstitutions}.
|
|
4309
|
+
*
|
|
4310
|
+
* `getStepDef` defaults to the real catalog lookup and is injectable so
|
|
4311
|
+
* tests can exercise the zero-build branch with a fabricated
|
|
4312
|
+
* `StepDefinition` — the real catalog currently has no step whose EVERY
|
|
4313
|
+
* model lacks a given format, so this branch is otherwise unreachable in a
|
|
4314
|
+
* black-box test. Unknown/custom addonIds (no registered `StepDefinition`)
|
|
4315
|
+
* are skipped, never thrown.
|
|
4316
|
+
*/
|
|
4317
|
+
function collectZeroBuildIssues(steps, format, getStepDef = getStepDefinition) {
|
|
4318
|
+
const result = [];
|
|
4319
|
+
for (const step of steps) {
|
|
4320
|
+
let def;
|
|
4321
|
+
try {
|
|
4322
|
+
def = getStepDef(step.addonId);
|
|
4323
|
+
} catch {
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (!def.models.some((m) => m.formats[format])) result.push({
|
|
4327
|
+
addonId: step.addonId,
|
|
4328
|
+
format
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
return result;
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Walk `steps` (recursing into children, mirroring {@link collectSubstitutions}'
|
|
4335
|
+
* recursion — every step in the tree is checked regardless of `enabled`) and
|
|
4336
|
+
* return one {@link UnknownAddonIssue} per addonId whose `getStepDef` lookup
|
|
4337
|
+
* throws. Pure, deterministic, never throws itself.
|
|
4338
|
+
*
|
|
4339
|
+
* `getStepDef` defaults to the real catalog lookup and is injectable so
|
|
4340
|
+
* tests can exercise the unknown-addon branch with a fabricated resolver
|
|
4341
|
+
* without `vi.mock`.
|
|
4342
|
+
*/
|
|
4343
|
+
function collectUnknownAddonIssues(steps, getStepDef = getStepDefinition) {
|
|
4344
|
+
const result = [];
|
|
4345
|
+
for (const step of steps) {
|
|
4346
|
+
try {
|
|
4347
|
+
getStepDef(step.addonId);
|
|
4348
|
+
} catch {
|
|
4349
|
+
result.push({ addonId: step.addonId });
|
|
4350
|
+
}
|
|
4351
|
+
if (step.children?.length) result.push(...collectUnknownAddonIssues(step.children, getStepDef));
|
|
4352
|
+
}
|
|
4353
|
+
return result;
|
|
4354
|
+
}
|
|
4355
|
+
/**
|
|
4356
|
+
* Enrich `PipelineStepInput[]` (the wire shape a camera pipeline sends) into
|
|
4357
|
+
* `PipelineDefaultStep[]` (the catalog-enriched shape the executor consumes),
|
|
4358
|
+
* resolving each step's model for `format` via `resolveModelForFormat`.
|
|
4359
|
+
*
|
|
4360
|
+
* Pure and side-effect-free — never throws. This is the seam behind the
|
|
4361
|
+
* LIVE per-camera dispatch path (`DetectionPipelineProvider.
|
|
4362
|
+
* inputStepsToPipelineSteps`), which runs once per decoded frame (up to
|
|
4363
|
+
* detectionFps × N cameras/sec): a bad step config referencing an unknown
|
|
4364
|
+
* `addonId` must never become a per-frame throw. Unknown addons are
|
|
4365
|
+
* dropped from the returned tree and reported in
|
|
4366
|
+
* `diagnostics.unknownAddonIds`; model substitutions are reported in
|
|
4367
|
+
* `diagnostics.substitutions` instead of happening silently. The caller
|
|
4368
|
+
* (the provider) owns logging + dedup for both.
|
|
4369
|
+
*/
|
|
4370
|
+
function resolveInputSteps(steps, format) {
|
|
4371
|
+
const resolvedSteps = [];
|
|
4372
|
+
const unknownAddonIds = [];
|
|
4373
|
+
const substitutions = [];
|
|
4374
|
+
for (const s of steps) {
|
|
4375
|
+
let def;
|
|
4376
|
+
try {
|
|
4377
|
+
def = getStepDefinition(s.addonId);
|
|
4378
|
+
} catch {
|
|
4379
|
+
unknownAddonIds.push(s.addonId);
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
const chosenModelId = s.modelId ?? null;
|
|
4383
|
+
const runningModelId = resolveModelForFormat(s.addonId, chosenModelId, format);
|
|
4384
|
+
if (chosenModelId !== null && runningModelId !== chosenModelId) substitutions.push({
|
|
4385
|
+
addonId: s.addonId,
|
|
4386
|
+
chosen: chosenModelId,
|
|
4387
|
+
running: runningModelId,
|
|
4388
|
+
format
|
|
4389
|
+
});
|
|
4390
|
+
const childResult = s.children ? resolveInputSteps(s.children, format) : null;
|
|
4391
|
+
if (childResult) {
|
|
4392
|
+
unknownAddonIds.push(...childResult.diagnostics.unknownAddonIds);
|
|
4393
|
+
substitutions.push(...childResult.diagnostics.substitutions);
|
|
4394
|
+
}
|
|
4395
|
+
resolvedSteps.push({
|
|
4396
|
+
addonId: s.addonId,
|
|
4397
|
+
addonName: def.name,
|
|
4398
|
+
slot: def.slot,
|
|
4399
|
+
inputClasses: def.inputClasses ?? [],
|
|
4400
|
+
outputClasses: def.outputClasses,
|
|
4401
|
+
enabled: s.enabled,
|
|
4402
|
+
modelId: runningModelId,
|
|
4403
|
+
children: childResult ? childResult.steps : [],
|
|
4404
|
+
...s.settings ? { settings: { ...s.settings } } : {}
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
return {
|
|
4408
|
+
steps: resolvedSteps,
|
|
4409
|
+
diagnostics: {
|
|
4410
|
+
unknownAddonIds,
|
|
4411
|
+
substitutions
|
|
4412
|
+
}
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
//#endregion
|
|
4201
4416
|
//#region src/detection-pipeline/provider.ts
|
|
4202
4417
|
/**
|
|
4203
4418
|
* DetectionPipelineProvider — implements IPipelineExecutorProvider.
|
|
@@ -4494,6 +4709,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4494
4709
|
*/
|
|
4495
4710
|
lastStepsSignature = null;
|
|
4496
4711
|
/**
|
|
4712
|
+
* Dedup guard for the live per-camera dispatch path
|
|
4713
|
+
* (`inputStepsToPipelineSteps`) — an unknown-addon skip or a runtime
|
|
4714
|
+
* model substitution logs ONCE per distinct signature instead of once
|
|
4715
|
+
* per decoded frame (that method runs up to detectionFps × N cameras
|
|
4716
|
+
* times/sec). Never cleared — it's a log-noise guard, not runtime
|
|
4717
|
+
* state, and the key space (addonId/model combos) is small and stable.
|
|
4718
|
+
*/
|
|
4719
|
+
loggedLiveDispatchIssues = /* @__PURE__ */ new Set();
|
|
4720
|
+
/**
|
|
4497
4721
|
* CB5 shm passthrough — the process-local, LONG-LIVED reader cache for
|
|
4498
4722
|
* `runPipeline({ frameHandle })`. Opens each named shm segment ONCE and
|
|
4499
4723
|
* reuses the mapping for every later handle on the same ring (never
|
|
@@ -4522,6 +4746,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4522
4746
|
*/
|
|
4523
4747
|
provisioner = null;
|
|
4524
4748
|
/**
|
|
4749
|
+
* Gate A (config-correctness gate at engine change). Human-readable config
|
|
4750
|
+
* issues — model substitutions and zero-build steps — for the CURRENT
|
|
4751
|
+
* node-default tree against `this.currentEngine.format`. Recomputed
|
|
4752
|
+
* EAGERLY by {@link recomputeConfigIssues} whenever the engine actually
|
|
4753
|
+
* changes (see `onEngineSelectionChanged`), rather than relying on a
|
|
4754
|
+
* `getGlobalSteps()` call that may never happen on a camera-only node.
|
|
4755
|
+
* Merged into {@link getEngineProvisioning}'s snapshot; never consulted by
|
|
4756
|
+
* `assertEngineReady` — this gate SURFACES, it does not block inference.
|
|
4757
|
+
*/
|
|
4758
|
+
configIssues = [];
|
|
4759
|
+
/**
|
|
4525
4760
|
* True when `init()` found no persisted engine choice (first boot).
|
|
4526
4761
|
* `setApi()` reads this flag and auto-picks the best supported runtime
|
|
4527
4762
|
* using probe data (hardware + bestScore hint), persists the selection,
|
|
@@ -4768,7 +5003,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4768
5003
|
const stored = await this.loadEngine();
|
|
4769
5004
|
if (stored) this.currentEngine = stored;
|
|
4770
5005
|
else await this.resolveAutoEngine();
|
|
4771
|
-
|
|
5006
|
+
const engineChanged = prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "");
|
|
5007
|
+
if (engineChanged && this.engineFactory) {
|
|
4772
5008
|
this.log.info("engine selection changed — rebuilding pool in place", { meta: {
|
|
4773
5009
|
from: `${prev.backend}/${prev.device ?? "default"}`,
|
|
4774
5010
|
to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
|
|
@@ -4780,18 +5016,56 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4780
5016
|
this.executor = null;
|
|
4781
5017
|
}
|
|
4782
5018
|
this.startProvisioningForCurrentEngine();
|
|
5019
|
+
if (engineChanged) await this.recomputeConfigIssues();
|
|
5020
|
+
}
|
|
5021
|
+
/**
|
|
5022
|
+
* Gate A (config-correctness gate at engine change) — eagerly re-resolve
|
|
5023
|
+
* the node-default step tree for `this.currentEngine.format` and compute
|
|
5024
|
+
* human-readable config issues from the result:
|
|
5025
|
+
* - model substitutions (`getActiveModelSubstitutions()`, populated as a
|
|
5026
|
+
* side effect of the `getGlobalSteps()` re-resolve below) — a step's
|
|
5027
|
+
* chosen model lacks a build for this format, so the format default
|
|
5028
|
+
* runs instead (degraded but loadable).
|
|
5029
|
+
* - zero-build steps (`collectZeroBuildIssues`) — a step whose FULL
|
|
5030
|
+
* catalog has no model at all with a build for this format (not even
|
|
5031
|
+
* the format default is loadable).
|
|
5032
|
+
*
|
|
5033
|
+
* Never throws — this is an additive diagnostic surface, not an
|
|
5034
|
+
* enforcement point (`assertEngineReady` still owns readiness). Any
|
|
5035
|
+
* failure clears `configIssues` and logs a warning rather than
|
|
5036
|
+
* propagating out of the engine-change path.
|
|
5037
|
+
*/
|
|
5038
|
+
async recomputeConfigIssues() {
|
|
5039
|
+
this.configIssues = [];
|
|
5040
|
+
try {
|
|
5041
|
+
const steps = await this.getGlobalSteps();
|
|
5042
|
+
if (!steps) return;
|
|
5043
|
+
const format = this.currentEngine.format;
|
|
5044
|
+
const substitutionIssues = this.getActiveModelSubstitutions().map((s) => `${s.addonId}: chose ${s.chosen}, running ${s.running} (${s.format})`);
|
|
5045
|
+
const zeroBuildIssues = collectZeroBuildIssues(flattenSteps(steps), format).map((i) => `${i.addonId}: no model has a ${i.format} build`);
|
|
5046
|
+
this.configIssues = [...substitutionIssues, ...zeroBuildIssues];
|
|
5047
|
+
} catch (err) {
|
|
5048
|
+
this.configIssues = [];
|
|
5049
|
+
this.log.warn("engine-change config gate failed — no config issues surfaced", { meta: { error: errMsg(err) } });
|
|
5050
|
+
}
|
|
4783
5051
|
}
|
|
4784
5052
|
/**
|
|
4785
5053
|
* Per-node engine-provisioning snapshot. The cap routes `{ nodeId }` for
|
|
4786
5054
|
* provider resolution only (the router strips it); each node returns its
|
|
4787
|
-
* own local machine state. Cold (pre-`setApi`) provider → `idle`.
|
|
5055
|
+
* own local machine state. Cold (pre-`setApi`) provider → `idle`. Merges
|
|
5056
|
+
* in Gate A's `configIssues` (see {@link recomputeConfigIssues}) when
|
|
5057
|
+
* non-empty — additive, never overrides the provisioner's own fields.
|
|
4788
5058
|
*/
|
|
4789
5059
|
getEngineProvisioning() {
|
|
4790
|
-
|
|
5060
|
+
const base = this.provisioner?.state ?? {
|
|
4791
5061
|
runtimeId: null,
|
|
4792
5062
|
device: null,
|
|
4793
5063
|
state: "idle"
|
|
4794
5064
|
};
|
|
5065
|
+
return this.configIssues.length ? {
|
|
5066
|
+
...base,
|
|
5067
|
+
configIssues: [...this.configIssues]
|
|
5068
|
+
} : base;
|
|
4795
5069
|
}
|
|
4796
5070
|
/**
|
|
4797
5071
|
* Inference gate. Rejects with the current phase/error unless the engine
|
|
@@ -4990,43 +5264,45 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4990
5264
|
return buildDefaultStepTree(engine.format);
|
|
4991
5265
|
}
|
|
4992
5266
|
/**
|
|
4993
|
-
*
|
|
4994
|
-
*
|
|
4995
|
-
*
|
|
4996
|
-
*
|
|
4997
|
-
*
|
|
4998
|
-
*
|
|
4999
|
-
*
|
|
5000
|
-
*
|
|
5001
|
-
*
|
|
5267
|
+
* Resolve every step's model for this node's engine format via the shared
|
|
5268
|
+
* `resolveModelForFormat`, and surface any substitution. `step.engine?.format`
|
|
5269
|
+
* is gone — `this.currentEngine.format` is the only format source (engine is
|
|
5270
|
+
* node-local).
|
|
5271
|
+
*
|
|
5272
|
+
* Rebuilds `modelSubstitutions` from scratch (via {@link collectSubstitutions})
|
|
5273
|
+
* on every call, from the FULL tree being resolved, so the map always
|
|
5274
|
+
* reflects EXACTLY the substitutions in effect for the current tree+format —
|
|
5275
|
+
* a step whose chosen model becomes compatible again (e.g. after the
|
|
5276
|
+
* operator switches the engine to a format it supports) is cleared here
|
|
5277
|
+
* instead of leaking a stale "chose X → running Y" entry.
|
|
5002
5278
|
*/
|
|
5003
|
-
|
|
5004
|
-
const
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
} catch {}
|
|
5022
|
-
const children = step.children?.length ? step.children.map(fix) : step.children;
|
|
5023
|
-
return modelId === step.modelId && children === step.children ? step : {
|
|
5279
|
+
resolveStepModels(steps) {
|
|
5280
|
+
const format = this.currentEngine.format;
|
|
5281
|
+
const substitutions = collectSubstitutions(steps, format);
|
|
5282
|
+
this.modelSubstitutions = new Map(substitutions.map((s) => [s.addonId, s]));
|
|
5283
|
+
for (const s of substitutions) this.log.info("Step model lacks engine format — running format default (node-local)", { meta: {
|
|
5284
|
+
step: s.addonId,
|
|
5285
|
+
chosen: s.chosen,
|
|
5286
|
+
running: s.running,
|
|
5287
|
+
format: s.format
|
|
5288
|
+
} });
|
|
5289
|
+
return this.resolveStepModelTree(steps, format);
|
|
5290
|
+
}
|
|
5291
|
+
/** Pure recursive resolver — builds the resolved step tree without touching `modelSubstitutions`. */
|
|
5292
|
+
resolveStepModelTree(steps, format) {
|
|
5293
|
+
return steps.map((step) => {
|
|
5294
|
+
const resolved = resolveModelForFormat(step.addonId, step.modelId ?? null, format);
|
|
5295
|
+
const children = step.children?.length ? this.resolveStepModelTree([...step.children], format) : step.children;
|
|
5296
|
+
return resolved === step.modelId && children === step.children ? step : {
|
|
5024
5297
|
...step,
|
|
5025
|
-
modelId,
|
|
5298
|
+
modelId: resolved,
|
|
5026
5299
|
...children ? { children } : {}
|
|
5027
5300
|
};
|
|
5028
|
-
};
|
|
5029
|
-
|
|
5301
|
+
});
|
|
5302
|
+
}
|
|
5303
|
+
modelSubstitutions = /* @__PURE__ */ new Map();
|
|
5304
|
+
getActiveModelSubstitutions() {
|
|
5305
|
+
return [...this.modelSubstitutions.values()];
|
|
5030
5306
|
}
|
|
5031
5307
|
async getGlobalSteps() {
|
|
5032
5308
|
if (this.currentSteps) return this.currentSteps;
|
|
@@ -5034,7 +5310,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5034
5310
|
if (!raw) {
|
|
5035
5311
|
const defaults = buildDefaultStepTree(this.currentEngine.format);
|
|
5036
5312
|
if (defaults.length === 0) return null;
|
|
5037
|
-
this.currentSteps = this.
|
|
5313
|
+
this.currentSteps = this.resolveStepModels(defaults);
|
|
5038
5314
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
5039
5315
|
this.log.info("Bootstrapped default pipeline — object-detection + face + plate recognition enabled by default", { meta: { rootSteps: defaults.length } });
|
|
5040
5316
|
return this.currentSteps;
|
|
@@ -5047,15 +5323,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5047
5323
|
const audioDef = getStepDefinition("audio-classifier");
|
|
5048
5324
|
if (audioDef.models.some((m) => m.formats[format])) {
|
|
5049
5325
|
const modelId = getDefaultModelForFormat("audio-classifier", format);
|
|
5050
|
-
const audioEngine = modelId === "apple-soundanalysis" ? {
|
|
5051
|
-
runtime: "python",
|
|
5052
|
-
backend: "coreml",
|
|
5053
|
-
format: "coreml"
|
|
5054
|
-
} : {
|
|
5055
|
-
runtime: "python",
|
|
5056
|
-
backend: "cpu",
|
|
5057
|
-
format: "onnx"
|
|
5058
|
-
};
|
|
5059
5326
|
const audioStep = {
|
|
5060
5327
|
addonId: audioDef.id,
|
|
5061
5328
|
addonName: audioDef.name,
|
|
@@ -5064,15 +5331,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5064
5331
|
outputClasses: audioDef.outputClasses,
|
|
5065
5332
|
enabled: true,
|
|
5066
5333
|
modelId,
|
|
5067
|
-
children: []
|
|
5068
|
-
engine: audioEngine
|
|
5334
|
+
children: []
|
|
5069
5335
|
};
|
|
5070
5336
|
steps = [...steps, audioStep];
|
|
5071
5337
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(steps) });
|
|
5072
5338
|
this.log.info("Migration: added audio-classifier step to persisted pipeline config");
|
|
5073
5339
|
}
|
|
5074
5340
|
}
|
|
5075
|
-
this.currentSteps = this.
|
|
5341
|
+
this.currentSteps = this.resolveStepModels(steps);
|
|
5076
5342
|
return this.currentSteps;
|
|
5077
5343
|
} catch {
|
|
5078
5344
|
throw new Error(`Failed to parse persisted pipeline steps: corrupt data in key "${KEY_STEPS}"`);
|
|
@@ -5082,6 +5348,128 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5082
5348
|
if (!this.currentSteps) return null;
|
|
5083
5349
|
return stepsToPipelineConfig(this.currentSteps, this.currentEngine);
|
|
5084
5350
|
}
|
|
5351
|
+
/**
|
|
5352
|
+
* Task G5 (Gate B) — pre-init config-correctness gate. PURE COMPUTE
|
|
5353
|
+
* against `this.currentEngine.format`: resolves `steps` the same way the
|
|
5354
|
+
* runtime dispatch path (`resolveInputSteps`) would and reports what
|
|
5355
|
+
* WOULD happen, without touching `this.modelSubstitutions` (that node-
|
|
5356
|
+
* global map stays the exclusive property of `resolveStepModels`, the
|
|
5357
|
+
* node-default tree resolver — writing it here would race two
|
|
5358
|
+
* independent callers over the same field). Called by the orchestrator
|
|
5359
|
+
* at attach time, node-pinned to the target node.
|
|
5360
|
+
*
|
|
5361
|
+
* Composes three pure helpers from `step-resolution.ts`, ALL scoped to
|
|
5362
|
+
* ENABLED steps only (a disabled step's subtree — and any unknown addonId
|
|
5363
|
+
* or incompatible model inside it — contributes NO issue and NO
|
|
5364
|
+
* substitution; it will never execute, so flagging it is false-positive
|
|
5365
|
+
* noise):
|
|
5366
|
+
* - `collectSubstitutions` → `substitutions` (informational, never
|
|
5367
|
+
* affects `ok`).
|
|
5368
|
+
* - `collectUnknownAddonIssues` → `unknown-addon` HARD issues.
|
|
5369
|
+
* - `collectZeroBuildIssues` (over the enabled, non-audio-classifier
|
|
5370
|
+
* subset — mirrors Gate A's `flattenSteps`/`flattenEnabledVideoSteps`
|
|
5371
|
+
* filter, since audio backends are validated by audio-analyzer, not
|
|
5372
|
+
* against this node's VIDEO engine format) → `no-format-build` HARD
|
|
5373
|
+
* issues.
|
|
5374
|
+
*
|
|
5375
|
+
* `collectSubstitutions` and `collectUnknownAddonIssues` recurse into
|
|
5376
|
+
* `children` themselves, so the enabled-scoping is applied ONCE up front
|
|
5377
|
+
* via {@link pruneToEnabledSteps} (which drops a disabled step's whole
|
|
5378
|
+
* subtree) rather than re-implemented inside each collector.
|
|
5379
|
+
*
|
|
5380
|
+
* Never throws — any unexpected failure degrades to a clean `ok: true`
|
|
5381
|
+
* result with a warning logged, same posture as `recomputeConfigIssues`.
|
|
5382
|
+
*/
|
|
5383
|
+
async validatePipeline(input) {
|
|
5384
|
+
const format = this.currentEngine.format;
|
|
5385
|
+
try {
|
|
5386
|
+
const steps = input.steps;
|
|
5387
|
+
const enabledSteps = this.pruneToEnabledSteps(steps);
|
|
5388
|
+
const substitutions = collectSubstitutions(enabledSteps, format);
|
|
5389
|
+
const unknownAddonIssues = collectUnknownAddonIssues(enabledSteps);
|
|
5390
|
+
const zeroBuildIssues = collectZeroBuildIssues(this.flattenEnabledVideoStepInputs(steps), format);
|
|
5391
|
+
const issues = [...unknownAddonIssues.map((u) => ({
|
|
5392
|
+
addonId: u.addonId,
|
|
5393
|
+
kind: "unknown-addon",
|
|
5394
|
+
detail: `${u.addonId}: no registered step definition`
|
|
5395
|
+
})), ...zeroBuildIssues.map((z) => ({
|
|
5396
|
+
addonId: z.addonId,
|
|
5397
|
+
kind: "no-format-build",
|
|
5398
|
+
detail: `${z.addonId}: no model has a ${z.format} build`
|
|
5399
|
+
}))];
|
|
5400
|
+
return {
|
|
5401
|
+
ok: issues.length === 0,
|
|
5402
|
+
issues,
|
|
5403
|
+
substitutions,
|
|
5404
|
+
format
|
|
5405
|
+
};
|
|
5406
|
+
} catch (err) {
|
|
5407
|
+
this.log.warn("validatePipeline failed — reporting a clean result", { meta: { error: errMsg(err) } });
|
|
5408
|
+
return {
|
|
5409
|
+
ok: true,
|
|
5410
|
+
issues: [],
|
|
5411
|
+
substitutions: [],
|
|
5412
|
+
format
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
/**
|
|
5417
|
+
* Prune `steps` to ENABLED steps only, recursing into ENABLED children —
|
|
5418
|
+
* a disabled step's whole subtree is dropped, never partially kept. This
|
|
5419
|
+
* is the enabled-only tree shape `collectSubstitutions` and
|
|
5420
|
+
* `collectUnknownAddonIssues` need in {@link validatePipeline}: both
|
|
5421
|
+
* recurse into `children` themselves (unlike `flattenEnabledVideoStepInputs`,
|
|
5422
|
+
* which flattens AND excludes `audio-classifier`), so pruning once up
|
|
5423
|
+
* front — without touching the audio/video split — is enough to make
|
|
5424
|
+
* Gate B's three checks agree on scope. Pure; returns the original array
|
|
5425
|
+
* reference when nothing changed.
|
|
5426
|
+
*/
|
|
5427
|
+
pruneToEnabledSteps(steps) {
|
|
5428
|
+
const result = [];
|
|
5429
|
+
let changed = false;
|
|
5430
|
+
for (const step of steps) {
|
|
5431
|
+
if (!step.enabled) {
|
|
5432
|
+
changed = true;
|
|
5433
|
+
continue;
|
|
5434
|
+
}
|
|
5435
|
+
const prunedChildren = step.children?.length ? this.pruneToEnabledSteps(step.children) : step.children;
|
|
5436
|
+
if (prunedChildren !== step.children) {
|
|
5437
|
+
changed = true;
|
|
5438
|
+
result.push({
|
|
5439
|
+
...step,
|
|
5440
|
+
children: prunedChildren
|
|
5441
|
+
});
|
|
5442
|
+
} else result.push(step);
|
|
5443
|
+
}
|
|
5444
|
+
return changed ? result : steps;
|
|
5445
|
+
}
|
|
5446
|
+
/**
|
|
5447
|
+
* Flatten `steps` (recursing into ENABLED children only — a disabled
|
|
5448
|
+
* step's subtree is skipped entirely, mirroring
|
|
5449
|
+
* `flattenEnabledVideoSteps`) into `{addonId}` entries, excluding
|
|
5450
|
+
* `audio-classifier` steps: Gate B validates the VIDEO engine format
|
|
5451
|
+
* (`this.currentEngine.format`) only — audio backends resolve their own
|
|
5452
|
+
* format independently and are validated by the audio-analyzer addon.
|
|
5453
|
+
* An addonId whose `StepDefinition` lookup throws is skipped here (it is
|
|
5454
|
+
* already reported separately by `collectUnknownAddonIssues`).
|
|
5455
|
+
*/
|
|
5456
|
+
flattenEnabledVideoStepInputs(steps) {
|
|
5457
|
+
const result = [];
|
|
5458
|
+
const walk = (nodes) => {
|
|
5459
|
+
for (const step of nodes) {
|
|
5460
|
+
if (!step.enabled) continue;
|
|
5461
|
+
try {
|
|
5462
|
+
if (getStepDefinition(step.addonId).slot === "audio-classifier") continue;
|
|
5463
|
+
} catch {
|
|
5464
|
+
continue;
|
|
5465
|
+
}
|
|
5466
|
+
result.push({ addonId: step.addonId });
|
|
5467
|
+
if (step.children?.length) walk(step.children);
|
|
5468
|
+
}
|
|
5469
|
+
};
|
|
5470
|
+
walk(steps);
|
|
5471
|
+
return result;
|
|
5472
|
+
}
|
|
5085
5473
|
async getSelectedEngine() {
|
|
5086
5474
|
return this.currentEngine;
|
|
5087
5475
|
}
|
|
@@ -5094,7 +5482,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5094
5482
|
const addons = ALL_PIPELINE_STEPS.map((s) => ({
|
|
5095
5483
|
id: s.definition.id,
|
|
5096
5484
|
name: s.definition.name,
|
|
5097
|
-
models: s.definition.models.map((m) => {
|
|
5485
|
+
models: s.definition.models.filter((m) => m.legacy !== true).map((m) => {
|
|
5098
5486
|
const formats = {};
|
|
5099
5487
|
for (const [formatKey, entry] of Object.entries(m.formats)) {
|
|
5100
5488
|
if (!entry) continue;
|
|
@@ -5408,7 +5796,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5408
5796
|
emit(`Image: ${imageWidth}×${imageHeight}`);
|
|
5409
5797
|
}
|
|
5410
5798
|
decodeMs = performance.now() - decodeT0;
|
|
5411
|
-
const
|
|
5799
|
+
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
5800
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat);
|
|
5412
5801
|
const stepsSignature = JSON.stringify(benchmarkSteps.map((s) => ({
|
|
5413
5802
|
id: s.addonId,
|
|
5414
5803
|
settings: s.settings ?? {}
|
|
@@ -5697,22 +6086,46 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5697
6086
|
this.log.warn("Override cache dispose failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
5698
6087
|
}
|
|
5699
6088
|
}
|
|
5700
|
-
/**
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
6089
|
+
/**
|
|
6090
|
+
* Convert PipelineStepInput[] → PipelineDefaultStep[] by enriching from
|
|
6091
|
+
* StepDefinitions. This is the LIVE per-camera dispatch path — it runs
|
|
6092
|
+
* once per decoded frame (up to detectionFps × N cameras/sec) — so it
|
|
6093
|
+
* must NEVER throw. A thin adapter over the pure {@link resolveInputSteps}
|
|
6094
|
+
* seam: an unknown `addonId` (a bad step config sent by a camera
|
|
6095
|
+
* pipeline) is dropped from the tree and logged once (deduped, not
|
|
6096
|
+
* per-frame) at warn level instead of propagating as a per-frame error
|
|
6097
|
+
* loop. A runtime model substitution (the step's explicit model lacks a
|
|
6098
|
+
* build for this node's engine format, so the format default runs
|
|
6099
|
+
* instead) is logged once (deduped) at info level so an operator can see
|
|
6100
|
+
* a camera×node model swap instead of it happening silently. Does NOT
|
|
6101
|
+
* write `this.modelSubstitutions` — that map is owned exclusively by
|
|
6102
|
+
* `resolveStepModels` (see its doc comment); a second writer here would
|
|
6103
|
+
* corrupt it.
|
|
6104
|
+
*
|
|
6105
|
+
* `format` defaults to `this.currentEngine.format` (the LIVE per-camera
|
|
6106
|
+
* dispatch path never passes it explicitly). Benchmark call sites with a
|
|
6107
|
+
* per-run engine override (`input.engine`) MUST pass the override's format
|
|
6108
|
+
* explicitly — `this.currentEngine` only gets swapped to the override
|
|
6109
|
+
* LATER in `runPipeline`/`runPipelineBatch`, so relying on the default here
|
|
6110
|
+
* would resolve models against the node's persisted format instead of the
|
|
6111
|
+
* format actually being benchmarked.
|
|
6112
|
+
*/
|
|
6113
|
+
inputStepsToPipelineSteps(steps, format = this.currentEngine.format) {
|
|
6114
|
+
const { steps: resolvedSteps, diagnostics } = resolveInputSteps(steps, format);
|
|
6115
|
+
for (const addonId of diagnostics.unknownAddonIds) this.logLiveDispatchIssueOnce(`unknown:${addonId}`, () => this.log.warn("Live pipeline step references an unknown addon — dropping step", { meta: { addonId } }));
|
|
6116
|
+
for (const sub of diagnostics.substitutions) this.logLiveDispatchIssueOnce(`${sub.addonId}|${sub.chosen}|${sub.running}|${sub.format}`, () => this.log.info("Live pipeline step model substituted for engine format (node-local)", { meta: {
|
|
6117
|
+
step: sub.addonId,
|
|
6118
|
+
chosen: sub.chosen,
|
|
6119
|
+
running: sub.running,
|
|
6120
|
+
format: sub.format
|
|
6121
|
+
} }));
|
|
6122
|
+
return resolvedSteps;
|
|
6123
|
+
}
|
|
6124
|
+
/** Run `fn()` at most once per `key` for the lifetime of this provider instance. */
|
|
6125
|
+
logLiveDispatchIssueOnce(key, fn) {
|
|
6126
|
+
if (this.loggedLiveDispatchIssues.has(key)) return;
|
|
6127
|
+
this.loggedLiveDispatchIssues.add(key);
|
|
6128
|
+
fn();
|
|
5716
6129
|
}
|
|
5717
6130
|
/**
|
|
5718
6131
|
* Single-flight gate around `ensureModelsForSteps`. Concurrent
|
|
@@ -5918,7 +6331,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5918
6331
|
}
|
|
5919
6332
|
async runPipelineBatchImpl(input) {
|
|
5920
6333
|
if (input.frames.length === 0) return { results: [] };
|
|
5921
|
-
const
|
|
6334
|
+
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
6335
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat);
|
|
5922
6336
|
const enabledSteps = flattenEnabledVideoSteps(benchmarkSteps);
|
|
5923
6337
|
if (enabledSteps.length === 0) throw new Error("runPipelineBatch: no enabled steps");
|
|
5924
6338
|
const rootStep = enabledSteps[0];
|
|
@@ -6610,7 +7024,7 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
|
6610
7024
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6611
7025
|
for (const pipelineStep of ALL_PIPELINE_STEPS) {
|
|
6612
7026
|
const step = pipelineStep.definition;
|
|
6613
|
-
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format]);
|
|
7027
|
+
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format] && m.legacy !== true);
|
|
6614
7028
|
if (availableModels.length === 0) continue;
|
|
6615
7029
|
const slot = step.slot;
|
|
6616
7030
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
@@ -6628,7 +7042,9 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
|
6628
7042
|
formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
|
|
6629
7043
|
downloaded: isModelDownloaded(modelsDir, m, f),
|
|
6630
7044
|
sizeMB: entry.sizeMB
|
|
6631
|
-
}]))
|
|
7045
|
+
}])),
|
|
7046
|
+
...m.group ? { group: m.group } : {},
|
|
7047
|
+
...m.legacy !== void 0 ? { legacy: m.legacy } : {}
|
|
6632
7048
|
})),
|
|
6633
7049
|
defaultModelId: step.defaultModelId,
|
|
6634
7050
|
...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},
|
|
@@ -6717,23 +7133,10 @@ function buildDefaultStepTree(format) {
|
|
|
6717
7133
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
6718
7134
|
makeStep("segmentation", [], { enabled: false })
|
|
6719
7135
|
].filter((s) => s !== null));
|
|
6720
|
-
const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
|
|
6721
|
-
runtime: "python",
|
|
6722
|
-
backend: "coreml",
|
|
6723
|
-
format: "coreml"
|
|
6724
|
-
} : {
|
|
6725
|
-
runtime: "python",
|
|
6726
|
-
backend: "cpu",
|
|
6727
|
-
format: "onnx"
|
|
6728
|
-
};
|
|
6729
7136
|
const audioStep = makeStep("audio-classifier", []);
|
|
6730
|
-
const audioClassifier = audioStep ? {
|
|
6731
|
-
...audioStep,
|
|
6732
|
-
engine: audioEngine
|
|
6733
|
-
} : null;
|
|
6734
7137
|
const steps = [];
|
|
6735
7138
|
if (root) steps.push(root);
|
|
6736
|
-
if (
|
|
7139
|
+
if (audioStep) steps.push(audioStep);
|
|
6737
7140
|
return steps;
|
|
6738
7141
|
}
|
|
6739
7142
|
function stepsToPipelineConfig(steps, engine) {
|
|
@@ -6753,7 +7156,7 @@ function stepsToPipelineConfig(steps, engine) {
|
|
|
6753
7156
|
video: videoSteps.map(toNode),
|
|
6754
7157
|
...audioStep ? {
|
|
6755
7158
|
audio: toNode(audioStep),
|
|
6756
|
-
audioEngine:
|
|
7159
|
+
audioEngine: engine
|
|
6757
7160
|
} : {}
|
|
6758
7161
|
};
|
|
6759
7162
|
}
|
|
@@ -6885,7 +7288,8 @@ var DEFAULT_CONFIG = {
|
|
|
6885
7288
|
engineBackend: "auto",
|
|
6886
7289
|
engineDevice: "cpu",
|
|
6887
7290
|
probedBestEngine: "",
|
|
6888
|
-
activeEngine: ""
|
|
7291
|
+
activeEngine: "",
|
|
7292
|
+
activeModelSubstitutions: ""
|
|
6889
7293
|
};
|
|
6890
7294
|
/** Derive the model-format from a backend value. Called by the provider. */
|
|
6891
7295
|
function backendToFormat(backend) {
|
|
@@ -7006,6 +7410,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7006
7410
|
readonlyField: true,
|
|
7007
7411
|
default: ""
|
|
7008
7412
|
}),
|
|
7413
|
+
this.field({
|
|
7414
|
+
type: "text",
|
|
7415
|
+
key: "activeModelSubstitutions",
|
|
7416
|
+
label: "Model substitutions",
|
|
7417
|
+
description: "Node-local model substitutions currently in effect: a step's chosen model has no build for this node's engine format, so a compatible default is running instead (format: \"step: chosen→running\"). Hidden when every step matches.",
|
|
7418
|
+
readonlyField: true,
|
|
7419
|
+
default: "",
|
|
7420
|
+
showWhen: {
|
|
7421
|
+
field: "activeModelSubstitutions",
|
|
7422
|
+
notEquals: ""
|
|
7423
|
+
}
|
|
7424
|
+
}),
|
|
7009
7425
|
this.field({
|
|
7010
7426
|
type: "select",
|
|
7011
7427
|
key: "engineBackend",
|
|
@@ -7175,12 +7591,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7175
7591
|
const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
|
|
7176
7592
|
const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
|
|
7177
7593
|
const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
|
|
7594
|
+
const substitutions = this.provider?.getActiveModelSubstitutions() ?? [];
|
|
7595
|
+
const activeModelSubstitutions = substitutions.length === 0 ? "" : substitutions.map((s) => `${s.addonId}: ${s.chosen}→${s.running}`).join(", ");
|
|
7178
7596
|
const displayBackend = isAuto ? "auto" : backend;
|
|
7179
7597
|
const raw = {
|
|
7180
7598
|
...merged,
|
|
7181
7599
|
engineBackend: displayBackend,
|
|
7182
7600
|
engineDevice: device,
|
|
7183
|
-
activeEngine
|
|
7601
|
+
activeEngine,
|
|
7602
|
+
activeModelSubstitutions
|
|
7184
7603
|
};
|
|
7185
7604
|
const schema = this.globalSettingsSchema();
|
|
7186
7605
|
if (!schema) return { sections: [] };
|
|
@@ -7444,7 +7863,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7444
7863
|
}).catch(() => {});
|
|
7445
7864
|
}
|
|
7446
7865
|
getModelCatalog() {
|
|
7447
|
-
return ALL_STEPS.flatMap((s) =>
|
|
7866
|
+
return ALL_STEPS.flatMap((s) => s.models.filter((m) => m.legacy !== true));
|
|
7448
7867
|
}
|
|
7449
7868
|
/**
|
|
7450
7869
|
* Proactively install the OpenVINO Python package when Intel hardware
|