@camstack/addon-pipeline 1.2.104 → 1.2.105
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/{addon-utils-DRbRzrDy.js → addon-utils-UzUMfVj1.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +367 -40
- package/dist/detection-pipeline/index.mjs +365 -38
- package/dist/{dist-D9sltFoR.mjs → dist-BSWr9Qg8.mjs} +66 -5
- package/dist/{dist-gLAVhbvO.js → dist-DVMPCqaj.js} +66 -5
- package/dist/{event-loop-stall-monitor-BUhYY39J.js → event-loop-stall-monitor-BazdZ0g0.js} +1 -1
- package/dist/{event-loop-stall-monitor-BS_lPX6H.mjs → event-loop-stall-monitor-BeQHivuY.mjs} +1 -1
- package/dist/{lazy-sharp-OiAUva0g.js → lazy-sharp-Du1-XaNP.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-DY4RHcTj.js → process-memory-CiNyC-wv.js} +1 -1
- package/dist/{process-memory-Dw7-9jCI.mjs → process-memory-DFL51gCx.mjs} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZaqZ17-x.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-pkzutnza.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DvZWLWk1.mjs +26 -0
- package/dist/stream-broker/{hostInit-aZY81RnZ.mjs → hostInit-B1i1rtBB.mjs} +3 -3
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-Bz7-sMZC.mjs → worker-protocol-Clcy8PvY.mjs} +1 -1
- package/dist/{worker-protocol-D_0q_4Le.js → worker-protocol-pUUt-yhh.js} +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +45 -1
- package/python/test_inference_pool_reconfigure.py +122 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BnhMnnKV.mjs +0 -26
|
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_process_memory = require("../process-memory-
|
|
7
|
-
const require_addon_utils = require("../addon-utils-
|
|
5
|
+
const require_dist = require("../dist-DVMPCqaj.js");
|
|
6
|
+
const require_process_memory = require("../process-memory-CiNyC-wv.js");
|
|
7
|
+
const require_addon_utils = require("../addon-utils-UzUMfVj1.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_dist.__toESM(node_path);
|
|
10
10
|
let node_fs = require("node:fs");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { Ct as hydrateSchema, K as mapAudioLabelToMacro, O as audioAnalysisCapability, at as resolvePoolMemoryPolicy, b as PoolMemoryWatchdog, ft as errMsg, g as HF_BASE_URL, k as audioAnalyzerCapability, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, vt as BaseAddon } from "../dist-
|
|
3
|
-
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-
|
|
2
|
+
import { Ct as hydrateSchema, K as mapAudioLabelToMacro, O as audioAnalysisCapability, at as resolvePoolMemoryPolicy, b as PoolMemoryWatchdog, ft as errMsg, g as HF_BASE_URL, k as audioAnalyzerCapability, n as AUDIO_BACKEND_CHOICES, s as DEFAULT_AUDIO_ANALYZER_CONFIG, vt as BaseAddon } from "../dist-BSWr9Qg8.mjs";
|
|
3
|
+
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DFL51gCx.mjs";
|
|
4
4
|
import { i as downloadFile } from "../addon-utils-A2S9D7pu.mjs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import * as fs from "node:fs";
|
|
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-
|
|
7
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
8
|
-
const require_process_memory = require("../process-memory-
|
|
9
|
-
const require_addon_utils = require("../addon-utils-
|
|
5
|
+
const require_dist = require("../dist-DVMPCqaj.js");
|
|
6
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-BazdZ0g0.js");
|
|
7
|
+
const require_lazy_sharp = require("../lazy-sharp-Du1-XaNP.js");
|
|
8
|
+
const require_process_memory = require("../process-memory-CiNyC-wv.js");
|
|
9
|
+
const require_addon_utils = require("../addon-utils-UzUMfVj1.js");
|
|
10
10
|
let sharp = require("sharp");
|
|
11
11
|
sharp = require_dist.__toESM(sharp);
|
|
12
12
|
let node_child_process = require("node:child_process");
|
|
@@ -1076,6 +1076,25 @@ var SharedInferencePool = class {
|
|
|
1076
1076
|
})));
|
|
1077
1077
|
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to unload model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
1078
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Retune a LOADED slot's decode-scoped settings without touching its
|
|
1081
|
+
* inference session — the narrow counterpart of `replaceModel`.
|
|
1082
|
+
*
|
|
1083
|
+
* `replace` is `unload` + `load`: it re-instantiates the session (~1.4s on
|
|
1084
|
+
* coreml, and an OpenVINO recompile) and stalls every live camera routed to
|
|
1085
|
+
* this pool. An operator moving the NMS slider must not cost that, and must
|
|
1086
|
+
* not touch any other slot: the Python side applies the patch to `index`
|
|
1087
|
+
* only, and refuses any key outside its reconfigurable allow-list rather
|
|
1088
|
+
* than accepting a `path` change that would silently not reload.
|
|
1089
|
+
*/
|
|
1090
|
+
async reconfigureModel(index, settings) {
|
|
1091
|
+
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
1092
|
+
cmd: "reconfigure",
|
|
1093
|
+
index,
|
|
1094
|
+
config: { nmsIouThreshold: settings.nmsIouThreshold }
|
|
1095
|
+
})));
|
|
1096
|
+
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to reconfigure model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
1097
|
+
}
|
|
1079
1098
|
async replaceModel(index, config) {
|
|
1080
1099
|
const responses = await Promise.all(this.workers.map((w) => w.sendCommand({
|
|
1081
1100
|
cmd: "replace",
|
|
@@ -1303,10 +1322,188 @@ function pruneUnrunnableDetailSteps(steps, isDetailStep, isUnrunnable) {
|
|
|
1303
1322
|
};
|
|
1304
1323
|
}
|
|
1305
1324
|
//#endregion
|
|
1325
|
+
//#region src/detection-pipeline/registry/effective-postprocessor.ts
|
|
1326
|
+
function resolveEffectivePostprocessor(definition, modelId) {
|
|
1327
|
+
return resolveEffectivePostprocessorForEntry(definition, modelId ? definition.models.find((m) => m.id === modelId) : void 0);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Entry-based variant for callers that already hold the `ModelCatalogEntry` —
|
|
1331
|
+
* required for CUSTOM (operator-registered) models, which are NOT in
|
|
1332
|
+
* `definition.models` and would otherwise silently fall back to the step
|
|
1333
|
+
* default decode (a Frigate `yolonas` model decoded as `yolo` produces no
|
|
1334
|
+
* error anywhere, just zero detections).
|
|
1335
|
+
*/
|
|
1336
|
+
function resolveEffectivePostprocessorForEntry(definition, entry) {
|
|
1337
|
+
return entry?.postprocessor ?? definition.postprocessor;
|
|
1338
|
+
}
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region src/detection-pipeline/registry/model-knob-applicability.ts
|
|
1341
|
+
/** Decodes that run NMS in the pipeline (and hence consult `nmsIouThreshold`).
|
|
1342
|
+
* Mirrors the Python postprocessors that read the knob — yolo.py,
|
|
1343
|
+
* yolo_seg.py, rfdetr.py, scrfd.py. */
|
|
1344
|
+
var NMS_CONSUMING_POSTPROCESSORS = new Set([
|
|
1345
|
+
"yolo",
|
|
1346
|
+
"yolo-seg",
|
|
1347
|
+
"rfdetr",
|
|
1348
|
+
"scrfd"
|
|
1349
|
+
]);
|
|
1350
|
+
/**
|
|
1351
|
+
* Does this decode consult `nmsIouThreshold`?
|
|
1352
|
+
*
|
|
1353
|
+
* ONE predicate for the whole system: the UI disables the slider through
|
|
1354
|
+
* {@link annotateConfigSchemaForModels}, and the runtime refuses to forward a
|
|
1355
|
+
* value through `resolvePoolDecodeSettings` — both off this set. Two copies of
|
|
1356
|
+
* the matrix would let the UI promise an effect the pool does not deliver,
|
|
1357
|
+
* which is the failure D214 exists to remove.
|
|
1358
|
+
*/
|
|
1359
|
+
function postprocessorConsumesNms(postprocessor) {
|
|
1360
|
+
return NMS_CONSUMING_POSTPROCESSORS.has(postprocessor);
|
|
1361
|
+
}
|
|
1362
|
+
var MIN_CONFIDENCE_MACRO_PREFIX = "minConfidence";
|
|
1363
|
+
/** `minConfidencePerson` → `person`; bare `minConfidence` → null (generic). */
|
|
1364
|
+
function macroOfMinConfidenceKey(key) {
|
|
1365
|
+
if (!key.startsWith(MIN_CONFIDENCE_MACRO_PREFIX)) return null;
|
|
1366
|
+
const rest = key.slice(13);
|
|
1367
|
+
if (rest.length === 0) return null;
|
|
1368
|
+
return rest.charAt(0).toLowerCase() + rest.slice(1);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* The macro classes a model's OWN labelmap can reach through the step's
|
|
1372
|
+
* classMap. `undefined` ⇒ the model declares no labels (catalog default
|
|
1373
|
+
* labelmap) — every macro of the step is reachable, nothing to annotate.
|
|
1374
|
+
*/
|
|
1375
|
+
function reachableMacros(def, entry) {
|
|
1376
|
+
if (entry.labels.length === 0) return void 0;
|
|
1377
|
+
const map = def.classMap;
|
|
1378
|
+
if (!map) return new Set(entry.labels.map((l) => l.id));
|
|
1379
|
+
if (map.preserveOriginal) return void 0;
|
|
1380
|
+
const macros = /* @__PURE__ */ new Set();
|
|
1381
|
+
for (const label of entry.labels) {
|
|
1382
|
+
const mapped = map.mapping[label.id];
|
|
1383
|
+
if (mapped !== void 0) macros.add(mapped);
|
|
1384
|
+
}
|
|
1385
|
+
return macros;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* The labels a custom model declares that the step's dropping classMap
|
|
1389
|
+
* (`preserveOriginal: false`) removes UPSTREAM of every operator setting.
|
|
1390
|
+
* `null` when the model declares no labels, the step has no classMap, the map
|
|
1391
|
+
* preserves originals, or nothing is dropped. The engine factory logs this at
|
|
1392
|
+
* pool-config time — a dropped class must never be silent (CLAUDE.md logging
|
|
1393
|
+
* rule; the discard trail alone answers "what", not "why zero forever").
|
|
1394
|
+
*/
|
|
1395
|
+
function collectClassMapDropWarning(def, entry) {
|
|
1396
|
+
if (entry.labels.length === 0) return null;
|
|
1397
|
+
const map = def.classMap;
|
|
1398
|
+
if (!map || map.preserveOriginal) return null;
|
|
1399
|
+
const unmapped = entry.labels.filter((l) => map.mapping[l.id] === void 0).map((l) => l.id);
|
|
1400
|
+
if (unmapped.length === 0) return null;
|
|
1401
|
+
return {
|
|
1402
|
+
unmappedLabels: unmapped,
|
|
1403
|
+
mappedCount: entry.labels.length - unmapped.length
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function toApplicability(acc) {
|
|
1407
|
+
const hasInapplicable = Object.keys(acc.inapplicable).length > 0;
|
|
1408
|
+
const hasOptions = Object.keys(acc.optionsByModel).length > 0;
|
|
1409
|
+
if (!hasInapplicable && !hasOptions) return void 0;
|
|
1410
|
+
return {
|
|
1411
|
+
...hasInapplicable ? { inapplicable: acc.inapplicable } : {},
|
|
1412
|
+
...hasOptions ? { optionsByModel: acc.optionsByModel } : {}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function isAnnotatable(field) {
|
|
1416
|
+
return field.type === "slider" || field.type === "number" || field.type === "multiselect" || field.type === "select" || field.type === "boolean";
|
|
1417
|
+
}
|
|
1418
|
+
function annotateField(field, inputs) {
|
|
1419
|
+
if (!isAnnotatable(field)) return field;
|
|
1420
|
+
const { def, models, customModelIds } = inputs;
|
|
1421
|
+
const acc = {
|
|
1422
|
+
inapplicable: {},
|
|
1423
|
+
optionsByModel: {}
|
|
1424
|
+
};
|
|
1425
|
+
const macroKey = macroOfMinConfidenceKey(field.key);
|
|
1426
|
+
for (const entry of models) {
|
|
1427
|
+
const postprocessor = resolveEffectivePostprocessorForEntry(def, entry);
|
|
1428
|
+
if (field.key === "nmsIouThreshold" && !postprocessorConsumesNms(postprocessor)) {
|
|
1429
|
+
acc.inapplicable[entry.id] = `NMS is fused inside this model's "${postprocessor}" export — the pipeline IoU threshold is not consulted.`;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (field.key === "rectify" && postprocessor !== "ctc") {
|
|
1433
|
+
acc.inapplicable[entry.id] = `The "${postprocessor}" reader consumes the raw plate box — its crop policy never deskews, so this switch has no effect.`;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (macroKey !== null) {
|
|
1437
|
+
const macros = reachableMacros(def, entry);
|
|
1438
|
+
if (macros !== void 0 && !macros.has(macroKey)) acc.inapplicable[entry.id] = `This model's labelmap cannot produce "${macroKey}" — the slider filters a class that never arrives.`;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (field.key === "enabledMacroClasses" && field.type === "multiselect") {
|
|
1442
|
+
const macros = reachableMacros(def, entry);
|
|
1443
|
+
if (macros === void 0) continue;
|
|
1444
|
+
const reachable = field.options.filter((o) => macros.has(o.value));
|
|
1445
|
+
if (reachable.length === 0) acc.inapplicable[entry.id] = "None of this model's labels map to a macro class the pipeline tracks — every detection it emits is dropped upstream of this filter.";
|
|
1446
|
+
else if (reachable.length < field.options.length) acc.optionsByModel[entry.id] = reachable.map((o) => ({
|
|
1447
|
+
value: o.value,
|
|
1448
|
+
label: o.label
|
|
1449
|
+
}));
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (field.key === "enabledClasses" && field.type === "multiselect" && customModelIds.has(entry.id) && entry.labels.length > 0) {
|
|
1453
|
+
acc.optionsByModel[entry.id] = entry.labels.map((l) => ({
|
|
1454
|
+
value: l.id,
|
|
1455
|
+
label: l.name
|
|
1456
|
+
}));
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
const applicability = toApplicability(acc);
|
|
1461
|
+
if (applicability === void 0) return field;
|
|
1462
|
+
return {
|
|
1463
|
+
...field,
|
|
1464
|
+
modelApplicability: applicability
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Decorate a step's config schema with per-model applicability, computed from
|
|
1469
|
+
* the same model union the picker offers. Fields with nothing to say are
|
|
1470
|
+
* returned untouched (identity), so a static-only step with fully-consuming
|
|
1471
|
+
* models produces a byte-identical schema.
|
|
1472
|
+
*/
|
|
1473
|
+
function annotateConfigSchemaForModels(inputs) {
|
|
1474
|
+
return inputs.configSchema.map((field) => annotateField(field, inputs));
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* A persisted settings blob is external data — it survives schema changes,
|
|
1478
|
+
* hand edits and migrations. Accept only a finite IoU in the open (0,1)
|
|
1479
|
+
* interval; anything else falls back rather than reaching the decode, where a
|
|
1480
|
+
* 0 would suppress every overlapping box and a 1 would suppress none.
|
|
1481
|
+
*/
|
|
1482
|
+
function readIouThreshold(value) {
|
|
1483
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
1484
|
+
if (value <= 0 || value >= 1) return void 0;
|
|
1485
|
+
return value;
|
|
1486
|
+
}
|
|
1487
|
+
/** The effective decode-scoped values for a `(step, model)` pair. */
|
|
1488
|
+
function resolvePoolDecodeSettings(inputs) {
|
|
1489
|
+
const fallback = readIouThreshold(inputs.fallbackNmsIouThreshold) ?? .45;
|
|
1490
|
+
if (!postprocessorConsumesNms(resolveEffectivePostprocessorForEntry(inputs.def, inputs.modelEntry))) return { nmsIouThreshold: fallback };
|
|
1491
|
+
return { nmsIouThreshold: readIouThreshold(inputs.settings?.["nmsIouThreshold"]) ?? fallback };
|
|
1492
|
+
}
|
|
1493
|
+
/** What a built pool config actually carries — the state the slot is IN after
|
|
1494
|
+
* a `load`, so the manager's diff is anchored to the command it sent, never
|
|
1495
|
+
* to a second derivation of the same inputs. */
|
|
1496
|
+
function poolDecodeSettingsOf(config) {
|
|
1497
|
+
return { nmsIouThreshold: config.nmsIouThreshold ?? .45 };
|
|
1498
|
+
}
|
|
1499
|
+
function poolDecodeSettingsEqual(a, b) {
|
|
1500
|
+
return a.nmsIouThreshold === b.nmsIouThreshold;
|
|
1501
|
+
}
|
|
1502
|
+
//#endregion
|
|
1306
1503
|
//#region src/detection-pipeline/engine/pipeline-model-manager.ts
|
|
1307
1504
|
var PipelineModelManager = class {
|
|
1308
1505
|
pool;
|
|
1309
|
-
|
|
1506
|
+
source;
|
|
1310
1507
|
/** stepId → modelId → loaded entry. */
|
|
1311
1508
|
loaded = /* @__PURE__ */ new Map();
|
|
1312
1509
|
/** stepId → currently-active modelId (the runtime choice). */
|
|
@@ -1315,9 +1512,9 @@ var PipelineModelManager = class {
|
|
|
1315
1512
|
lruClock = 0;
|
|
1316
1513
|
log;
|
|
1317
1514
|
maxModelsPerStep;
|
|
1318
|
-
constructor(pool,
|
|
1515
|
+
constructor(pool, source, logger, options) {
|
|
1319
1516
|
this.pool = pool;
|
|
1320
|
-
this.
|
|
1517
|
+
this.source = source;
|
|
1321
1518
|
this.log = logger;
|
|
1322
1519
|
this.maxModelsPerStep = options?.maxModelsPerStep ?? 4;
|
|
1323
1520
|
}
|
|
@@ -1341,7 +1538,7 @@ var PipelineModelManager = class {
|
|
|
1341
1538
|
} });
|
|
1342
1539
|
}
|
|
1343
1540
|
for (const added of diff.added) {
|
|
1344
|
-
await this.ensureLoaded(added.addonId, added.modelId);
|
|
1541
|
+
await this.ensureLoaded(added.addonId, added.modelId, added.settings);
|
|
1345
1542
|
this.activeByStep.set(added.addonId, added.modelId);
|
|
1346
1543
|
this.log.info("Step loaded", { meta: {
|
|
1347
1544
|
step: added.addonId,
|
|
@@ -1350,7 +1547,7 @@ var PipelineModelManager = class {
|
|
|
1350
1547
|
} });
|
|
1351
1548
|
}
|
|
1352
1549
|
for (const { step, oldModelId } of diff.modelChanged) {
|
|
1353
|
-
await this.ensureLoaded(step.addonId, step.modelId);
|
|
1550
|
+
await this.ensureLoaded(step.addonId, step.modelId, step.settings);
|
|
1354
1551
|
this.activeByStep.set(step.addonId, step.modelId);
|
|
1355
1552
|
this.log.info("Active model switched", { meta: {
|
|
1356
1553
|
step: step.addonId,
|
|
@@ -1359,6 +1556,7 @@ var PipelineModelManager = class {
|
|
|
1359
1556
|
} });
|
|
1360
1557
|
await this.enforceLruCap(step.addonId);
|
|
1361
1558
|
}
|
|
1559
|
+
for (const { entry, step } of diff.unchanged) await this.reconcileDecode(entry, step.settings);
|
|
1362
1560
|
}
|
|
1363
1561
|
/**
|
|
1364
1562
|
* Get an `IInferenceEngine` handle. Without `modelId`, returns the
|
|
@@ -1380,6 +1578,22 @@ var PipelineModelManager = class {
|
|
|
1380
1578
|
isLoadedWithModel(stepId, modelId) {
|
|
1381
1579
|
return this.loaded.get(stepId)?.has(modelId) ?? false;
|
|
1382
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Does the pool NOT yet reflect this step? True when the (step, model) pair
|
|
1583
|
+
* is absent, and also when it is resident with different decode-scoped
|
|
1584
|
+
* settings — the case `isLoadedWithModel` cannot see.
|
|
1585
|
+
*
|
|
1586
|
+
* This is the per-dispatch skip gate: callers that short-circuit on
|
|
1587
|
+
* "already loaded" were the reason a saved threshold never reached a
|
|
1588
|
+
* running pool. Cheap by contract — `resolveDecode` never touches the fs.
|
|
1589
|
+
*/
|
|
1590
|
+
needsPoolUpdate(step) {
|
|
1591
|
+
const entry = this.loaded.get(step.addonId)?.get(step.modelId);
|
|
1592
|
+
if (!entry) return true;
|
|
1593
|
+
const desired = this.source.resolveDecode(step.addonId, step.modelId, step.settings);
|
|
1594
|
+
if (desired === null) return true;
|
|
1595
|
+
return !poolDecodeSettingsEqual(entry.decode, desired);
|
|
1596
|
+
}
|
|
1383
1597
|
/** ModelId currently designated as active for the step. */
|
|
1384
1598
|
getLoadedModelId(stepId) {
|
|
1385
1599
|
return this.activeByStep.get(stepId);
|
|
@@ -1431,8 +1645,7 @@ var PipelineModelManager = class {
|
|
|
1431
1645
|
*/
|
|
1432
1646
|
async loadAdditional(steps) {
|
|
1433
1647
|
for (const step of steps) {
|
|
1434
|
-
|
|
1435
|
-
await this.ensureLoaded(step.addonId, step.modelId);
|
|
1648
|
+
await this.ensureLoaded(step.addonId, step.modelId, step.settings);
|
|
1436
1649
|
if (!this.activeByStep.has(step.addonId)) this.activeByStep.set(step.addonId, step.modelId);
|
|
1437
1650
|
}
|
|
1438
1651
|
}
|
|
@@ -1441,7 +1654,7 @@ var PipelineModelManager = class {
|
|
|
1441
1654
|
* Honors `maxModelsPerStep` — evicts a non-active LRU variant first
|
|
1442
1655
|
* when the cap would otherwise be exceeded.
|
|
1443
1656
|
*/
|
|
1444
|
-
async ensureLoaded(stepId, modelId) {
|
|
1657
|
+
async ensureLoaded(stepId, modelId, settings) {
|
|
1445
1658
|
let perStep = this.loaded.get(stepId);
|
|
1446
1659
|
if (!perStep) {
|
|
1447
1660
|
perStep = /* @__PURE__ */ new Map();
|
|
@@ -1450,6 +1663,7 @@ var PipelineModelManager = class {
|
|
|
1450
1663
|
const existing = perStep.get(modelId);
|
|
1451
1664
|
if (existing) {
|
|
1452
1665
|
this.touch(existing);
|
|
1666
|
+
await this.reconcileDecode(existing, settings);
|
|
1453
1667
|
return existing;
|
|
1454
1668
|
}
|
|
1455
1669
|
while (perStep.size >= this.maxModelsPerStep) {
|
|
@@ -1464,11 +1678,13 @@ var PipelineModelManager = class {
|
|
|
1464
1678
|
} });
|
|
1465
1679
|
}
|
|
1466
1680
|
const index = this.pool.allocateIndex();
|
|
1467
|
-
const config = this.
|
|
1681
|
+
const config = this.source.buildConfig(stepId, modelId, settings);
|
|
1682
|
+
const decode = poolDecodeSettingsOf(config);
|
|
1468
1683
|
this.log.info("Loading step variant", { meta: {
|
|
1469
1684
|
step: stepId,
|
|
1470
1685
|
modelId,
|
|
1471
|
-
poolIndex: index
|
|
1686
|
+
poolIndex: index,
|
|
1687
|
+
...decode
|
|
1472
1688
|
} });
|
|
1473
1689
|
const { loadMs } = await this.pool.loadModel(index, config);
|
|
1474
1690
|
this.log.info("Step variant loaded", { meta: {
|
|
@@ -1481,11 +1697,71 @@ var PipelineModelManager = class {
|
|
|
1481
1697
|
stepId,
|
|
1482
1698
|
modelId,
|
|
1483
1699
|
poolIndex: index,
|
|
1484
|
-
lruTick: ++this.lruClock
|
|
1700
|
+
lruTick: ++this.lruClock,
|
|
1701
|
+
decode
|
|
1485
1702
|
};
|
|
1486
1703
|
perStep.set(modelId, entry);
|
|
1487
1704
|
return entry;
|
|
1488
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Push a retuned decode setting into ONE already-loaded slot.
|
|
1708
|
+
*
|
|
1709
|
+
* Scope is the point: `reconfigure` names a single pool index, so a slider
|
|
1710
|
+
* moved on `object-detection/yolo26n` never disturbs the face detector, the
|
|
1711
|
+
* classifiers, or the same step's warm benchmark variants — and it never
|
|
1712
|
+
* costs a session reload (see `SharedInferencePool.reconfigureModel`).
|
|
1713
|
+
*
|
|
1714
|
+
* A no-op when nothing moved: this runs on the per-dispatch path, so the
|
|
1715
|
+
* common case must send zero commands.
|
|
1716
|
+
*/
|
|
1717
|
+
async reconcileDecode(entry, settings) {
|
|
1718
|
+
const desired = this.source.resolveDecode(entry.stepId, entry.modelId, settings);
|
|
1719
|
+
if (desired === null) return;
|
|
1720
|
+
if (poolDecodeSettingsEqual(entry.decode, desired)) return;
|
|
1721
|
+
const previous = entry.decode;
|
|
1722
|
+
await this.pool.reconfigureModel(entry.poolIndex, desired);
|
|
1723
|
+
entry.decode = desired;
|
|
1724
|
+
this.log.info("Decode settings retuned in place — no model reload", { meta: {
|
|
1725
|
+
step: entry.stepId,
|
|
1726
|
+
modelId: entry.modelId,
|
|
1727
|
+
poolIndex: entry.poolIndex,
|
|
1728
|
+
previousNmsIouThreshold: previous.nmsIouThreshold,
|
|
1729
|
+
...desired
|
|
1730
|
+
} });
|
|
1731
|
+
this.reportContention(entry, previous, desired);
|
|
1732
|
+
}
|
|
1733
|
+
/** Decode values each (step, model) pair has already run, so a value
|
|
1734
|
+
* RETURNING can be told apart from a value being changed. */
|
|
1735
|
+
decodeHistory = /* @__PURE__ */ new Map();
|
|
1736
|
+
contentionReported = /* @__PURE__ */ new Set();
|
|
1737
|
+
/**
|
|
1738
|
+
* A pool slot is NODE-level; the settings driving it arrive per dispatch, so
|
|
1739
|
+
* two cameras with different per-camera overrides for the same (step, model)
|
|
1740
|
+
* pull the same slot back and forth. That is not expressible — one of them is
|
|
1741
|
+
* always decoding with the other's threshold — and it is invisible from the
|
|
1742
|
+
* outside, so it is reported the first time a pair flip-flops BACK to a value
|
|
1743
|
+
* it already ran. One steady retune (the operator moving a slider) never
|
|
1744
|
+
* trips it; alternating writers do, immediately.
|
|
1745
|
+
*/
|
|
1746
|
+
reportContention(entry, previous, desired) {
|
|
1747
|
+
const key = `${entry.stepId}::${entry.modelId}`;
|
|
1748
|
+
let seen = this.decodeHistory.get(key);
|
|
1749
|
+
if (!seen) {
|
|
1750
|
+
seen = new Set([previous.nmsIouThreshold]);
|
|
1751
|
+
this.decodeHistory.set(key, seen);
|
|
1752
|
+
}
|
|
1753
|
+
const returning = seen.has(desired.nmsIouThreshold);
|
|
1754
|
+
seen.add(desired.nmsIouThreshold);
|
|
1755
|
+
if (!returning || this.contentionReported.has(key)) return;
|
|
1756
|
+
this.contentionReported.add(key);
|
|
1757
|
+
this.log.warn("Two dispatches disagree on this step’s decode settings — the pool slot is node-level, so one of them always decodes with the other’s value", { meta: {
|
|
1758
|
+
step: entry.stepId,
|
|
1759
|
+
modelId: entry.modelId,
|
|
1760
|
+
poolIndex: entry.poolIndex,
|
|
1761
|
+
valuesSeen: [...seen].sort((a, b) => a - b),
|
|
1762
|
+
hint: "a per-camera override of a decode-scoped knob cannot be honoured on a shared pool — keep this knob at the node level"
|
|
1763
|
+
} });
|
|
1764
|
+
}
|
|
1489
1765
|
async unloadEntry(entry) {
|
|
1490
1766
|
await this.pool.unloadModel(entry.poolIndex);
|
|
1491
1767
|
const perStep = this.loaded.get(entry.stepId);
|
|
@@ -1551,7 +1827,10 @@ var PipelineModelManager = class {
|
|
|
1551
1827
|
step: desired,
|
|
1552
1828
|
oldModelId: activeModelId
|
|
1553
1829
|
});
|
|
1554
|
-
else unchanged.push(
|
|
1830
|
+
else unchanged.push({
|
|
1831
|
+
entry: activeEntry,
|
|
1832
|
+
step: desired
|
|
1833
|
+
});
|
|
1555
1834
|
}
|
|
1556
1835
|
for (const [stepId, step] of desiredMap) if (!this.activeByStep.has(stepId)) added.push(step);
|
|
1557
1836
|
return {
|
|
@@ -1563,21 +1842,6 @@ var PipelineModelManager = class {
|
|
|
1563
1842
|
}
|
|
1564
1843
|
};
|
|
1565
1844
|
//#endregion
|
|
1566
|
-
//#region src/detection-pipeline/registry/effective-postprocessor.ts
|
|
1567
|
-
function resolveEffectivePostprocessor(definition, modelId) {
|
|
1568
|
-
return resolveEffectivePostprocessorForEntry(definition, modelId ? definition.models.find((m) => m.id === modelId) : void 0);
|
|
1569
|
-
}
|
|
1570
|
-
/**
|
|
1571
|
-
* Entry-based variant for callers that already hold the `ModelCatalogEntry` —
|
|
1572
|
-
* required for CUSTOM (operator-registered) models, which are NOT in
|
|
1573
|
-
* `definition.models` and would otherwise silently fall back to the step
|
|
1574
|
-
* default decode (a Frigate `yolonas` model decoded as `yolo` produces no
|
|
1575
|
-
* error anywhere, just zero detections).
|
|
1576
|
-
*/
|
|
1577
|
-
function resolveEffectivePostprocessorForEntry(definition, entry) {
|
|
1578
|
-
return entry?.postprocessor ?? definition.postprocessor;
|
|
1579
|
-
}
|
|
1580
|
-
//#endregion
|
|
1581
1845
|
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1582
1846
|
/**
|
|
1583
1847
|
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
@@ -1844,6 +2108,14 @@ var EngineFactory = class {
|
|
|
1844
2108
|
return this.poolManager?.isLoadedWithModel(stepId, modelId) ?? false;
|
|
1845
2109
|
}
|
|
1846
2110
|
/**
|
|
2111
|
+
* Does the pool still owe this step something — a load, or a retune of its
|
|
2112
|
+
* decode-scoped settings? The skip gate every dispatch path must use instead
|
|
2113
|
+
* of `isLoadedWithModel`, which cannot see a changed threshold (D214).
|
|
2114
|
+
*/
|
|
2115
|
+
needsPoolUpdate(step) {
|
|
2116
|
+
return this.poolManager?.needsPoolUpdate(step) ?? true;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
1847
2119
|
* List every loaded (stepId, modelId) variant — the pool surfaces each
|
|
1848
2120
|
* warm slot independently (including bench overrides).
|
|
1849
2121
|
*/
|
|
@@ -1979,19 +2251,57 @@ var EngineFactory = class {
|
|
|
1979
2251
|
numWorkers,
|
|
1980
2252
|
...this.opts.engine.device ? { device: this.opts.engine.device } : {}
|
|
1981
2253
|
});
|
|
1982
|
-
this.poolManager = new PipelineModelManager(this.pool,
|
|
2254
|
+
this.poolManager = new PipelineModelManager(this.pool, {
|
|
2255
|
+
buildConfig: (stepId, modelId, settings) => this.buildPoolModelConfig(stepId, modelId, poolRuntime, settings),
|
|
2256
|
+
resolveDecode: (stepId, modelId, settings) => this.resolveDecode(stepId, modelId, settings)
|
|
2257
|
+
}, this.log.child("model-mgr"));
|
|
1983
2258
|
await this.pool.initialize([]);
|
|
1984
2259
|
await this.poolManager.applyConfig(steps);
|
|
1985
2260
|
}
|
|
1986
|
-
|
|
2261
|
+
/** (step, model) pairs whose classMap-drop warning already logged — the
|
|
2262
|
+
* pool reloads models on every applyConfig; the fact doesn't change. */
|
|
2263
|
+
classMapDropLogged = /* @__PURE__ */ new Set();
|
|
2264
|
+
/**
|
|
2265
|
+
* The fs-free half of {@link PoolModelSource} — the effective decode-scoped
|
|
2266
|
+
* settings for a `(step, model, settings)` triple. Runs on the per-dispatch
|
|
2267
|
+
* path, so it must resolve the catalog entry and nothing more; the label
|
|
2268
|
+
* file, the format build and the model path are load-time concerns.
|
|
2269
|
+
*
|
|
2270
|
+
* `null` when the pair resolves to no model at all — the caller routes that
|
|
2271
|
+
* to `buildConfig`, which throws the message naming the step and the model.
|
|
2272
|
+
*/
|
|
2273
|
+
resolveDecode(stepId, modelId, settings) {
|
|
2274
|
+
const def = require_event_loop_stall_monitor.getStepDefinition(stepId);
|
|
2275
|
+
const modelEntry = def.models.find((m) => m.id === modelId) ?? this.opts.resolveCustomModel?.(stepId, modelId);
|
|
2276
|
+
if (!modelEntry) return null;
|
|
2277
|
+
return resolvePoolDecodeSettings({
|
|
2278
|
+
def,
|
|
2279
|
+
modelEntry,
|
|
2280
|
+
settings,
|
|
2281
|
+
fallbackNmsIouThreshold: this.opts.nmsIouThreshold
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
buildPoolModelConfig(stepId, modelId, poolRuntime, stepSettings) {
|
|
1987
2285
|
return buildPoolModelConfigForStep({
|
|
1988
2286
|
stepId,
|
|
1989
2287
|
modelId,
|
|
1990
2288
|
poolRuntime,
|
|
1991
2289
|
modelsDir: this.opts.modelsDir,
|
|
1992
2290
|
nmsIouThreshold: this.opts.nmsIouThreshold,
|
|
2291
|
+
stepSettings,
|
|
1993
2292
|
device: this.opts.engine.device,
|
|
1994
|
-
resolveCustomModel: this.opts.resolveCustomModel
|
|
2293
|
+
resolveCustomModel: this.opts.resolveCustomModel,
|
|
2294
|
+
onClassMapDrop: ({ stepId: sid, modelId: mid, unmappedLabels }) => {
|
|
2295
|
+
const key = `${sid}::${mid}`;
|
|
2296
|
+
if (this.classMapDropLogged.has(key)) return;
|
|
2297
|
+
this.classMapDropLogged.add(key);
|
|
2298
|
+
this.log.warn("Model labels dropped by step classMap — unreachable by any setting", { meta: {
|
|
2299
|
+
step: sid,
|
|
2300
|
+
modelId: mid,
|
|
2301
|
+
unmappedLabels: [...unmappedLabels],
|
|
2302
|
+
hint: "these classes are removed before enabledMacroClasses / minConfidence* run; only labels in the step classMap are tracked"
|
|
2303
|
+
} });
|
|
2304
|
+
}
|
|
1995
2305
|
});
|
|
1996
2306
|
}
|
|
1997
2307
|
};
|
|
@@ -2010,6 +2320,12 @@ function buildPoolModelConfigForStep(inputs) {
|
|
|
2010
2320
|
if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${stepId}" catalog (static or custom registry)`);
|
|
2011
2321
|
const format = RUNTIME_TO_FORMAT[poolRuntime];
|
|
2012
2322
|
if (!format) throw new Error(`No format mapping for pool runtime "${poolRuntime}"`);
|
|
2323
|
+
const dropWarning = collectClassMapDropWarning(def, modelEntry);
|
|
2324
|
+
if (dropWarning !== null) inputs.onClassMapDrop?.({
|
|
2325
|
+
stepId,
|
|
2326
|
+
modelId,
|
|
2327
|
+
unmappedLabels: dropWarning.unmappedLabels
|
|
2328
|
+
});
|
|
2013
2329
|
const formatEntry = modelEntry.formats[format];
|
|
2014
2330
|
if (!formatEntry) throw new Error(`Model "${modelId}" has no ${format} format. Available: ${Object.keys(modelEntry.formats).join(", ")}`);
|
|
2015
2331
|
const urlParts = formatEntry.url.split("/");
|
|
@@ -2031,7 +2347,12 @@ function buildPoolModelConfigForStep(inputs) {
|
|
|
2031
2347
|
outputProbabilities: modelEntry.outputProbabilities,
|
|
2032
2348
|
postprocessor: resolveEffectivePostprocessorForEntry(def, modelEntry),
|
|
2033
2349
|
confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
|
|
2034
|
-
nmsIouThreshold:
|
|
2350
|
+
nmsIouThreshold: resolvePoolDecodeSettings({
|
|
2351
|
+
def,
|
|
2352
|
+
modelEntry,
|
|
2353
|
+
settings: inputs.stepSettings,
|
|
2354
|
+
fallbackNmsIouThreshold: inputs.nmsIouThreshold
|
|
2355
|
+
}).nmsIouThreshold,
|
|
2035
2356
|
labels,
|
|
2036
2357
|
charset: def.charset,
|
|
2037
2358
|
plateRegion: def.plateRegion,
|
|
@@ -6951,7 +7272,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6951
7272
|
this.executor = executor;
|
|
6952
7273
|
const dispatchEngine = dispatchDeviceKey ? resolveDeviceEngine(dispatchDeviceKey) : runEngine;
|
|
6953
7274
|
const dispatchFactory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
|
|
6954
|
-
const needed = enabledSteps.filter((s) =>
|
|
7275
|
+
const needed = enabledSteps.filter((s) => dispatchFactory.needsPoolUpdate(s));
|
|
6955
7276
|
if (needed.length > 0) {
|
|
6956
7277
|
this.log.info("Benchmark: models to load", { meta: {
|
|
6957
7278
|
count: needed.length,
|
|
@@ -7392,7 +7713,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
7392
7713
|
const needed = [];
|
|
7393
7714
|
for (const step of allEnabled) {
|
|
7394
7715
|
if (!step.enabled) continue;
|
|
7395
|
-
if (factory
|
|
7716
|
+
if (factory !== null && !factory.needsPoolUpdate(step)) continue;
|
|
7396
7717
|
needed.push(step);
|
|
7397
7718
|
}
|
|
7398
7719
|
if (needed.length === 0) return;
|
|
@@ -7551,7 +7872,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
7551
7872
|
try {
|
|
7552
7873
|
const factory = dispatchDeviceKey ? await this.resolveDeviceFactory(dispatchDeviceKey) : this.engineFactory;
|
|
7553
7874
|
if (!factory) throw new Error("runPipelineBatch: factory not initialised");
|
|
7554
|
-
if (enabledSteps.filter((s) =>
|
|
7875
|
+
if (enabledSteps.filter((s) => factory.needsPoolUpdate(s)).length > 0) await this.ensureModelsForSteps(benchmarkSteps, factory, resolveFormat);
|
|
7555
7876
|
const canFastPath = singleRoot && allRaw && uniformDims && factory.supportsBatch();
|
|
7556
7877
|
this.log.info("runPipelineBatch path decision", { meta: {
|
|
7557
7878
|
phase: "batch",
|
|
@@ -8260,7 +8581,13 @@ function buildSchemaSlots(formats, modelsDir, customByStep) {
|
|
|
8260
8581
|
if (availableModels.length === 0) continue;
|
|
8261
8582
|
const slot = step.slot;
|
|
8262
8583
|
if (!slotMap.has(slot)) slotMap.set(slot, []);
|
|
8263
|
-
const
|
|
8584
|
+
const customIds = new Set((customByStep?.get(step.id) ?? []).map((m) => m.id));
|
|
8585
|
+
const configSchema = annotateConfigSchemaForModels({
|
|
8586
|
+
def: step,
|
|
8587
|
+
configSchema: pipelineStep.getConfigSchema(),
|
|
8588
|
+
models: availableModels,
|
|
8589
|
+
customModelIds: customIds
|
|
8590
|
+
});
|
|
8264
8591
|
slotMap.get(slot).push({
|
|
8265
8592
|
id: step.id,
|
|
8266
8593
|
name: step.name,
|