@camstack/addon-pipeline 1.1.43 → 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 +526 -109
- package/dist/detection-pipeline/index.mjs +520 -104
- 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 +60 -20
- package/dist/recorder/index.mjs +57 -18
- 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 +86 -2
- package/dist/session-decode/decode-worker-child.mjs +83 -2
- 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 +746 -135
- package/dist/stream-broker/index.mjs +737 -127
- 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 = {
|
|
@@ -4208,6 +4280,147 @@ function mergeCustomModels(staticModels, customModels) {
|
|
|
4208
4280
|
return merged;
|
|
4209
4281
|
}
|
|
4210
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
|
|
4211
4424
|
//#region src/detection-pipeline/provider.ts
|
|
4212
4425
|
/**
|
|
4213
4426
|
* DetectionPipelineProvider — implements IPipelineExecutorProvider.
|
|
@@ -4504,6 +4717,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4504
4717
|
*/
|
|
4505
4718
|
lastStepsSignature = null;
|
|
4506
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
|
+
/**
|
|
4507
4729
|
* CB5 shm passthrough — the process-local, LONG-LIVED reader cache for
|
|
4508
4730
|
* `runPipeline({ frameHandle })`. Opens each named shm segment ONCE and
|
|
4509
4731
|
* reuses the mapping for every later handle on the same ring (never
|
|
@@ -4532,6 +4754,17 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4532
4754
|
*/
|
|
4533
4755
|
provisioner = null;
|
|
4534
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
|
+
/**
|
|
4535
4768
|
* True when `init()` found no persisted engine choice (first boot).
|
|
4536
4769
|
* `setApi()` reads this flag and auto-picks the best supported runtime
|
|
4537
4770
|
* using probe data (hardware + bestScore hint), persists the selection,
|
|
@@ -4778,7 +5011,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4778
5011
|
const stored = await this.loadEngine();
|
|
4779
5012
|
if (stored) this.currentEngine = stored;
|
|
4780
5013
|
else await this.resolveAutoEngine();
|
|
4781
|
-
|
|
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) {
|
|
4782
5016
|
this.log.info("engine selection changed — rebuilding pool in place", { meta: {
|
|
4783
5017
|
from: `${prev.backend}/${prev.device ?? "default"}`,
|
|
4784
5018
|
to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
|
|
@@ -4790,18 +5024,56 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4790
5024
|
this.executor = null;
|
|
4791
5025
|
}
|
|
4792
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
|
+
}
|
|
4793
5059
|
}
|
|
4794
5060
|
/**
|
|
4795
5061
|
* Per-node engine-provisioning snapshot. The cap routes `{ nodeId }` for
|
|
4796
5062
|
* provider resolution only (the router strips it); each node returns its
|
|
4797
|
-
* 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.
|
|
4798
5066
|
*/
|
|
4799
5067
|
getEngineProvisioning() {
|
|
4800
|
-
|
|
5068
|
+
const base = this.provisioner?.state ?? {
|
|
4801
5069
|
runtimeId: null,
|
|
4802
5070
|
device: null,
|
|
4803
5071
|
state: "idle"
|
|
4804
5072
|
};
|
|
5073
|
+
return this.configIssues.length ? {
|
|
5074
|
+
...base,
|
|
5075
|
+
configIssues: [...this.configIssues]
|
|
5076
|
+
} : base;
|
|
4805
5077
|
}
|
|
4806
5078
|
/**
|
|
4807
5079
|
* Inference gate. Rejects with the current phase/error unless the engine
|
|
@@ -5000,43 +5272,45 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5000
5272
|
return buildDefaultStepTree(engine.format);
|
|
5001
5273
|
}
|
|
5002
5274
|
/**
|
|
5003
|
-
*
|
|
5004
|
-
*
|
|
5005
|
-
*
|
|
5006
|
-
*
|
|
5007
|
-
*
|
|
5008
|
-
*
|
|
5009
|
-
*
|
|
5010
|
-
*
|
|
5011
|
-
*
|
|
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.
|
|
5012
5286
|
*/
|
|
5013
|
-
|
|
5014
|
-
const
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
} catch {}
|
|
5032
|
-
const children = step.children?.length ? step.children.map(fix) : step.children;
|
|
5033
|
-
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 : {
|
|
5034
5305
|
...step,
|
|
5035
|
-
modelId,
|
|
5306
|
+
modelId: resolved,
|
|
5036
5307
|
...children ? { children } : {}
|
|
5037
5308
|
};
|
|
5038
|
-
};
|
|
5039
|
-
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
modelSubstitutions = /* @__PURE__ */ new Map();
|
|
5312
|
+
getActiveModelSubstitutions() {
|
|
5313
|
+
return [...this.modelSubstitutions.values()];
|
|
5040
5314
|
}
|
|
5041
5315
|
async getGlobalSteps() {
|
|
5042
5316
|
if (this.currentSteps) return this.currentSteps;
|
|
@@ -5044,7 +5318,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5044
5318
|
if (!raw) {
|
|
5045
5319
|
const defaults = buildDefaultStepTree(this.currentEngine.format);
|
|
5046
5320
|
if (defaults.length === 0) return null;
|
|
5047
|
-
this.currentSteps = this.
|
|
5321
|
+
this.currentSteps = this.resolveStepModels(defaults);
|
|
5048
5322
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(defaults) });
|
|
5049
5323
|
this.log.info("Bootstrapped default pipeline — object-detection + face + plate recognition enabled by default", { meta: { rootSteps: defaults.length } });
|
|
5050
5324
|
return this.currentSteps;
|
|
@@ -5057,15 +5331,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5057
5331
|
const audioDef = getStepDefinition("audio-classifier");
|
|
5058
5332
|
if (audioDef.models.some((m) => m.formats[format])) {
|
|
5059
5333
|
const modelId = getDefaultModelForFormat("audio-classifier", format);
|
|
5060
|
-
const audioEngine = modelId === "apple-soundanalysis" ? {
|
|
5061
|
-
runtime: "python",
|
|
5062
|
-
backend: "coreml",
|
|
5063
|
-
format: "coreml"
|
|
5064
|
-
} : {
|
|
5065
|
-
runtime: "python",
|
|
5066
|
-
backend: "cpu",
|
|
5067
|
-
format: "onnx"
|
|
5068
|
-
};
|
|
5069
5334
|
const audioStep = {
|
|
5070
5335
|
addonId: audioDef.id,
|
|
5071
5336
|
addonName: audioDef.name,
|
|
@@ -5074,15 +5339,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5074
5339
|
outputClasses: audioDef.outputClasses,
|
|
5075
5340
|
enabled: true,
|
|
5076
5341
|
modelId,
|
|
5077
|
-
children: []
|
|
5078
|
-
engine: audioEngine
|
|
5342
|
+
children: []
|
|
5079
5343
|
};
|
|
5080
5344
|
steps = [...steps, audioStep];
|
|
5081
5345
|
this.writeStore({ [KEY_STEPS]: JSON.stringify(steps) });
|
|
5082
5346
|
this.log.info("Migration: added audio-classifier step to persisted pipeline config");
|
|
5083
5347
|
}
|
|
5084
5348
|
}
|
|
5085
|
-
this.currentSteps = this.
|
|
5349
|
+
this.currentSteps = this.resolveStepModels(steps);
|
|
5086
5350
|
return this.currentSteps;
|
|
5087
5351
|
} catch {
|
|
5088
5352
|
throw new Error(`Failed to parse persisted pipeline steps: corrupt data in key "${KEY_STEPS}"`);
|
|
@@ -5092,6 +5356,128 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5092
5356
|
if (!this.currentSteps) return null;
|
|
5093
5357
|
return stepsToPipelineConfig(this.currentSteps, this.currentEngine);
|
|
5094
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
|
+
}
|
|
5095
5481
|
async getSelectedEngine() {
|
|
5096
5482
|
return this.currentEngine;
|
|
5097
5483
|
}
|
|
@@ -5104,7 +5490,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5104
5490
|
const addons = ALL_PIPELINE_STEPS.map((s) => ({
|
|
5105
5491
|
id: s.definition.id,
|
|
5106
5492
|
name: s.definition.name,
|
|
5107
|
-
models: s.definition.models.map((m) => {
|
|
5493
|
+
models: s.definition.models.filter((m) => m.legacy !== true).map((m) => {
|
|
5108
5494
|
const formats = {};
|
|
5109
5495
|
for (const [formatKey, entry] of Object.entries(m.formats)) {
|
|
5110
5496
|
if (!entry) continue;
|
|
@@ -5418,7 +5804,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5418
5804
|
emit(`Image: ${imageWidth}×${imageHeight}`);
|
|
5419
5805
|
}
|
|
5420
5806
|
decodeMs = performance.now() - decodeT0;
|
|
5421
|
-
const
|
|
5807
|
+
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
5808
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat);
|
|
5422
5809
|
const stepsSignature = JSON.stringify(benchmarkSteps.map((s) => ({
|
|
5423
5810
|
id: s.addonId,
|
|
5424
5811
|
settings: s.settings ?? {}
|
|
@@ -5707,22 +6094,46 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5707
6094
|
this.log.warn("Override cache dispose failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
5708
6095
|
}
|
|
5709
6096
|
}
|
|
5710
|
-
/**
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
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();
|
|
5726
6137
|
}
|
|
5727
6138
|
/**
|
|
5728
6139
|
* Single-flight gate around `ensureModelsForSteps`. Concurrent
|
|
@@ -5928,7 +6339,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5928
6339
|
}
|
|
5929
6340
|
async runPipelineBatchImpl(input) {
|
|
5930
6341
|
if (input.frames.length === 0) return { results: [] };
|
|
5931
|
-
const
|
|
6342
|
+
const resolveFormat = input.engine?.format ?? this.currentEngine.format;
|
|
6343
|
+
const benchmarkSteps = this.inputStepsToPipelineSteps(input.steps, resolveFormat);
|
|
5932
6344
|
const enabledSteps = flattenEnabledVideoSteps(benchmarkSteps);
|
|
5933
6345
|
if (enabledSteps.length === 0) throw new Error("runPipelineBatch: no enabled steps");
|
|
5934
6346
|
const rootStep = enabledSteps[0];
|
|
@@ -6620,7 +7032,7 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
|
6620
7032
|
const slotMap = /* @__PURE__ */ new Map();
|
|
6621
7033
|
for (const pipelineStep of ALL_PIPELINE_STEPS) {
|
|
6622
7034
|
const step = pipelineStep.definition;
|
|
6623
|
-
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);
|
|
6624
7036
|
if (availableModels.length === 0) continue;
|
|
6625
7037
|
const slot = step.slot;
|
|
6626
7038
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
@@ -6638,7 +7050,9 @@ function buildSchemaSlots(format, modelsDir, customByStep) {
|
|
|
6638
7050
|
formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
|
|
6639
7051
|
downloaded: require_model_download_service_C_IHWnXx.isModelDownloaded(modelsDir, m, f),
|
|
6640
7052
|
sizeMB: entry.sizeMB
|
|
6641
|
-
}]))
|
|
7053
|
+
}])),
|
|
7054
|
+
...m.group ? { group: m.group } : {},
|
|
7055
|
+
...m.legacy !== void 0 ? { legacy: m.legacy } : {}
|
|
6642
7056
|
})),
|
|
6643
7057
|
defaultModelId: step.defaultModelId,
|
|
6644
7058
|
...step.defaultModelIdByFormat ? { defaultModelIdByFormat: { ...step.defaultModelIdByFormat } } : {},
|
|
@@ -6727,23 +7141,10 @@ function buildDefaultStepTree(format) {
|
|
|
6727
7141
|
makeStep("vehicle-classifier", [], { enabled: false }),
|
|
6728
7142
|
makeStep("segmentation", [], { enabled: false })
|
|
6729
7143
|
].filter((s) => s !== null));
|
|
6730
|
-
const audioEngine = getDefaultModelForFormat("audio-classifier", format) === "apple-soundanalysis" ? {
|
|
6731
|
-
runtime: "python",
|
|
6732
|
-
backend: "coreml",
|
|
6733
|
-
format: "coreml"
|
|
6734
|
-
} : {
|
|
6735
|
-
runtime: "python",
|
|
6736
|
-
backend: "cpu",
|
|
6737
|
-
format: "onnx"
|
|
6738
|
-
};
|
|
6739
7144
|
const audioStep = makeStep("audio-classifier", []);
|
|
6740
|
-
const audioClassifier = audioStep ? {
|
|
6741
|
-
...audioStep,
|
|
6742
|
-
engine: audioEngine
|
|
6743
|
-
} : null;
|
|
6744
7145
|
const steps = [];
|
|
6745
7146
|
if (root) steps.push(root);
|
|
6746
|
-
if (
|
|
7147
|
+
if (audioStep) steps.push(audioStep);
|
|
6747
7148
|
return steps;
|
|
6748
7149
|
}
|
|
6749
7150
|
function stepsToPipelineConfig(steps, engine) {
|
|
@@ -6763,7 +7164,7 @@ function stepsToPipelineConfig(steps, engine) {
|
|
|
6763
7164
|
video: videoSteps.map(toNode),
|
|
6764
7165
|
...audioStep ? {
|
|
6765
7166
|
audio: toNode(audioStep),
|
|
6766
|
-
audioEngine:
|
|
7167
|
+
audioEngine: engine
|
|
6767
7168
|
} : {}
|
|
6768
7169
|
};
|
|
6769
7170
|
}
|
|
@@ -6895,7 +7296,8 @@ var DEFAULT_CONFIG = {
|
|
|
6895
7296
|
engineBackend: "auto",
|
|
6896
7297
|
engineDevice: "cpu",
|
|
6897
7298
|
probedBestEngine: "",
|
|
6898
|
-
activeEngine: ""
|
|
7299
|
+
activeEngine: "",
|
|
7300
|
+
activeModelSubstitutions: ""
|
|
6899
7301
|
};
|
|
6900
7302
|
/** Derive the model-format from a backend value. Called by the provider. */
|
|
6901
7303
|
function backendToFormat(backend) {
|
|
@@ -7016,6 +7418,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7016
7418
|
readonlyField: true,
|
|
7017
7419
|
default: ""
|
|
7018
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
|
+
}),
|
|
7019
7433
|
this.field({
|
|
7020
7434
|
type: "select",
|
|
7021
7435
|
key: "engineBackend",
|
|
@@ -7185,12 +7599,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7185
7599
|
const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
|
|
7186
7600
|
const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
|
|
7187
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(", ");
|
|
7188
7604
|
const displayBackend = isAuto ? "auto" : backend;
|
|
7189
7605
|
const raw = {
|
|
7190
7606
|
...merged,
|
|
7191
7607
|
engineBackend: displayBackend,
|
|
7192
7608
|
engineDevice: device,
|
|
7193
|
-
activeEngine
|
|
7609
|
+
activeEngine,
|
|
7610
|
+
activeModelSubstitutions
|
|
7194
7611
|
};
|
|
7195
7612
|
const schema = this.globalSettingsSchema();
|
|
7196
7613
|
if (!schema) return { sections: [] };
|
|
@@ -7454,7 +7871,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7454
7871
|
}).catch(() => {});
|
|
7455
7872
|
}
|
|
7456
7873
|
getModelCatalog() {
|
|
7457
|
-
return ALL_STEPS.flatMap((s) =>
|
|
7874
|
+
return ALL_STEPS.flatMap((s) => s.models.filter((m) => m.legacy !== true));
|
|
7458
7875
|
}
|
|
7459
7876
|
/**
|
|
7460
7877
|
* Proactively install the OpenVINO Python package when Intel hardware
|