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