@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
|
@@ -2,19 +2,20 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
6
|
+
const require_dist = require("../dist-CySHUkXY.js");
|
|
7
7
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
8
|
+
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
|
|
8
9
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
9
10
|
let node_child_process = require("node:child_process");
|
|
10
11
|
let node_fs = require("node:fs");
|
|
11
|
-
node_fs =
|
|
12
|
+
node_fs = require_chunk.__toESM(node_fs);
|
|
12
13
|
let node_path = require("node:path");
|
|
13
|
-
node_path =
|
|
14
|
+
node_path = require_chunk.__toESM(node_path);
|
|
14
15
|
let node_os = require("node:os");
|
|
15
|
-
node_os =
|
|
16
|
+
node_os = require_chunk.__toESM(node_os);
|
|
16
17
|
let sharp = require("sharp");
|
|
17
|
-
sharp =
|
|
18
|
+
sharp = require_chunk.__toESM(sharp);
|
|
18
19
|
//#region src/detection-pipeline/runtimes.ts
|
|
19
20
|
var KNOWN_PLATFORMS = [
|
|
20
21
|
"darwin",
|
|
@@ -1182,7 +1183,7 @@ var ovFormat = (url, sizeMB) => {
|
|
|
1182
1183
|
* `camstack-<id>-<precision>.xml`. Lets an operator scale the model to the node
|
|
1183
1184
|
* (e.g. yolo26x-int8 on a 265K, yolo26n-int8 on an N100).
|
|
1184
1185
|
*/
|
|
1185
|
-
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
|
|
1186
|
+
var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB, legacy = false) => ({
|
|
1186
1187
|
id: `${baseId}-${precision}`,
|
|
1187
1188
|
name: `${baseName} (${precision.toUpperCase()})`,
|
|
1188
1189
|
description: `${baseName} — OpenVINO ${precision.toUpperCase()} variant for Intel iGPU/NPU; scale by hardware`,
|
|
@@ -1192,7 +1193,13 @@ var ovPrecisionVariant = (baseId, ovDir, baseName, precision, sizeMB) => ({
|
|
|
1192
1193
|
},
|
|
1193
1194
|
labels: [],
|
|
1194
1195
|
preprocessMode: "letterbox",
|
|
1195
|
-
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) }
|
|
1196
|
+
formats: { openvino: ovFormat(hf(`${ovDir}/camstack-${baseId}-${precision}.xml`), sizeMB) },
|
|
1197
|
+
...legacy ? { legacy: true } : {},
|
|
1198
|
+
...precision === "int8" ? { group: {
|
|
1199
|
+
family: baseId.slice(0, -1),
|
|
1200
|
+
tier: baseId.slice(-1),
|
|
1201
|
+
precision: "int8"
|
|
1202
|
+
} } : {}
|
|
1196
1203
|
});
|
|
1197
1204
|
var MLPACKAGE_FILES = [
|
|
1198
1205
|
"Manifest.json",
|
|
@@ -1202,6 +1209,7 @@ var MLPACKAGE_FILES = [
|
|
|
1202
1209
|
var OBJECT_DETECTION_MODELS = [
|
|
1203
1210
|
{
|
|
1204
1211
|
id: "yolov9t",
|
|
1212
|
+
legacy: true,
|
|
1205
1213
|
name: "YOLOv9 Tiny",
|
|
1206
1214
|
description: "YOLOv9 Tiny — ultra-lightweight next-gen detector",
|
|
1207
1215
|
inputSize: {
|
|
@@ -1227,6 +1235,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1227
1235
|
},
|
|
1228
1236
|
{
|
|
1229
1237
|
id: "yolov9s",
|
|
1238
|
+
legacy: true,
|
|
1230
1239
|
name: "YOLOv9 Small",
|
|
1231
1240
|
description: "YOLOv9 Small — improved efficiency over YOLOv8s",
|
|
1232
1241
|
inputSize: {
|
|
@@ -1252,6 +1261,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1252
1261
|
},
|
|
1253
1262
|
{
|
|
1254
1263
|
id: "yolov9c",
|
|
1264
|
+
legacy: true,
|
|
1255
1265
|
name: "YOLOv9 C",
|
|
1256
1266
|
description: "YOLOv9 C — high-accuracy compact model",
|
|
1257
1267
|
inputSize: {
|
|
@@ -1277,6 +1287,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1277
1287
|
},
|
|
1278
1288
|
{
|
|
1279
1289
|
id: "yolo26n",
|
|
1290
|
+
group: {
|
|
1291
|
+
family: "yolo26",
|
|
1292
|
+
tier: "n"
|
|
1293
|
+
},
|
|
1280
1294
|
name: "YOLO26 Nano",
|
|
1281
1295
|
description: "YOLO26 Nano — latest generation ultra-lightweight detector with attention mechanism",
|
|
1282
1296
|
inputSize: {
|
|
@@ -1302,6 +1316,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1302
1316
|
},
|
|
1303
1317
|
{
|
|
1304
1318
|
id: "yolo26s",
|
|
1319
|
+
group: {
|
|
1320
|
+
family: "yolo26",
|
|
1321
|
+
tier: "s"
|
|
1322
|
+
},
|
|
1305
1323
|
name: "YOLO26 Small",
|
|
1306
1324
|
description: "YOLO26 Small — balanced speed/accuracy with attention-based architecture",
|
|
1307
1325
|
inputSize: {
|
|
@@ -1327,6 +1345,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1327
1345
|
},
|
|
1328
1346
|
{
|
|
1329
1347
|
id: "yolo26m",
|
|
1348
|
+
group: {
|
|
1349
|
+
family: "yolo26",
|
|
1350
|
+
tier: "m"
|
|
1351
|
+
},
|
|
1330
1352
|
name: "YOLO26 Medium",
|
|
1331
1353
|
description: "YOLO26 Medium — high accuracy general-purpose detector",
|
|
1332
1354
|
inputSize: {
|
|
@@ -1352,6 +1374,10 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1352
1374
|
},
|
|
1353
1375
|
{
|
|
1354
1376
|
id: "yolo26l",
|
|
1377
|
+
group: {
|
|
1378
|
+
family: "yolo26",
|
|
1379
|
+
tier: "l"
|
|
1380
|
+
},
|
|
1355
1381
|
name: "YOLO26 Large",
|
|
1356
1382
|
description: "YOLO26 Large — maximum accuracy for demanding deployments",
|
|
1357
1383
|
inputSize: {
|
|
@@ -1377,6 +1403,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1377
1403
|
},
|
|
1378
1404
|
{
|
|
1379
1405
|
id: "yolo26x",
|
|
1406
|
+
legacy: true,
|
|
1380
1407
|
name: "YOLO26 XLarge",
|
|
1381
1408
|
description: "YOLO26 XLarge — highest accuracy, attention-based architecture",
|
|
1382
1409
|
inputSize: {
|
|
@@ -1402,6 +1429,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1402
1429
|
},
|
|
1403
1430
|
{
|
|
1404
1431
|
id: "scrypted-yolov9t-relu",
|
|
1432
|
+
legacy: true,
|
|
1405
1433
|
name: "YOLOv9 Tiny ReLU",
|
|
1406
1434
|
description: "YOLOv9t ReLU — optimized for int8 quantization",
|
|
1407
1435
|
inputSize: {
|
|
@@ -1427,6 +1455,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1427
1455
|
},
|
|
1428
1456
|
{
|
|
1429
1457
|
id: "scrypted-yolov9s-relu",
|
|
1458
|
+
legacy: true,
|
|
1430
1459
|
name: "YOLOv9 Small ReLU",
|
|
1431
1460
|
description: "YOLOv9s ReLU — improved efficiency, int8 ready",
|
|
1432
1461
|
inputSize: {
|
|
@@ -1452,6 +1481,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1452
1481
|
},
|
|
1453
1482
|
{
|
|
1454
1483
|
id: "scrypted-yolov9c-relu",
|
|
1484
|
+
legacy: true,
|
|
1455
1485
|
name: "YOLOv9 C ReLU",
|
|
1456
1486
|
description: "YOLOv9c ReLU — high-accuracy, int8 ready",
|
|
1457
1487
|
inputSize: {
|
|
@@ -1477,6 +1507,7 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1477
1507
|
},
|
|
1478
1508
|
{
|
|
1479
1509
|
id: "scrypted-yolov9m-relu",
|
|
1510
|
+
legacy: true,
|
|
1480
1511
|
name: "YOLOv9 M ReLU",
|
|
1481
1512
|
description: "YOLOv9m ReLU — medium, int8 ready",
|
|
1482
1513
|
inputSize: {
|
|
@@ -1500,20 +1531,20 @@ var OBJECT_DETECTION_MODELS = [
|
|
|
1500
1531
|
openvino: ovFormat(hf("objectDetection/scrypted-yolov9-relu/openvino/scrypted_yolov9m_relu.xml"), 38)
|
|
1501
1532
|
}
|
|
1502
1533
|
},
|
|
1503
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5),
|
|
1504
|
-
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3),
|
|
1505
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15),
|
|
1506
|
-
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8),
|
|
1507
|
-
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5),
|
|
1534
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "fp16", 5, true),
|
|
1535
|
+
ovPrecisionVariant("yolov9t", "objectDetection/yolov9/openvino", "YOLOv9 Tiny", "int8", 3, true),
|
|
1536
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "fp16", 15, true),
|
|
1537
|
+
ovPrecisionVariant("yolov9s", "objectDetection/yolov9/openvino", "YOLOv9 Small", "int8", 8, true),
|
|
1538
|
+
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "fp16", 5, true),
|
|
1508
1539
|
ovPrecisionVariant("yolo26n", "objectDetection/yolo26/openvino", "YOLO26 Nano", "int8", 3),
|
|
1509
|
-
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19),
|
|
1540
|
+
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "fp16", 19, true),
|
|
1510
1541
|
ovPrecisionVariant("yolo26s", "objectDetection/yolo26/openvino", "YOLO26 Small", "int8", 10),
|
|
1511
|
-
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41),
|
|
1542
|
+
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "fp16", 41, true),
|
|
1512
1543
|
ovPrecisionVariant("yolo26m", "objectDetection/yolo26/openvino", "YOLO26 Medium", "int8", 21),
|
|
1513
|
-
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50),
|
|
1544
|
+
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "fp16", 50, true),
|
|
1514
1545
|
ovPrecisionVariant("yolo26l", "objectDetection/yolo26/openvino", "YOLO26 Large", "int8", 25),
|
|
1515
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112),
|
|
1516
|
-
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56)
|
|
1546
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "fp16", 112, true),
|
|
1547
|
+
ovPrecisionVariant("yolo26x", "objectDetection/yolo26/openvino", "YOLO26 XLarge", "int8", 56, true)
|
|
1517
1548
|
];
|
|
1518
1549
|
var FACE_DETECTION_MODELS = [{
|
|
1519
1550
|
id: "scrfd-2.5g",
|
|
@@ -2379,17 +2410,58 @@ function getStepDefinition(stepId) {
|
|
|
2379
2410
|
}
|
|
2380
2411
|
/**
|
|
2381
2412
|
* Get the default model ID for a step given the current model format.
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2413
|
+
*
|
|
2414
|
+
* Resolution order:
|
|
2415
|
+
* 1. `def.defaultModelIdByFormat?.[format]` — the step's declared
|
|
2416
|
+
* per-format preference — if that model exists in `def.models` AND
|
|
2417
|
+
* has a build for `format`.
|
|
2418
|
+
* 2. `def.defaultModelId` — the step's plain declared default — if it
|
|
2419
|
+
* exists in `def.models` AND has a build for `format`.
|
|
2420
|
+
* 3. The smallest-by-size model among those with a `format` build
|
|
2421
|
+
* (legacy fallback, preserved for steps/formats with no declared
|
|
2422
|
+
* preference reachable).
|
|
2423
|
+
* 4. `def.defaultModelId` unchanged, when ZERO models have a `format`
|
|
2424
|
+
* build — an unloadable case flagged elsewhere, not resolved here.
|
|
2384
2425
|
*/
|
|
2385
2426
|
function getDefaultModelForFormat(stepId, format) {
|
|
2386
2427
|
const def = getStepDefinition(stepId);
|
|
2387
|
-
const
|
|
2428
|
+
const hasFormatBuild = (modelId) => def.models.find((m) => m.id === modelId)?.formats[format] !== void 0;
|
|
2429
|
+
const declaredForFormat = def.defaultModelIdByFormat?.[format];
|
|
2430
|
+
if (declaredForFormat !== void 0 && hasFormatBuild(declaredForFormat)) return declaredForFormat;
|
|
2431
|
+
if (hasFormatBuild(def.defaultModelId)) return def.defaultModelId;
|
|
2432
|
+
const available = def.models.filter((m) => m.formats[format] && m.legacy !== true);
|
|
2388
2433
|
if (available.length === 0) return def.defaultModelId;
|
|
2389
2434
|
return [...available].toSorted((a, b) => {
|
|
2390
2435
|
return (a.formats[format]?.sizeMB ?? Infinity) - (b.formats[format]?.sizeMB ?? Infinity);
|
|
2391
2436
|
})[0].id;
|
|
2392
2437
|
}
|
|
2438
|
+
/**
|
|
2439
|
+
* The single node-local authority for turning a CHOSEN model (or none) into a
|
|
2440
|
+
* concrete model that has a build for this node's engine `format`. Fuses the
|
|
2441
|
+
* two previously-duplicated decisions (orchestrator `pickDefaultModelId` +
|
|
2442
|
+
* provider `substituteIncompatibleModels`):
|
|
2443
|
+
*
|
|
2444
|
+
* - chosen === null → the addon's default model for `format`.
|
|
2445
|
+
* - chosen is a catalog model → keep it if it has a `format` build; else
|
|
2446
|
+
* the addon's default model for `format`
|
|
2447
|
+
* (a substitution — the caller surfaces it).
|
|
2448
|
+
* - chosen is NOT in the catalog (custom/unknown) → returned unchanged; the
|
|
2449
|
+
* operator owns a custom model's format compat.
|
|
2450
|
+
* - unknown stepId → chosen returned unchanged (never throws).
|
|
2451
|
+
*/
|
|
2452
|
+
function resolveModelForFormat(stepId, chosenModelId, format) {
|
|
2453
|
+
let def;
|
|
2454
|
+
try {
|
|
2455
|
+
def = getStepDefinition(stepId);
|
|
2456
|
+
} catch {
|
|
2457
|
+
return chosenModelId ?? "";
|
|
2458
|
+
}
|
|
2459
|
+
if (chosenModelId === null) return getDefaultModelForFormat(stepId, format);
|
|
2460
|
+
const entry = def.models.find((m) => m.id === chosenModelId);
|
|
2461
|
+
if (!entry) return chosenModelId;
|
|
2462
|
+
if (entry.formats[format]) return chosenModelId;
|
|
2463
|
+
return getDefaultModelForFormat(stepId, format);
|
|
2464
|
+
}
|
|
2393
2465
|
//#endregion
|
|
2394
2466
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
2395
2467
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
@@ -3935,12 +4007,15 @@ var PipelineExecutor = class {
|
|
|
3935
4007
|
try {
|
|
3936
4008
|
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg);
|
|
3937
4009
|
} catch (err) {
|
|
3938
|
-
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4010
|
+
this.opts.logger?.warn("Pipeline child execution failed — keeping parent detection", {
|
|
4011
|
+
tags: { deviceId },
|
|
4012
|
+
meta: {
|
|
4013
|
+
rootStepId: rootStep.stepId,
|
|
4014
|
+
parentClass: mutable.macroClass,
|
|
4015
|
+
parentScore: mutable.score,
|
|
4016
|
+
error: require_dist.errMsg(err)
|
|
4017
|
+
}
|
|
4018
|
+
});
|
|
3944
4019
|
}
|
|
3945
4020
|
firstLevel.push(mutable);
|
|
3946
4021
|
}
|
|
@@ -4205,6 +4280,147 @@ function mergeCustomModels(staticModels, customModels) {
|
|
|
4205
4280
|
return merged;
|
|
4206
4281
|
}
|
|
4207
4282
|
//#endregion
|
|
4283
|
+
//#region src/detection-pipeline/step-resolution.ts
|
|
4284
|
+
/**
|
|
4285
|
+
* Walk `steps` (recursing into children) and return exactly the
|
|
4286
|
+
* substitutions in effect for `format` — one entry per step whose chosen
|
|
4287
|
+
* model differs from its resolved model. Pure and deterministic: the same
|
|
4288
|
+
* `(steps, format)` always yields the same result, with no entries left over
|
|
4289
|
+
* from a prior format. Callers that cache substitutions (e.g. for a status
|
|
4290
|
+
* surface) should REBUILD their cache from this result on every resolve
|
|
4291
|
+
* pass rather than merging into it, so a step that becomes compatible again
|
|
4292
|
+
* (chosen === resolved) is dropped instead of going stale.
|
|
4293
|
+
*/
|
|
4294
|
+
function collectSubstitutions(steps, format) {
|
|
4295
|
+
const result = [];
|
|
4296
|
+
for (const step of steps) {
|
|
4297
|
+
const chosen = step.modelId;
|
|
4298
|
+
if (chosen != null) {
|
|
4299
|
+
const running = resolveModelForFormat(step.addonId, chosen, format);
|
|
4300
|
+
if (running !== chosen) result.push({
|
|
4301
|
+
addonId: step.addonId,
|
|
4302
|
+
chosen,
|
|
4303
|
+
running,
|
|
4304
|
+
format
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
4307
|
+
if (step.children?.length) result.push(...collectSubstitutions(step.children, format));
|
|
4308
|
+
}
|
|
4309
|
+
return result;
|
|
4310
|
+
}
|
|
4311
|
+
/**
|
|
4312
|
+
* Return one {@link ZeroBuildIssue} per step in `steps` whose catalog entry
|
|
4313
|
+
* (via `getStepDef`) has ZERO models with a build for `format`. `steps` is
|
|
4314
|
+
* expected to already be flattened + filtered to the set the caller cares
|
|
4315
|
+
* about (e.g. enabled video steps) — this does not recurse into children or
|
|
4316
|
+
* filter by `enabled`, unlike {@link collectSubstitutions}.
|
|
4317
|
+
*
|
|
4318
|
+
* `getStepDef` defaults to the real catalog lookup and is injectable so
|
|
4319
|
+
* tests can exercise the zero-build branch with a fabricated
|
|
4320
|
+
* `StepDefinition` — the real catalog currently has no step whose EVERY
|
|
4321
|
+
* model lacks a given format, so this branch is otherwise unreachable in a
|
|
4322
|
+
* black-box test. Unknown/custom addonIds (no registered `StepDefinition`)
|
|
4323
|
+
* are skipped, never thrown.
|
|
4324
|
+
*/
|
|
4325
|
+
function collectZeroBuildIssues(steps, format, getStepDef = getStepDefinition) {
|
|
4326
|
+
const result = [];
|
|
4327
|
+
for (const step of steps) {
|
|
4328
|
+
let def;
|
|
4329
|
+
try {
|
|
4330
|
+
def = getStepDef(step.addonId);
|
|
4331
|
+
} catch {
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
if (!def.models.some((m) => m.formats[format])) result.push({
|
|
4335
|
+
addonId: step.addonId,
|
|
4336
|
+
format
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
return result;
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Walk `steps` (recursing into children, mirroring {@link collectSubstitutions}'
|
|
4343
|
+
* recursion — every step in the tree is checked regardless of `enabled`) and
|
|
4344
|
+
* return one {@link UnknownAddonIssue} per addonId whose `getStepDef` lookup
|
|
4345
|
+
* throws. Pure, deterministic, never throws itself.
|
|
4346
|
+
*
|
|
4347
|
+
* `getStepDef` defaults to the real catalog lookup and is injectable so
|
|
4348
|
+
* tests can exercise the unknown-addon branch with a fabricated resolver
|
|
4349
|
+
* without `vi.mock`.
|
|
4350
|
+
*/
|
|
4351
|
+
function collectUnknownAddonIssues(steps, getStepDef = getStepDefinition) {
|
|
4352
|
+
const result = [];
|
|
4353
|
+
for (const step of steps) {
|
|
4354
|
+
try {
|
|
4355
|
+
getStepDef(step.addonId);
|
|
4356
|
+
} catch {
|
|
4357
|
+
result.push({ addonId: step.addonId });
|
|
4358
|
+
}
|
|
4359
|
+
if (step.children?.length) result.push(...collectUnknownAddonIssues(step.children, getStepDef));
|
|
4360
|
+
}
|
|
4361
|
+
return result;
|
|
4362
|
+
}
|
|
4363
|
+
/**
|
|
4364
|
+
* Enrich `PipelineStepInput[]` (the wire shape a camera pipeline sends) into
|
|
4365
|
+
* `PipelineDefaultStep[]` (the catalog-enriched shape the executor consumes),
|
|
4366
|
+
* resolving each step's model for `format` via `resolveModelForFormat`.
|
|
4367
|
+
*
|
|
4368
|
+
* Pure and side-effect-free — never throws. This is the seam behind the
|
|
4369
|
+
* LIVE per-camera dispatch path (`DetectionPipelineProvider.
|
|
4370
|
+
* inputStepsToPipelineSteps`), which runs once per decoded frame (up to
|
|
4371
|
+
* detectionFps × N cameras/sec): a bad step config referencing an unknown
|
|
4372
|
+
* `addonId` must never become a per-frame throw. Unknown addons are
|
|
4373
|
+
* dropped from the returned tree and reported in
|
|
4374
|
+
* `diagnostics.unknownAddonIds`; model substitutions are reported in
|
|
4375
|
+
* `diagnostics.substitutions` instead of happening silently. The caller
|
|
4376
|
+
* (the provider) owns logging + dedup for both.
|
|
4377
|
+
*/
|
|
4378
|
+
function resolveInputSteps(steps, format) {
|
|
4379
|
+
const resolvedSteps = [];
|
|
4380
|
+
const unknownAddonIds = [];
|
|
4381
|
+
const substitutions = [];
|
|
4382
|
+
for (const s of steps) {
|
|
4383
|
+
let def;
|
|
4384
|
+
try {
|
|
4385
|
+
def = getStepDefinition(s.addonId);
|
|
4386
|
+
} catch {
|
|
4387
|
+
unknownAddonIds.push(s.addonId);
|
|
4388
|
+
continue;
|
|
4389
|
+
}
|
|
4390
|
+
const chosenModelId = s.modelId ?? null;
|
|
4391
|
+
const runningModelId = resolveModelForFormat(s.addonId, chosenModelId, format);
|
|
4392
|
+
if (chosenModelId !== null && runningModelId !== chosenModelId) substitutions.push({
|
|
4393
|
+
addonId: s.addonId,
|
|
4394
|
+
chosen: chosenModelId,
|
|
4395
|
+
running: runningModelId,
|
|
4396
|
+
format
|
|
4397
|
+
});
|
|
4398
|
+
const childResult = s.children ? resolveInputSteps(s.children, format) : null;
|
|
4399
|
+
if (childResult) {
|
|
4400
|
+
unknownAddonIds.push(...childResult.diagnostics.unknownAddonIds);
|
|
4401
|
+
substitutions.push(...childResult.diagnostics.substitutions);
|
|
4402
|
+
}
|
|
4403
|
+
resolvedSteps.push({
|
|
4404
|
+
addonId: s.addonId,
|
|
4405
|
+
addonName: def.name,
|
|
4406
|
+
slot: def.slot,
|
|
4407
|
+
inputClasses: def.inputClasses ?? [],
|
|
4408
|
+
outputClasses: def.outputClasses,
|
|
4409
|
+
enabled: s.enabled,
|
|
4410
|
+
modelId: runningModelId,
|
|
4411
|
+
children: childResult ? childResult.steps : [],
|
|
4412
|
+
...s.settings ? { settings: { ...s.settings } } : {}
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
return {
|
|
4416
|
+
steps: resolvedSteps,
|
|
4417
|
+
diagnostics: {
|
|
4418
|
+
unknownAddonIds,
|
|
4419
|
+
substitutions
|
|
4420
|
+
}
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
//#endregion
|
|
4208
4424
|
//#region src/detection-pipeline/provider.ts
|
|
4209
4425
|
/**
|
|
4210
4426
|
* DetectionPipelineProvider — implements IPipelineExecutorProvider.
|
|
@@ -4501,6 +4717,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4501
4717
|
*/
|
|
4502
4718
|
lastStepsSignature = null;
|
|
4503
4719
|
/**
|
|
4720
|
+
* Dedup guard for the live per-camera dispatch path
|
|
4721
|
+
* (`inputStepsToPipelineSteps`) — an unknown-addon skip or a runtime
|
|
4722
|
+
* model substitution logs ONCE per distinct signature instead of once
|
|
4723
|
+
* per decoded frame (that method runs up to detectionFps × N cameras
|
|
4724
|
+
* times/sec). Never cleared — it's a log-noise guard, not runtime
|
|
4725
|
+
* state, and the key space (addonId/model combos) is small and stable.
|
|
4726
|
+
*/
|
|
4727
|
+
loggedLiveDispatchIssues = /* @__PURE__ */ new Set();
|
|
4728
|
+
/**
|
|
4504
4729
|
* CB5 shm passthrough — the process-local, LONG-LIVED reader cache for
|
|
4505
4730
|
* `runPipeline({ frameHandle })`. Opens each named shm segment ONCE and
|
|
4506
4731
|
* reuses the mapping for every later handle on the same ring (never
|
|
@@ -4529,6 +4754,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4529
4754
|
*/
|
|
4530
4755
|
provisioner = null;
|
|
4531
4756
|
/**
|
|
4757
|
+
* Gate A (config-correctness gate at engine change). Human-readable config
|
|
4758
|
+
* issues — model substitutions and zero-build steps — for the CURRENT
|
|
4759
|
+
* node-default tree against `this.currentEngine.format`. Recomputed
|
|
4760
|
+
* EAGERLY by {@link recomputeConfigIssues} whenever the engine actually
|
|
4761
|
+
* changes (see `onEngineSelectionChanged`), rather than relying on a
|
|
4762
|
+
* `getGlobalSteps()` call that may never happen on a camera-only node.
|
|
4763
|
+
* Merged into {@link getEngineProvisioning}'s snapshot; never consulted by
|
|
4764
|
+
* `assertEngineReady` — this gate SURFACES, it does not block inference.
|
|
4765
|
+
*/
|
|
4766
|
+
configIssues = [];
|
|
4767
|
+
/**
|
|
4532
4768
|
* True when `init()` found no persisted engine choice (first boot).
|
|
4533
4769
|
* `setApi()` reads this flag and auto-picks the best supported runtime
|
|
4534
4770
|
* using probe data (hardware + bestScore hint), persists the selection,
|
|
@@ -4775,7 +5011,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4775
5011
|
const stored = await this.loadEngine();
|
|
4776
5012
|
if (stored) this.currentEngine = stored;
|
|
4777
5013
|
else await this.resolveAutoEngine();
|
|
4778
|
-
|
|
5014
|
+
const engineChanged = prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "");
|
|
5015
|
+
if (engineChanged && this.engineFactory) {
|
|
4779
5016
|
this.log.info("engine selection changed — rebuilding pool in place", { meta: {
|
|
4780
5017
|
from: `${prev.backend}/${prev.device ?? "default"}`,
|
|
4781
5018
|
to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
|
|
@@ -4787,18 +5024,56 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4787
5024
|
this.executor = null;
|
|
4788
5025
|
}
|
|
4789
5026
|
this.startProvisioningForCurrentEngine();
|
|
5027
|
+
if (engineChanged) await this.recomputeConfigIssues();
|
|
5028
|
+
}
|
|
5029
|
+
/**
|
|
5030
|
+
* Gate A (config-correctness gate at engine change) — eagerly re-resolve
|
|
5031
|
+
* the node-default step tree for `this.currentEngine.format` and compute
|
|
5032
|
+
* human-readable config issues from the result:
|
|
5033
|
+
* - model substitutions (`getActiveModelSubstitutions()`, populated as a
|
|
5034
|
+
* side effect of the `getGlobalSteps()` re-resolve below) — a step's
|
|
5035
|
+
* chosen model lacks a build for this format, so the format default
|
|
5036
|
+
* runs instead (degraded but loadable).
|
|
5037
|
+
* - zero-build steps (`collectZeroBuildIssues`) — a step whose FULL
|
|
5038
|
+
* catalog has no model at all with a build for this format (not even
|
|
5039
|
+
* the format default is loadable).
|
|
5040
|
+
*
|
|
5041
|
+
* Never throws — this is an additive diagnostic surface, not an
|
|
5042
|
+
* enforcement point (`assertEngineReady` still owns readiness). Any
|
|
5043
|
+
* failure clears `configIssues` and logs a warning rather than
|
|
5044
|
+
* propagating out of the engine-change path.
|
|
5045
|
+
*/
|
|
5046
|
+
async recomputeConfigIssues() {
|
|
5047
|
+
this.configIssues = [];
|
|
5048
|
+
try {
|
|
5049
|
+
const steps = await this.getGlobalSteps();
|
|
5050
|
+
if (!steps) return;
|
|
5051
|
+
const format = this.currentEngine.format;
|
|
5052
|
+
const substitutionIssues = this.getActiveModelSubstitutions().map((s) => `${s.addonId}: chose ${s.chosen}, running ${s.running} (${s.format})`);
|
|
5053
|
+
const zeroBuildIssues = collectZeroBuildIssues(flattenSteps(steps), format).map((i) => `${i.addonId}: no model has a ${i.format} build`);
|
|
5054
|
+
this.configIssues = [...substitutionIssues, ...zeroBuildIssues];
|
|
5055
|
+
} catch (err) {
|
|
5056
|
+
this.configIssues = [];
|
|
5057
|
+
this.log.warn("engine-change config gate failed — no config issues surfaced", { meta: { error: require_dist.errMsg(err) } });
|
|
5058
|
+
}
|
|
4790
5059
|
}
|
|
4791
5060
|
/**
|
|
4792
5061
|
* Per-node engine-provisioning snapshot. The cap routes `{ nodeId }` for
|
|
4793
5062
|
* provider resolution only (the router strips it); each node returns its
|
|
4794
|
-
* own local machine state. Cold (pre-`setApi`) provider → `idle`.
|
|
5063
|
+
* own local machine state. Cold (pre-`setApi`) provider → `idle`. Merges
|
|
5064
|
+
* in Gate A's `configIssues` (see {@link recomputeConfigIssues}) when
|
|
5065
|
+
* non-empty — additive, never overrides the provisioner's own fields.
|
|
4795
5066
|
*/
|
|
4796
5067
|
getEngineProvisioning() {
|
|
4797
|
-
|
|
5068
|
+
const base = this.provisioner?.state ?? {
|
|
4798
5069
|
runtimeId: null,
|
|
4799
5070
|
device: null,
|
|
4800
5071
|
state: "idle"
|
|
4801
5072
|
};
|
|
5073
|
+
return this.configIssues.length ? {
|
|
5074
|
+
...base,
|
|
5075
|
+
configIssues: [...this.configIssues]
|
|
5076
|
+
} : base;
|
|
4802
5077
|
}
|
|
4803
5078
|
/**
|
|
4804
5079
|
* Inference gate. Rejects with the current phase/error unless the engine
|
|
@@ -4997,43 +5272,45 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4997
5272
|
return buildDefaultStepTree(engine.format);
|
|
4998
5273
|
}
|
|
4999
5274
|
/**
|
|
5000
|
-
*
|
|
5001
|
-
*
|
|
5002
|
-
*
|
|
5003
|
-
*
|
|
5004
|
-
*
|
|
5005
|
-
*
|
|
5006
|
-
*
|
|
5007
|
-
*
|
|
5008
|
-
*
|
|
5275
|
+
* Resolve every step's model for this node's engine format via the shared
|
|
5276
|
+
* `resolveModelForFormat`, and surface any substitution. `step.engine?.format`
|
|
5277
|
+
* is gone — `this.currentEngine.format` is the only format source (engine is
|
|
5278
|
+
* node-local).
|
|
5279
|
+
*
|
|
5280
|
+
* Rebuilds `modelSubstitutions` from scratch (via {@link collectSubstitutions})
|
|
5281
|
+
* on every call, from the FULL tree being resolved, so the map always
|
|
5282
|
+
* reflects EXACTLY the substitutions in effect for the current tree+format —
|
|
5283
|
+
* a step whose chosen model becomes compatible again (e.g. after the
|
|
5284
|
+
* operator switches the engine to a format it supports) is cleared here
|
|
5285
|
+
* instead of leaking a stale "chose X → running Y" entry.
|
|
5009
5286
|
*/
|
|
5010
|
-
|
|
5011
|
-
const
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
} catch {}
|
|
5029
|
-
const children = step.children?.length ? step.children.map(fix) : step.children;
|
|
5030
|
-
return modelId === step.modelId && children === step.children ? step : {
|
|
5287
|
+
resolveStepModels(steps) {
|
|
5288
|
+
const format = this.currentEngine.format;
|
|
5289
|
+
const substitutions = collectSubstitutions(steps, format);
|
|
5290
|
+
this.modelSubstitutions = new Map(substitutions.map((s) => [s.addonId, s]));
|
|
5291
|
+
for (const s of substitutions) this.log.info("Step model lacks engine format — running format default (node-local)", { meta: {
|
|
5292
|
+
step: s.addonId,
|
|
5293
|
+
chosen: s.chosen,
|
|
5294
|
+
running: s.running,
|
|
5295
|
+
format: s.format
|
|
5296
|
+
} });
|
|
5297
|
+
return this.resolveStepModelTree(steps, format);
|
|
5298
|
+
}
|
|
5299
|
+
/** Pure recursive resolver — builds the resolved step tree without touching `modelSubstitutions`. */
|
|
5300
|
+
resolveStepModelTree(steps, format) {
|
|
5301
|
+
return steps.map((step) => {
|
|
5302
|
+
const resolved = resolveModelForFormat(step.addonId, step.modelId ?? null, format);
|
|
5303
|
+
const children = step.children?.length ? this.resolveStepModelTree([...step.children], format) : step.children;
|
|
5304
|
+
return resolved === step.modelId && children === step.children ? step : {
|
|
5031
5305
|
...step,
|
|
5032
|
-
modelId,
|
|
5306
|
+
modelId: resolved,
|
|
5033
5307
|
...children ? { children } : {}
|
|
5034
5308
|
};
|
|
5035
|
-
};
|
|
5036
|
-
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
modelSubstitutions = /* @__PURE__ */ new Map();
|
|
5312
|
+
getActiveModelSubstitutions() {
|
|
5313
|
+
return [...this.modelSubstitutions.values()];
|
|
5037
5314
|
}
|
|
5038
5315
|
async getGlobalSteps() {
|
|
5039
5316
|
if (this.currentSteps) return this.currentSteps;
|
|
@@ -5041,7 +5318,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5041
5318
|
if (!raw) {
|
|
5042
5319
|
const defaults = buildDefaultStepTree(this.currentEngine.format);
|
|
5043
5320
|
if (defaults.length === 0) return null;
|
|
5044
|
-
this.currentSteps = this.
|
|
5321
|
+
this.currentSteps = this.resolveStepModels(defaults);
|
|
5045
5322
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
5046
5323
|
this.log.info("Bootstrapped default pipeline — object-detection + face + plate recognition enabled by default", { meta: { rootSteps: defaults.length } });
|
|
5047
5324
|
return this.currentSteps;
|
|
@@ -5054,15 +5331,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5054
5331
|
const audioDef = getStepDefinition("audio-classifier");
|
|
5055
5332
|
if (audioDef.models.some((m) => m.formats[format])) {
|
|
5056
5333
|
const modelId = getDefaultModelForFormat("audio-classifier", format);
|
|
5057
|
-
const audioEngine = modelId === "apple-soundanalysis" ? {
|
|
5058
|
-
runtime: "python",
|
|
5059
|
-
backend: "coreml",
|
|
5060
|
-
format: "coreml"
|
|
5061
|
-
} : {
|
|
5062
|
-
runtime: "python",
|
|
5063
|
-
backend: "cpu",
|
|
5064
|
-
format: "onnx"
|
|
5065
|
-
};
|
|
5066
5334
|
const audioStep = {
|
|
5067
5335
|
addonId: audioDef.id,
|
|
5068
5336
|
addonName: audioDef.name,
|
|
@@ -5071,15 +5339,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5071
5339
|
outputClasses: audioDef.outputClasses,
|
|
5072
5340
|
enabled: true,
|
|
5073
5341
|
modelId,
|
|
5074
|
-
children: []
|
|
5075
|
-
engine: audioEngine
|
|
5342
|
+
children: []
|
|
5076
5343
|
};
|
|
5077
5344
|
steps = [...steps, audioStep];
|
|
5078
5345
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(steps) });
|
|
5079
5346
|
this.log.info("Migration: added audio-classifier step to persisted pipeline config");
|
|
5080
5347
|
}
|
|
5081
5348
|
}
|
|
5082
|
-
this.currentSteps = this.
|
|
5349
|
+
this.currentSteps = this.resolveStepModels(steps);
|
|
5083
5350
|
return this.currentSteps;
|
|
5084
5351
|
} catch {
|
|
5085
5352
|
throw new Error(`Failed to parse persisted pipeline steps: corrupt data in key "${KEY_STEPS}"`);
|
|
@@ -5089,6 +5356,128 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5089
5356
|
if (!this.currentSteps) return null;
|
|
5090
5357
|
return stepsToPipelineConfig(this.currentSteps, this.currentEngine);
|
|
5091
5358
|
}
|
|
5359
|
+
/**
|
|
5360
|
+
* Task G5 (Gate B) — pre-init config-correctness gate. PURE COMPUTE
|
|
5361
|
+
* against `this.currentEngine.format`: resolves `steps` the same way the
|
|
5362
|
+
* runtime dispatch path (`resolveInputSteps`) would and reports what
|
|
5363
|
+
* WOULD happen, without touching `this.modelSubstitutions` (that node-
|
|
5364
|
+
* global map stays the exclusive property of `resolveStepModels`, the
|
|
5365
|
+
* node-default tree resolver — writing it here would race two
|
|
5366
|
+
* independent callers over the same field). Called by the orchestrator
|
|
5367
|
+
* at attach time, node-pinned to the target node.
|
|
5368
|
+
*
|
|
5369
|
+
* Composes three pure helpers from `step-resolution.ts`, ALL scoped to
|
|
5370
|
+
* ENABLED steps only (a disabled step's subtree — and any unknown addonId
|
|
5371
|
+
* or incompatible model inside it — contributes NO issue and NO
|
|
5372
|
+
* substitution; it will never execute, so flagging it is false-positive
|
|
5373
|
+
* noise):
|
|
5374
|
+
* - `collectSubstitutions` → `substitutions` (informational, never
|
|
5375
|
+
* affects `ok`).
|
|
5376
|
+
* - `collectUnknownAddonIssues` → `unknown-addon` HARD issues.
|
|
5377
|
+
* - `collectZeroBuildIssues` (over the enabled, non-audio-classifier
|
|
5378
|
+
* subset — mirrors Gate A's `flattenSteps`/`flattenEnabledVideoSteps`
|
|
5379
|
+
* filter, since audio backends are validated by audio-analyzer, not
|
|
5380
|
+
* against this node's VIDEO engine format) → `no-format-build` HARD
|
|
5381
|
+
* issues.
|
|
5382
|
+
*
|
|
5383
|
+
* `collectSubstitutions` and `collectUnknownAddonIssues` recurse into
|
|
5384
|
+
* `children` themselves, so the enabled-scoping is applied ONCE up front
|
|
5385
|
+
* via {@link pruneToEnabledSteps} (which drops a disabled step's whole
|
|
5386
|
+
* subtree) rather than re-implemented inside each collector.
|
|
5387
|
+
*
|
|
5388
|
+
* Never throws — any unexpected failure degrades to a clean `ok: true`
|
|
5389
|
+
* result with a warning logged, same posture as `recomputeConfigIssues`.
|
|
5390
|
+
*/
|
|
5391
|
+
async validatePipeline(input) {
|
|
5392
|
+
const format = this.currentEngine.format;
|
|
5393
|
+
try {
|
|
5394
|
+
const steps = input.steps;
|
|
5395
|
+
const enabledSteps = this.pruneToEnabledSteps(steps);
|
|
5396
|
+
const substitutions = collectSubstitutions(enabledSteps, format);
|
|
5397
|
+
const unknownAddonIssues = collectUnknownAddonIssues(enabledSteps);
|
|
5398
|
+
const zeroBuildIssues = collectZeroBuildIssues(this.flattenEnabledVideoStepInputs(steps), format);
|
|
5399
|
+
const issues = [...unknownAddonIssues.map((u) => ({
|
|
5400
|
+
addonId: u.addonId,
|
|
5401
|
+
kind: "unknown-addon",
|
|
5402
|
+
detail: `${u.addonId}: no registered step definition`
|
|
5403
|
+
})), ...zeroBuildIssues.map((z) => ({
|
|
5404
|
+
addonId: z.addonId,
|
|
5405
|
+
kind: "no-format-build",
|
|
5406
|
+
detail: `${z.addonId}: no model has a ${z.format} build`
|
|
5407
|
+
}))];
|
|
5408
|
+
return {
|
|
5409
|
+
ok: issues.length === 0,
|
|
5410
|
+
issues,
|
|
5411
|
+
substitutions,
|
|
5412
|
+
format
|
|
5413
|
+
};
|
|
5414
|
+
} catch (err) {
|
|
5415
|
+
this.log.warn("validatePipeline failed — reporting a clean result", { meta: { error: require_dist.errMsg(err) } });
|
|
5416
|
+
return {
|
|
5417
|
+
ok: true,
|
|
5418
|
+
issues: [],
|
|
5419
|
+
substitutions: [],
|
|
5420
|
+
format
|
|
5421
|
+
};
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
/**
|
|
5425
|
+
* Prune `steps` to ENABLED steps only, recursing into ENABLED children —
|
|
5426
|
+
* a disabled step's whole subtree is dropped, never partially kept. This
|
|
5427
|
+
* is the enabled-only tree shape `collectSubstitutions` and
|
|
5428
|
+
* `collectUnknownAddonIssues` need in {@link validatePipeline}: both
|
|
5429
|
+
* recurse into `children` themselves (unlike `flattenEnabledVideoStepInputs`,
|
|
5430
|
+
* which flattens AND excludes `audio-classifier`), so pruning once up
|
|
5431
|
+
* front — without touching the audio/video split — is enough to make
|
|
5432
|
+
* Gate B's three checks agree on scope. Pure; returns the original array
|
|
5433
|
+
* reference when nothing changed.
|
|
5434
|
+
*/
|
|
5435
|
+
pruneToEnabledSteps(steps) {
|
|
5436
|
+
const result = [];
|
|
5437
|
+
let changed = false;
|
|
5438
|
+
for (const step of steps) {
|
|
5439
|
+
if (!step.enabled) {
|
|
5440
|
+
changed = true;
|
|
5441
|
+
continue;
|
|
5442
|
+
}
|
|
5443
|
+
const prunedChildren = step.children?.length ? this.pruneToEnabledSteps(step.children) : step.children;
|
|
5444
|
+
if (prunedChildren !== step.children) {
|
|
5445
|
+
changed = true;
|
|
5446
|
+
result.push({
|
|
5447
|
+
...step,
|
|
5448
|
+
children: prunedChildren
|
|
5449
|
+
});
|
|
5450
|
+
} else result.push(step);
|
|
5451
|
+
}
|
|
5452
|
+
return changed ? result : steps;
|
|
5453
|
+
}
|
|
5454
|
+
/**
|
|
5455
|
+
* Flatten `steps` (recursing into ENABLED children only — a disabled
|
|
5456
|
+
* step's subtree is skipped entirely, mirroring
|
|
5457
|
+
* `flattenEnabledVideoSteps`) into `{addonId}` entries, excluding
|
|
5458
|
+
* `audio-classifier` steps: Gate B validates the VIDEO engine format
|
|
5459
|
+
* (`this.currentEngine.format`) only — audio backends resolve their own
|
|
5460
|
+
* format independently and are validated by the audio-analyzer addon.
|
|
5461
|
+
* An addonId whose `StepDefinition` lookup throws is skipped here (it is
|
|
5462
|
+
* already reported separately by `collectUnknownAddonIssues`).
|
|
5463
|
+
*/
|
|
5464
|
+
flattenEnabledVideoStepInputs(steps) {
|
|
5465
|
+
const result = [];
|
|
5466
|
+
const walk = (nodes) => {
|
|
5467
|
+
for (const step of nodes) {
|
|
5468
|
+
if (!step.enabled) continue;
|
|
5469
|
+
try {
|
|
5470
|
+
if (getStepDefinition(step.addonId).slot === "audio-classifier") continue;
|
|
5471
|
+
} catch {
|
|
5472
|
+
continue;
|
|
5473
|
+
}
|
|
5474
|
+
result.push({ addonId: step.addonId });
|
|
5475
|
+
if (step.children?.length) walk(step.children);
|
|
5476
|
+
}
|
|
5477
|
+
};
|
|
5478
|
+
walk(steps);
|
|
5479
|
+
return result;
|
|
5480
|
+
}
|
|
5092
5481
|
async getSelectedEngine() {
|
|
5093
5482
|
return this.currentEngine;
|
|
5094
5483
|
}
|
|
@@ -5101,7 +5490,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5101
5490
|
const addons = ALL_PIPELINE_STEPS.map((s) => ({
|
|
5102
5491
|
id: s.definition.id,
|
|
5103
5492
|
name: s.definition.name,
|
|
5104
|
-
models: s.definition.models.map((m) => {
|
|
5493
|
+
models: s.definition.models.filter((m) => m.legacy !== true).map((m) => {
|
|
5105
5494
|
const formats = {};
|
|
5106
5495
|
for (const [formatKey, entry] of Object.entries(m.formats)) {
|
|
5107
5496
|
if (!entry) continue;
|
|
@@ -5415,7 +5804,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5415
5804
|
emit(`Image: ${imageWidth}×${imageHeight}`);
|
|
5416
5805
|
}
|
|
5417
5806
|
decodeMs = performance.now() - decodeT0;
|
|
5418
|
-
const
|
|
5807
|
+
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
5808
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat);
|
|
5419
5809
|
const stepsSignature = JSON.stringify(benchmarkSteps.map((s) => ({
|
|
5420
5810
|
id: s.addonId,
|
|
5421
5811
|
settings: s.settings ?? {}
|
|
@@ -5704,22 +6094,46 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5704
6094
|
this.log.warn("Override cache dispose failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
5705
6095
|
}
|
|
5706
6096
|
}
|
|
5707
|
-
/**
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
6097
|
+
/**
|
|
6098
|
+
* Convert PipelineStepInput[] → PipelineDefaultStep[] by enriching from
|
|
6099
|
+
* StepDefinitions. This is the LIVE per-camera dispatch path — it runs
|
|
6100
|
+
* once per decoded frame (up to detectionFps × N cameras/sec) — so it
|
|
6101
|
+
* must NEVER throw. A thin adapter over the pure {@link resolveInputSteps}
|
|
6102
|
+
* seam: an unknown `addonId` (a bad step config sent by a camera
|
|
6103
|
+
* pipeline) is dropped from the tree and logged once (deduped, not
|
|
6104
|
+
* per-frame) at warn level instead of propagating as a per-frame error
|
|
6105
|
+
* loop. A runtime model substitution (the step's explicit model lacks a
|
|
6106
|
+
* build for this node's engine format, so the format default runs
|
|
6107
|
+
* instead) is logged once (deduped) at info level so an operator can see
|
|
6108
|
+
* a camera×node model swap instead of it happening silently. Does NOT
|
|
6109
|
+
* write `this.modelSubstitutions` — that map is owned exclusively by
|
|
6110
|
+
* `resolveStepModels` (see its doc comment); a second writer here would
|
|
6111
|
+
* corrupt it.
|
|
6112
|
+
*
|
|
6113
|
+
* `format` defaults to `this.currentEngine.format` (the LIVE per-camera
|
|
6114
|
+
* dispatch path never passes it explicitly). Benchmark call sites with a
|
|
6115
|
+
* per-run engine override (`input.engine`) MUST pass the override's format
|
|
6116
|
+
* explicitly — `this.currentEngine` only gets swapped to the override
|
|
6117
|
+
* LATER in `runPipeline`/`runPipelineBatch`, so relying on the default here
|
|
6118
|
+
* would resolve models against the node's persisted format instead of the
|
|
6119
|
+
* format actually being benchmarked.
|
|
6120
|
+
*/
|
|
6121
|
+
inputStepsToPipelineSteps(steps, format = this.currentEngine.format) {
|
|
6122
|
+
const { steps: resolvedSteps, diagnostics } = resolveInputSteps(steps, format);
|
|
6123
|
+
for (const addonId of diagnostics.unknownAddonIds) this.logLiveDispatchIssueOnce(`unknown:${addonId}`, () => this.log.warn("Live pipeline step references an unknown addon — dropping step", { meta: { addonId } }));
|
|
6124
|
+
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: {
|
|
6125
|
+
step: sub.addonId,
|
|
6126
|
+
chosen: sub.chosen,
|
|
6127
|
+
running: sub.running,
|
|
6128
|
+
format: sub.format
|
|
6129
|
+
} }));
|
|
6130
|
+
return resolvedSteps;
|
|
6131
|
+
}
|
|
6132
|
+
/** Run `fn()` at most once per `key` for the lifetime of this provider instance. */
|
|
6133
|
+
logLiveDispatchIssueOnce(key, fn) {
|
|
6134
|
+
if (this.loggedLiveDispatchIssues.has(key)) return;
|
|
6135
|
+
this.loggedLiveDispatchIssues.add(key);
|
|
6136
|
+
fn();
|
|
5723
6137
|
}
|
|
5724
6138
|
/**
|
|
5725
6139
|
* Single-flight gate around `ensureModelsForSteps`. Concurrent
|
|
@@ -5925,7 +6339,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5925
6339
|
}
|
|
5926
6340
|
async runPipelineBatchImpl(input) {
|
|
5927
6341
|
if (input.frames.length === 0) return { results: [] };
|
|
5928
|
-
const
|
|
6342
|
+
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
6343
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat);
|
|
5929
6344
|
const enabledSteps = flattenEnabledVideoSteps(benchmarkSteps);
|
|
5930
6345
|
if (enabledSteps.length === 0) throw new Error("runPipelineBatch: no enabled steps");
|
|
5931
6346
|
const rootStep = enabledSteps[0];
|
|
@@ -6617,7 +7032,7 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
|
6617
7032
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6618
7033
|
for (const pipelineStep of ALL_PIPELINE_STEPS) {
|
|
6619
7034
|
const step = pipelineStep.definition;
|
|
6620
|
-
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format]);
|
|
7035
|
+
const availableModels = mergeCustomModels(step.models, customByStep?.get(step.id) ?? []).filter((m) => m.formats[format] && m.legacy !== true);
|
|
6621
7036
|
if (availableModels.length === 0) continue;
|
|
6622
7037
|
const slot = step.slot;
|
|
6623
7038
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
@@ -6635,7 +7050,9 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
|
6635
7050
|
formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
|
|
6636
7051
|
downloaded: require_model_download_service_C_IHWnXx.isModelDownloaded(modelsDir, m, f),
|
|
6637
7052
|
sizeMB: entry.sizeMB
|
|
6638
|
-
}]))
|
|
7053
|
+
}])),
|
|
7054
|
+
...m.group ? { group: m.group } : {},
|
|
7055
|
+
...m.legacy !== void 0 ? { legacy: m.legacy } : {}
|
|
6639
7056
|
})),
|
|
6640
7057
|
defaultModelId: step.defaultModelId,
|
|
6641
7058
|
...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},
|
|
@@ -6724,23 +7141,10 @@ function buildDefaultStepTree(format) {
|
|
|
6724
7141
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
6725
7142
|
makeStep("segmentation", [], { enabled: false })
|
|
6726
7143
|
].filter((s) => s !== null));
|
|
6727
|
-
const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
|
|
6728
|
-
runtime: "python",
|
|
6729
|
-
backend: "coreml",
|
|
6730
|
-
format: "coreml"
|
|
6731
|
-
} : {
|
|
6732
|
-
runtime: "python",
|
|
6733
|
-
backend: "cpu",
|
|
6734
|
-
format: "onnx"
|
|
6735
|
-
};
|
|
6736
7144
|
const audioStep = makeStep("audio-classifier", []);
|
|
6737
|
-
const audioClassifier = audioStep ? {
|
|
6738
|
-
...audioStep,
|
|
6739
|
-
engine: audioEngine
|
|
6740
|
-
} : null;
|
|
6741
7145
|
const steps = [];
|
|
6742
7146
|
if (root) steps.push(root);
|
|
6743
|
-
if (
|
|
7147
|
+
if (audioStep) steps.push(audioStep);
|
|
6744
7148
|
return steps;
|
|
6745
7149
|
}
|
|
6746
7150
|
function stepsToPipelineConfig(steps, engine) {
|
|
@@ -6760,7 +7164,7 @@ function stepsToPipelineConfig(steps, engine) {
|
|
|
6760
7164
|
video: videoSteps.map(toNode),
|
|
6761
7165
|
...audioStep ? {
|
|
6762
7166
|
audio: toNode(audioStep),
|
|
6763
|
-
audioEngine:
|
|
7167
|
+
audioEngine: engine
|
|
6764
7168
|
} : {}
|
|
6765
7169
|
};
|
|
6766
7170
|
}
|
|
@@ -6892,7 +7296,8 @@ var DEFAULT_CONFIG = {
|
|
|
6892
7296
|
engineBackend: "auto",
|
|
6893
7297
|
engineDevice: "cpu",
|
|
6894
7298
|
probedBestEngine: "",
|
|
6895
|
-
activeEngine: ""
|
|
7299
|
+
activeEngine: "",
|
|
7300
|
+
activeModelSubstitutions: ""
|
|
6896
7301
|
};
|
|
6897
7302
|
/** Derive the model-format from a backend value. Called by the provider. */
|
|
6898
7303
|
function backendToFormat(backend) {
|
|
@@ -7013,6 +7418,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7013
7418
|
readonlyField: true,
|
|
7014
7419
|
default: ""
|
|
7015
7420
|
}),
|
|
7421
|
+
this.field({
|
|
7422
|
+
type: "text",
|
|
7423
|
+
key: "activeModelSubstitutions",
|
|
7424
|
+
label: "Model substitutions",
|
|
7425
|
+
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.",
|
|
7426
|
+
readonlyField: true,
|
|
7427
|
+
default: "",
|
|
7428
|
+
showWhen: {
|
|
7429
|
+
field: "activeModelSubstitutions",
|
|
7430
|
+
notEquals: ""
|
|
7431
|
+
}
|
|
7432
|
+
}),
|
|
7016
7433
|
this.field({
|
|
7017
7434
|
type: "select",
|
|
7018
7435
|
key: "engineBackend",
|
|
@@ -7182,12 +7599,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7182
7599
|
const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
|
|
7183
7600
|
const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
|
|
7184
7601
|
const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
|
|
7602
|
+
const substitutions = this.provider?.getActiveModelSubstitutions() ?? [];
|
|
7603
|
+
const activeModelSubstitutions = substitutions.length === 0 ? "" : substitutions.map((s) => `${s.addonId}: ${s.chosen}→${s.running}`).join(", ");
|
|
7185
7604
|
const displayBackend = isAuto ? "auto" : backend;
|
|
7186
7605
|
const raw = {
|
|
7187
7606
|
...merged,
|
|
7188
7607
|
engineBackend: displayBackend,
|
|
7189
7608
|
engineDevice: device,
|
|
7190
|
-
activeEngine
|
|
7609
|
+
activeEngine,
|
|
7610
|
+
activeModelSubstitutions
|
|
7191
7611
|
};
|
|
7192
7612
|
const schema = this.globalSettingsSchema();
|
|
7193
7613
|
if (!schema) return { sections: [] };
|
|
@@ -7451,7 +7871,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7451
7871
|
}).catch(() => {});
|
|
7452
7872
|
}
|
|
7453
7873
|
getModelCatalog() {
|
|
7454
|
-
return ALL_STEPS.flatMap((s) =>
|
|
7874
|
+
return ALL_STEPS.flatMap((s) => s.models.filter((m) => m.legacy !== true));
|
|
7455
7875
|
}
|
|
7456
7876
|
/**
|
|
7457
7877
|
* Proactively install the OpenVINO Python package when Intel hardware
|