@camstack/addon-pipeline 1.1.69 → 1.1.71

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.
Files changed (35) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +215 -485
  4. package/dist/detection-pipeline/index.mjs +216 -485
  5. package/dist/{dist-BT4Ug2Bf.mjs → dist-DyALtmD7.mjs} +240 -45
  6. package/dist/{dist-DUcDVlg7.js → dist-DzTjxf39.js} +251 -44
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +24 -2
  10. package/dist/pipeline-runner/index.mjs +24 -2
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/session-decode/decode-worker-child.js +177 -12
  14. package/dist/session-decode/decode-worker-child.mjs +177 -12
  15. package/dist/{step-definitions-4tt-nRM4.js → step-definitions-Djy8aZLt.js} +6 -2
  16. package/dist/{step-definitions-DO0hIDMx.mjs → step-definitions-rmBALHX4.mjs} +6 -2
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CP4DpABx.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CMP316sx.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BflyNuZv.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-5Zc9wj-A.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-Do2ymS0O.mjs → hostInit-DLJNx0Ly.mjs} +2 -2
  22. package/dist/stream-broker/index.js +1 -1
  23. package/dist/stream-broker/index.mjs +1 -1
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DlI3udxg.js → MaskShapeCanvas-DI4BY7W2-sOOxiGvP.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Du8J0kED.js → MotionZonesSettings-NcxxQN8r-DdTQuT23.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DSST47WI.js → PrivacyMaskSettings-APgPLF7p-BrR0L3ql.js} +1 -1
  28. package/embed-dist/assets/index-D4MGuECB.css +2 -0
  29. package/embed-dist/assets/{index-B2ux2xJ7.js → index-Dk21l1_3.js} +14 -14
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +30 -5
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D68Lj69v.mjs +0 -26
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DlALQ_wp.mjs +0 -26
  35. package/embed-dist/assets/index-D2pF9Z3W.css +0 -2
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, lt as string, m as YAMNET_TO_MACRO, st as object, t as APPLE_SA_TO_MACRO, tt as array, ut as union } from "../dist-BT4Ug2Bf.mjs";
3
- import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-DO0hIDMx.mjs";
2
+ import { B as supportedRuntimes$1, C as detectionPipelineCapability, H as errMsg, J as hydrateSchema, L as runtimeDevices$1, M as pipelineExecutorCapability, Q as parseJsonUnknown, T as evaluateZoneRules, U as BaseAddon, dt as string, ft as union, h as YAMNET_TO_MACRO, j as nodePin, lt as object, pt as EventCategory, q as createEvent, rt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, tt as sleep, w as enumerateInferenceDevices, x as defaultDeviceFor$1 } from "../dist-DyALtmD7.mjs";
3
+ import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-rmBALHX4.mjs";
4
4
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
5
5
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
6
6
  import { spawn } from "node:child_process";
@@ -156,9 +156,6 @@ function pythonRequirementsFor(id) {
156
156
  function tuningFor(id) {
157
157
  return RUNTIME_DETAIL[id].tuning;
158
158
  }
159
- function runtimeLabel(id) {
160
- return RUNTIME_DETAIL[id].label;
161
- }
162
159
  /**
163
160
  * Proactive-install hint kept for back-compat (re-exported from index.ts).
164
161
  * Intel on Linux warrants installing openvino; coremltools covers macOS;
@@ -194,117 +191,27 @@ function pickBestRuntime(env, bestBackendHint) {
194
191
  device: defaultDeviceFor(chosen)
195
192
  };
196
193
  }
197
- /**
198
- * The ordered device candidates a runtime will try for an `'auto'`/`'all'`
199
- * device pick — mirrors what the python inference pool actually resolves
200
- * (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
201
- * ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
202
- * accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
203
- * tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
204
- * caller then keeps the plain `backend/device` label.
205
- */
206
- function engineDeviceSequence(runtimeId, env) {
207
- const hw = env.hardware;
208
- switch (runtimeId) {
209
- case "openvino": {
210
- const seq = [];
211
- if (hw?.npu?.type === "intel-npu") seq.push("NPU");
212
- if (hw?.gpu?.type === "intel") seq.push("GPU");
213
- seq.push("CPU");
214
- return seq;
215
- }
216
- case "coreml": {
217
- const seq = [];
218
- if (hw?.npu?.type === "apple-ane") seq.push("ANE");
219
- if (hw?.gpu != null) seq.push("GPU");
220
- seq.push("CPU");
221
- return seq;
222
- }
223
- default: return [];
224
- }
225
- }
226
194
  //#endregion
227
195
  //#region src/detection-pipeline/engine-store-keys.ts
228
196
  /**
229
- * Per-node scoping for the detection-pipeline engine cascade.
197
+ * Node-id normalisation for the detection pipeline.
230
198
  *
231
- * The detection addon's settings store is a single CLUSTER-SHARED blob (the
232
- * settings-store cap is hub-resident; every node's detection instance reads and
233
- * writes the same keys). That is correct for node-agnostic settings (pipeline
234
- * steps, tuning) but WRONG for the engine cascade `engineBackend` /
235
- * `engineDevice` / `probedBestEngine` are hardware-specific, so the hub (NPU)
236
- * and a remote agent (iGPU, or no accelerator at all) must hold INDEPENDENT
237
- * selections. Sharing them lets one node's pick (e.g. `openvino/npu`) override
238
- * another node that has no NPU.
239
- *
240
- * These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
241
- * Everything else in the store stays shared. There is NO fallback to a legacy
242
- * un-scoped value for ANY node (the hub included) and no in-code migration: a
243
- * bare key (written before scoping, or by an older build) is inert — invisible
244
- * to every node. A node with no scoped key reads `undefined`, which drives its
245
- * OWN live resolve (`resolveAutoEngine` picks the node's runtime from the local
246
- * kernel each boot; `reprobeEngine` persists an explicit probe under
247
- * `<key>@<node>`). Serving a bare value to any node
248
- * would leak one node's hardware pick (e.g. `openvino/auto`) onto a node that
249
- * cannot run it (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). This
250
- * mirrors the decoder's `readNodeDecoderBackend` no-fallback rule.
199
+ * The legacy per-node engine CASCADE keys (`engineBackend` / `engineDevice` /
200
+ * `probedBestEngine`) were removed together with the operator-facing engine
201
+ * election: each node now runs a device-array of inference pools balanced
202
+ * per-session, and the node-default pool is derived live from the node's own
203
+ * hardware (`resolveAutoEngine`). All that survives from the old cascade module
204
+ * is the node-id normaliser, still used to target the platform-probe per node.
251
205
  */
252
- var ENGINE_CASCADE_KEYS = [
253
- "engineBackend",
254
- "engineDevice",
255
- "probedBestEngine"
256
- ];
257
- function isEngineCascadeKey(key) {
258
- return ENGINE_CASCADE_KEYS.includes(key);
259
- }
260
206
  /**
261
- * Normalise a raw kernel node id to the bare node id used for scoping.
262
- * `localNodeId` can carry a `<node>/<addon>` suffix; the engine selection is
263
- * per-NODE, so strip the addon segment. Falls back to `hub`.
207
+ * Normalise a raw kernel node id to the bare node id. `localNodeId` can carry a
208
+ * `<node>/<addon>` suffix; probe targeting is per-NODE, so strip the addon
209
+ * segment. Falls back to `hub`.
264
210
  */
265
211
  function normalizeEngineNodeId(rawNodeId) {
266
212
  const raw = rawNodeId ?? "hub";
267
213
  return raw.includes("/") ? raw.split("/")[0] ?? "hub" : raw;
268
214
  }
269
- /** The node-scoped store key for an engine cascade field. */
270
- function nodeEngineKey(base, nodeId) {
271
- return `${base}@${normalizeEngineNodeId(nodeId)}`;
272
- }
273
- /**
274
- * Read an engine cascade value for a node: the node-scoped key if present,
275
- * otherwise `undefined` — for EVERY node, the hub included. There is no
276
- * fallback to the legacy un-scoped key: a bare value in the store is inert.
277
- * A node with no scoped key gets `undefined` so its own platform-driven
278
- * auto-pick runs and persists a real per-node selection under `<key>@<node>`.
279
- * (A bare fallback would leak one node's engine selection cluster-wide — CB3.)
280
- */
281
- function readNodeEngineValue(store, base, nodeId) {
282
- return store[nodeEngineKey(base, nodeId)];
283
- }
284
- /**
285
- * Project a raw store onto the plain engine cascade keys for THIS node, so the
286
- * UI schema (whose field keys are the bare `engineBackend` etc.) hydrates from
287
- * the node's own selection. Non-engine keys are left untouched. Node-scoped
288
- * keys for OTHER nodes are dropped from the projection (not relevant to this
289
- * node's form).
290
- */
291
- function projectNodeEngine(store, nodeId) {
292
- const out = {};
293
- const scopedForAnyNode = /* @__PURE__ */ new Set();
294
- for (const key of Object.keys(store)) {
295
- const atIdx = key.indexOf("@");
296
- if (isEngineCascadeKey(atIdx >= 0 ? key.slice(0, atIdx) : key)) {
297
- scopedForAnyNode.add(key);
298
- continue;
299
- }
300
- out[key] = store[key];
301
- }
302
- for (const base of ENGINE_CASCADE_KEYS) {
303
- const value = readNodeEngineValue(store, base, nodeId);
304
- if (value !== void 0) out[base] = value;
305
- }
306
- return out;
307
- }
308
215
  //#endregion
309
216
  //#region src/detection-pipeline/default-detection-model.ts
310
217
  /** The object-detection step id — the only slot this resolver applies to. */
@@ -1542,28 +1449,17 @@ var EngineFactory = class {
1542
1449
  //#endregion
1543
1450
  //#region src/detection-pipeline/engine/resolve-device-engine.ts
1544
1451
  /**
1545
- * Backend → model format for a device pool. Mirrors the Phase-1 device
1546
- * enumeration (`enumerateInferenceDevices`) and the pool-runtime mapping.
1547
- */
1548
- var BACKEND_FORMAT = {
1549
- openvino: "openvino",
1550
- edgetpu: "tflite",
1551
- onnx: "onnx",
1552
- cpu: "onnx",
1553
- cuda: "onnx",
1554
- coreml: "coreml"
1555
- };
1556
- /**
1557
1452
  * Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
1558
1453
  * {@link PipelineEngineChoice} used to build that device's inference pool.
1559
1454
  * Throws on an unknown backend token so a bad `deviceKey` fails loud rather
1560
- * than silently building a wrong pool.
1455
+ * than silently building a wrong pool. Format comes from the shared
1456
+ * {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
1561
1457
  */
1562
1458
  function resolveDeviceEngine(deviceKey) {
1563
1459
  const colon = deviceKey.indexOf(":");
1564
1460
  const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
1565
1461
  const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
1566
- const format = BACKEND_FORMAT[backend];
1462
+ const format = DEVICE_BACKEND_TO_FORMAT[backend];
1567
1463
  if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
1568
1464
  return {
1569
1465
  runtime: "python",
@@ -1573,6 +1469,69 @@ function resolveDeviceEngine(deviceKey) {
1573
1469
  };
1574
1470
  }
1575
1471
  //#endregion
1472
+ //#region src/detection-pipeline/engine/idle-pool-reaper.ts
1473
+ var IdlePoolReaper = class {
1474
+ lastUsed = /* @__PURE__ */ new Map();
1475
+ timers = /* @__PURE__ */ new Map();
1476
+ ttlMs;
1477
+ disposeFn;
1478
+ now;
1479
+ setTimer;
1480
+ clearTimer;
1481
+ onError;
1482
+ constructor(opts) {
1483
+ this.ttlMs = opts.ttlMs;
1484
+ this.disposeFn = opts.dispose;
1485
+ this.now = opts.now ?? (() => Date.now());
1486
+ this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1487
+ this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
1488
+ this.onError = opts.onError;
1489
+ }
1490
+ /** Record a use of `key` and (re)arm its idle-eviction timer. */
1491
+ touch(key) {
1492
+ this.lastUsed.set(key, this.now());
1493
+ this.arm(key);
1494
+ }
1495
+ /** Stop tracking `key` (the pool was disposed/removed elsewhere). */
1496
+ cancel(key) {
1497
+ const timer = this.timers.get(key);
1498
+ if (timer) this.clearTimer(timer);
1499
+ this.timers.delete(key);
1500
+ this.lastUsed.delete(key);
1501
+ }
1502
+ /** Clear every timer (shutdown). Does not dispose pools — the caller does. */
1503
+ disposeAll() {
1504
+ for (const timer of this.timers.values()) this.clearTimer(timer);
1505
+ this.timers.clear();
1506
+ this.lastUsed.clear();
1507
+ }
1508
+ arm(key) {
1509
+ const existing = this.timers.get(key);
1510
+ if (existing) this.clearTimer(existing);
1511
+ this.timers.set(key, this.setTimer(() => {
1512
+ this.reap(key);
1513
+ }, this.ttlMs));
1514
+ }
1515
+ async reap(key) {
1516
+ const last = this.lastUsed.get(key);
1517
+ if (last === void 0) return;
1518
+ const idleMs = this.now() - last;
1519
+ if (idleMs < this.ttlMs) {
1520
+ this.timers.set(key, this.setTimer(() => {
1521
+ this.reap(key);
1522
+ }, this.ttlMs - idleMs));
1523
+ return;
1524
+ }
1525
+ this.timers.delete(key);
1526
+ this.lastUsed.delete(key);
1527
+ try {
1528
+ await this.disposeFn(key);
1529
+ } catch (err) {
1530
+ this.onError?.(key, err);
1531
+ }
1532
+ }
1533
+ };
1534
+ //#endregion
1576
1535
  //#region src/detection-pipeline/engine-provisioner.ts
1577
1536
  /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
1578
1537
  var BACKOFF_SCHEDULE_MS = [
@@ -3929,26 +3888,6 @@ var PROBE_DIRECT_RPC_TIMEOUT_MS = 6e4;
3929
3888
  * result is identical (onnx / cpu) but is now derived through the shared rules
3930
3889
  * instead of duplicated inline.
3931
3890
  */
3932
- function onnxFloorPick() {
3933
- return ONNX_FLOOR;
3934
- }
3935
- /**
3936
- * The platform-deterministic engine pick computed SYNCHRONOUSLY from this node's
3937
- * own `process.platform`/`arch` alone (no probe): darwin → coreml, else → onnx
3938
- * (gpu-dependent openvino/cuda need the probe and converge via the auto-pick).
3939
- * Used as the ENGINE fallback when a persisted selection is unsupported on this
3940
- * node — so a stale GLOBAL engine config (e.g. the cluster's OpenVINO choice)
3941
- * can never force an impossible engine onto a node whose platform rejects it.
3942
- */
3943
- function platformDefaultPick() {
3944
- const pick = pickBestRuntime(runtimeEnvFromProcess(null), null);
3945
- return {
3946
- runtime: "python",
3947
- backend: pick.runtimeId,
3948
- format: modelFormatFor(pick.runtimeId),
3949
- device: pick.device
3950
- };
3951
- }
3952
3891
  /**
3953
3892
  * Is `backend` even POSSIBLE on this node's OS/arch (ignoring gpu detail)?
3954
3893
  * coreml ⇒ darwin only; openvino ⇒ x64 non-darwin only; onnx ⇒ anywhere. Used to
@@ -4064,6 +4003,22 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4064
4003
  */
4065
4004
  overrideCache = null;
4066
4005
  overrideCacheTimer = null;
4006
+ /**
4007
+ * Idle-TTL for a per-device inference pool (multi-device C3). A device pool
4008
+ * with no dispatch for this long is disposed and recreated on next use — the
4009
+ * memory guardrail that keeps `factoriesByDevice` from accumulating warm pools
4010
+ * forever. Longer than the override cache (60s) since a device pool is heavier
4011
+ * to respin; still well above any real inter-session gap for an active camera.
4012
+ */
4013
+ static DEVICE_POOL_IDLE_TTL_MS = 12e4;
4014
+ deviceReaper = new IdlePoolReaper({
4015
+ ttlMs: DetectionPipelineProvider.DEVICE_POOL_IDLE_TTL_MS,
4016
+ dispose: (deviceKey) => this.disposeDevicePool(deviceKey),
4017
+ onError: (deviceKey, err) => this.log.warn("Idle device pool dispose failed", { meta: {
4018
+ deviceKey,
4019
+ error: err instanceof Error ? err.message : String(err)
4020
+ } })
4021
+ });
4067
4022
  static OVERRIDE_CACHE_TTL_MS = 6e4;
4068
4023
  /** Read the addon settings store (all keys). */
4069
4024
  readStore;
@@ -4091,20 +4046,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4091
4046
  format: this.currentEngine.format
4092
4047
  } });
4093
4048
  }
4094
- /** Load persisted engine choice. Call after construction. */
4049
+ /**
4050
+ * Call after construction. The node-default engine is always DERIVED live
4051
+ * from this node's own hardware (there is no persisted operator override
4052
+ * anymore — the elected single-engine cascade was removed). Defer the derive
4053
+ * to `setApi()` where the probe cap / kernel resolver is reachable.
4054
+ */
4095
4055
  async init() {
4096
- const stored = await this.loadEngine();
4097
- if (stored) {
4098
- this.currentEngine = stored;
4099
- this.log.info("Engine loaded (persisted)", { meta: {
4100
- runtime: stored.runtime,
4101
- backend: stored.backend,
4102
- format: stored.format
4103
- } });
4104
- } else {
4105
- this.needsAutoPick = true;
4106
- this.log.info("No persisted engine — auto-pick deferred to setApi()");
4107
- }
4056
+ this.needsAutoPick = true;
4057
+ this.log.info("Node-default engine derive deferred to setApi()");
4108
4058
  }
4109
4059
  /**
4110
4060
  * Lazy-install the pip requirements file matching `engine.backend` into
@@ -4178,6 +4128,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4178
4128
  await this.resolveAutoEngine();
4179
4129
  this.needsAutoPick = false;
4180
4130
  this.startProvisioningForCurrentEngine();
4131
+ await this.recomputeConfigIssues().catch((err) => {
4132
+ this.log.warn("config-issue recompute failed after node-default derive", { meta: { error: errMsg(err) } });
4133
+ });
4181
4134
  }
4182
4135
  /**
4183
4136
  * Authoritative platform-probe reconcile: query the probe provider
@@ -4205,34 +4158,38 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4205
4158
  }
4206
4159
  }
4207
4160
  /**
4208
- * Resolve the `'auto'` engine LIVE from this node's own kernel inference
4161
+ * Derive the node-default engine LIVE from this node's own kernel inference
4209
4162
  * resolver (`ctx.kernel.inferenceEngine`) — the runtime mirror of
4210
4163
  * `ctx.kernel.hwaccel`. Each node (hub + forked worker + remote agent)
4211
4164
  * resolves its OWN accelerators in-process, so a forked agent child never
4212
4165
  * adopts the hub's runtime from the `platform-probe` singleton.
4213
4166
  *
4214
- * NEVER persists. `'auto'` is a live-resolved sentinel: the pick is recomputed
4215
- * every boot from the local hardware, so it is inherently self-healing — the
4216
- * old `autoPickAndPersist` sticky-onnx bug (a premature onnx pick persisted
4217
- * during a boot race, then locked forever) cannot recur because the store is
4218
- * never written here. Only an explicit operator override is persisted, and
4219
- * that write happens through the settings UI, not this path.
4167
+ * The default is the TOP-SCORED CONCRETE device from the SAME
4168
+ * `enumerateInferenceDevices` enumeration the orchestrator balances over
4169
+ * (e.g. `openvino:npu`, not `openvino:auto`) NOT `pickBestRuntime`, whose
4170
+ * `defaultDeviceFor('openvino')` is the abstract `'auto'` sentinel. This is
4171
+ * load-bearing for pool COLLAPSE (D2/NEW-1): a `deviceKey`-less/lone session
4172
+ * routes through the default pool, and a balanced session that picks the same
4173
+ * top device (`openvino:npu`) must reuse THAT pool via `resolveDeviceFactory`'s
4174
+ * `enginesEqual(engine, currentEngine)` check — which only matches when the
4175
+ * default carries the concrete device, not `'auto'`. An abstract `'auto'`
4176
+ * default would spawn a THIRD pool contending with the per-device npu/gpu pools
4177
+ * on the same physical accelerators (double model load / VRAM). Falls back to
4178
+ * the onnx/cpu floor when the enumeration is empty. NEVER persists — recomputed
4179
+ * every boot from the local hardware, inherently self-healing.
4220
4180
  */
4221
4181
  async resolveAutoEngine() {
4222
4182
  let hardware = null;
4223
4183
  try {
4224
4184
  hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
4225
4185
  } catch {}
4226
- const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
4227
- this.currentEngine = {
4228
- runtime: "python",
4229
- backend: pick.runtimeId,
4230
- format: modelFormatFor(pick.runtimeId),
4231
- device: pick.device
4232
- };
4233
- this.log.info("Resolved auto engine (local kernel)", { meta: {
4234
- backend: pick.runtimeId,
4235
- device: pick.device,
4186
+ const devices = enumerateInferenceDevices(envToHardwareInfo(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null)));
4187
+ const top = devices.find((d) => d.available) ?? devices[0];
4188
+ this.currentEngine = top ? resolveDeviceEngine(top.key) : ONNX_FLOOR;
4189
+ this.log.info("Derived node-default engine (local kernel)", { meta: {
4190
+ deviceKey: top?.key ?? "onnx:cpu(floor)",
4191
+ backend: this.currentEngine.backend,
4192
+ device: this.currentEngine.device ?? null,
4236
4193
  hadHardware: hardware !== null
4237
4194
  } });
4238
4195
  }
@@ -4311,33 +4268,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4311
4268
  this.provisioner.select(runtimeId, device);
4312
4269
  }
4313
4270
  /**
4314
- * Re-trigger provisioning after the operator changes the engine
4315
- * cascade (`engineBackend`/`engineDevice`). Reloads the persisted
4316
- * selection into `this.currentEngine`, then `select()`s it — the
4317
- * machine cancels any superseded provisioning and drives the new
4318
- * runtime through installing → verifying → ready.
4319
- */
4320
- async onEngineSelectionChanged() {
4321
- const prev = this.currentEngine;
4322
- const stored = await this.loadEngine();
4323
- if (stored) this.currentEngine = stored;
4324
- else await this.resolveAutoEngine();
4325
- const engineChanged = prev.runtime !== this.currentEngine.runtime || prev.backend !== this.currentEngine.backend || prev.format !== this.currentEngine.format || (prev.device ?? "") !== (this.currentEngine.device ?? "");
4326
- if (engineChanged && this.engineFactory) {
4327
- this.log.info("engine selection changed — rebuilding pool in place", { meta: {
4328
- from: `${prev.backend}/${prev.device ?? "default"}`,
4329
- to: `${this.currentEngine.backend}/${this.currentEngine.device ?? "default"}`
4330
- } });
4331
- try {
4332
- await this.engineFactory.dispose();
4333
- } catch {}
4334
- this.engineFactory = null;
4335
- this.executor = null;
4336
- }
4337
- this.startProvisioningForCurrentEngine();
4338
- if (engineChanged) await this.recomputeConfigIssues();
4339
- }
4340
- /**
4341
4271
  * Gate A (config-correctness gate at engine change) — eagerly re-resolve
4342
4272
  * the node-default step tree for `this.currentEngine.format` and compute
4343
4273
  * human-readable config issues from the result:
@@ -5781,6 +5711,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5781
5711
  this.engineFactory = null;
5782
5712
  this.executor = null;
5783
5713
  }
5714
+ this.deviceReaper.disposeAll();
5784
5715
  const deviceFactories = Array.from(this.factoriesByDevice.values());
5785
5716
  this.factoriesByDevice.clear();
5786
5717
  await Promise.all(deviceFactories.map((f) => f.dispose().catch(() => void 0)));
@@ -5909,6 +5840,14 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5909
5840
  * `this.engineFactory`; when the requested device equals the node's current
5910
5841
  * engine the default pool is reused (no duplicate pool for the same device).
5911
5842
  */
5843
+ /**
5844
+ * In-flight device-pool creations, keyed by deviceKey. Single-flights
5845
+ * `resolveDeviceFactory` so two concurrent calls for the SAME device never
5846
+ * both spawn a pool — critical for single-context accelerators like the Coral
5847
+ * Edge TPU, where two pools racing to `load_delegate` on the one USB unit both
5848
+ * fail (the loser can't claim the already-held device).
5849
+ */
5850
+ deviceFactoryInflight = /* @__PURE__ */ new Map();
5912
5851
  async resolveDeviceFactory(deviceKey) {
5913
5852
  const engine = resolveDeviceEngine(deviceKey);
5914
5853
  if (enginesEqual(engine, this.currentEngine)) {
@@ -5916,86 +5855,56 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5916
5855
  return this.engineFactory;
5917
5856
  }
5918
5857
  const existing = this.factoriesByDevice.get(deviceKey);
5919
- if (existing) return existing;
5920
- await this.ensureBackendDeps(engine);
5921
- const factory = new EngineFactory({
5922
- engine,
5923
- modelsDir: this.modelsDir,
5924
- logger: this.log.child(`engine:${deviceKey}`),
5925
- pythonPath: this.executorOptions.pythonPath ?? "",
5926
- concurrency: this.executorOptions.concurrency,
5927
- tuning: this.executorOptions.tuning,
5928
- numWorkers: this.executorOptions.numWorkers
5929
- });
5930
- try {
5931
- await factory.initialize([]);
5932
- } catch (err) {
5933
- await factory.dispose().catch(() => void 0);
5934
- throw err;
5858
+ if (existing) {
5859
+ this.deviceReaper.touch(deviceKey);
5860
+ return existing;
5935
5861
  }
5936
- this.factoriesByDevice.set(deviceKey, factory);
5937
- return factory;
5938
- }
5939
- /**
5940
- * Phase 2b — resolve the engine from the addon's new schema-backed
5941
- * fields (`engineRuntime`, `engineBackend`, `engineDevice`). The legacy
5942
- * `pipelineEngine` JSON-blob fallback was removed with the executor seed
5943
- * (any store predating the per-node cascade simply re-runs auto-pick).
5944
- *
5945
- * The `'auto'` sentinel is treated as NO override (returns null) so the
5946
- * caller re-resolves LIVE via `resolveAutoEngine()` — `'auto'` is the
5947
- * self-healing default, never a persisted concrete backend. An explicit
5948
- * concrete backend (onnx/openvino/coreml) is honored as an operator override.
5949
- * Returns null when neither source has a concrete selection; the caller keeps
5950
- * the onnx floor set at construction until `resolveAutoEngine()` runs.
5951
- */
5952
- async loadEngine() {
5953
- const store = await this.readStore();
5954
- const storedRuntime = store["engineRuntime"];
5955
- const node = this.localProbeNodeId();
5956
- const storedBackend = readNodeEngineValue(store, "engineBackend", node);
5957
- if (storedBackend === "auto") return null;
5958
- if (typeof storedBackend === "string" && storedBackend.length > 0 && storedBackend !== "auto") {
5959
- const backend = storedBackend;
5960
- const storedDeviceRaw = readNodeEngineValue(store, "engineDevice", node);
5961
- const storedDevice = typeof storedDeviceRaw === "string" ? storedDeviceRaw : "";
5962
- const floor = onnxFloorPick();
5963
- const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5964
- if (!backendPossibleOnPlatform(migratedBackend)) {
5965
- const platformDefault = platformDefaultPick();
5966
- this.log.warn("Stored engine backend impossible on this platform — using platform default", { meta: {
5967
- stored: migratedBackend,
5968
- fallback: platformDefault.backend
5969
- } });
5970
- this.writeStore({
5971
- [nodeEngineKey("engineBackend", node)]: platformDefault.backend,
5972
- ...platformDefault.device !== void 0 ? { [nodeEngineKey("engineDevice", node)]: platformDefault.device } : {}
5973
- }).catch((err) => {
5974
- this.log.warn("Failed to persist platform-default engine after impossible stored pick", { meta: { error: err instanceof Error ? err.message : String(err) } });
5975
- });
5976
- return platformDefault;
5862
+ const inflight = this.deviceFactoryInflight.get(deviceKey);
5863
+ if (inflight) return inflight;
5864
+ const creation = (async () => {
5865
+ await this.ensureBackendDeps(engine);
5866
+ const factory = new EngineFactory({
5867
+ engine,
5868
+ modelsDir: this.modelsDir,
5869
+ logger: this.log.child(`engine:${deviceKey}`),
5870
+ pythonPath: this.executorOptions.pythonPath ?? "",
5871
+ concurrency: this.executorOptions.concurrency,
5872
+ tuning: this.executorOptions.tuning,
5873
+ numWorkers: this.executorOptions.numWorkers
5874
+ });
5875
+ try {
5876
+ await factory.initialize([]);
5877
+ } catch (err) {
5878
+ await factory.dispose().catch(() => void 0);
5879
+ throw err;
5977
5880
  }
5978
- const device = storedDevice || floor.device;
5979
- return {
5980
- runtime: "python",
5981
- backend: migratedBackend,
5982
- format: backendToFormat(migratedBackend),
5983
- ...device ? { device } : {}
5984
- };
5881
+ this.factoriesByDevice.set(deviceKey, factory);
5882
+ this.deviceReaper.touch(deviceKey);
5883
+ return factory;
5884
+ })();
5885
+ this.deviceFactoryInflight.set(deviceKey, creation);
5886
+ try {
5887
+ return await creation;
5888
+ } finally {
5889
+ this.deviceFactoryInflight.delete(deviceKey);
5985
5890
  }
5986
- return null;
5987
5891
  }
5988
5892
  /**
5989
- * Re-run the platform probe for the inference engine and persist the
5990
- * result into the addon's own global-settings keys. Because those
5991
- * fields are declared `requiresRestart: true`, the
5992
- * `updateGlobalSettings` pathway auto-schedules an addon restart
5993
- * here we do the write directly via the settings store and trigger
5994
- * restart manually through the addons cap.
5995
- *
5996
- * Returns the chosen engine so the UI can confirm the pick without a
5997
- * second round-trip.
5893
+ * Dispose a single idle device pool (invoked by the {@link deviceReaper} when
5894
+ * `deviceKey` has had no dispatch for the idle-TTL). Removed from the map
5895
+ * FIRST so a concurrent `resolveDeviceFactory` recreates it fresh via the
5896
+ * single-flight guard rather than handing back the disposing instance.
5998
5897
  */
5898
+ async disposeDevicePool(deviceKey) {
5899
+ const factory = this.factoriesByDevice.get(deviceKey);
5900
+ if (!factory) return;
5901
+ this.factoriesByDevice.delete(deviceKey);
5902
+ await factory.dispose();
5903
+ this.log.info("Idle device pool evicted", { meta: {
5904
+ deviceKey,
5905
+ idleTtlMs: DetectionPipelineProvider.DEVICE_POOL_IDLE_TTL_MS
5906
+ } });
5907
+ }
5999
5908
  /**
6000
5909
  * Phase 2c — flat per-addonId map holding the video-pipeline step
6001
5910
  * config (`modelId` / `settings`). No `enabled` field: whether an
@@ -6181,35 +6090,6 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6181
6090
  } });
6182
6091
  return { success: true };
6183
6092
  }
6184
- async reprobeEngine() {
6185
- let hardware = null;
6186
- try {
6187
- hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6188
- } catch {}
6189
- const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
6190
- const pick = pickBestRuntime(env, null);
6191
- const best = {
6192
- runtime: "python",
6193
- backend: pick.runtimeId,
6194
- format: modelFormatFor(pick.runtimeId),
6195
- device: pick.device
6196
- };
6197
- const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
6198
- const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
6199
- const rpNode = this.localProbeNodeId();
6200
- if (pick.runtimeId !== "onnx" || hardware !== null) {
6201
- await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });
6202
- this.log.info("Re-probed engine (local kernel) — updated probedBestEngine hint", { meta: {
6203
- backend: best.backend,
6204
- device: best.device ?? null,
6205
- probedBestEngine: probedLabel
6206
- } });
6207
- } else this.log.info("Re-probe: onnx floor pending gpu probe — hint NOT persisted", { meta: {
6208
- backend: best.backend,
6209
- device: best.device ?? null
6210
- } });
6211
- return best;
6212
- }
6213
6093
  async getReferenceAudioFiles() {
6214
6094
  const dir = resolveReferenceAudioDir();
6215
6095
  if (!dir) return [];
@@ -6564,47 +6444,6 @@ function resolveAddonPythonDir() {
6564
6444
  for (const c of candidates) if (fs.existsSync(path$1.join(c, "inference_pool.py"))) return c;
6565
6445
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
6566
6446
  }
6567
- var BACKEND_TO_FORMAT = {
6568
- cpu: "onnx",
6569
- coreml: "coreml",
6570
- openvino: "openvino",
6571
- onnx: "onnx",
6572
- edgetpu: "tflite"
6573
- };
6574
- /**
6575
- * Every runtime the registry knows about, in offer order. Used only to build
6576
- * the STATIC base schema (returned by `globalSettingsSchema()` before a live
6577
- * ctx exists, e.g. addon-registry boot introspection). The live, hardware-
6578
- * gated subset is injected by `getGlobalSettings()` via `supportedRuntimes(env)`.
6579
- */
6580
- var ALL_RUNTIME_IDS = [
6581
- "onnx",
6582
- "openvino",
6583
- "coreml",
6584
- "edgetpu"
6585
- ];
6586
- /**
6587
- * Narrow an arbitrary stored backend string to a known `RuntimeId`. Legacy or
6588
- * unknown values (e.g. the removed `'cpu'` backend) fall back to the `onnx`
6589
- * floor so the registry lookup never throws.
6590
- */
6591
- function toRuntimeId(backend) {
6592
- return ALL_RUNTIME_IDS.find((id) => id === backend) ?? "onnx";
6593
- }
6594
- /** Static universe of backend options (every runtime). Labels from the registry. */
6595
- var STATIC_BACKEND_OPTIONS = ALL_RUNTIME_IDS.map((id) => ({
6596
- value: id,
6597
- label: runtimeLabel(id)
6598
- }));
6599
- /**
6600
- * Static device options for the schema's default backend. The live device
6601
- * list per the selected backend + probed hardware is injected by
6602
- * `getGlobalSettings()` via `runtimeDevices(backend, hardware)`.
6603
- */
6604
- var STATIC_DEFAULT_DEVICE_OPTIONS = runtimeDevices(ALL_RUNTIME_IDS[0], null).map((d) => ({
6605
- value: d.value,
6606
- label: d.label
6607
- }));
6608
6447
  var DEFAULT_CONFIG = {
6609
6448
  concurrency: 0,
6610
6449
  batchMode: "",
@@ -6613,17 +6452,8 @@ var DEFAULT_CONFIG = {
6613
6452
  numStreams: 0,
6614
6453
  intraOpThreads: 0,
6615
6454
  numWorkers: 0,
6616
- engineRuntime: "python",
6617
- engineBackend: "auto",
6618
- engineDevice: "cpu",
6619
- probedBestEngine: "",
6620
- activeEngine: "",
6621
6455
  activeModelSubstitutions: ""
6622
6456
  };
6623
- /** Derive the model-format from a backend value. Called by the provider. */
6624
- function backendToFormat(backend) {
6625
- return BACKEND_TO_FORMAT[backend] ?? "onnx";
6626
- }
6627
6457
  /**
6628
6458
  * Cadence for the per-node engine-inventory snapshot. Engines change
6629
6459
  * slowly (manual spin/kill, idle eviction, attach/detach). 5 s strikes
@@ -6716,63 +6546,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
6716
6546
  tab: "engine",
6717
6547
  order: 0,
6718
6548
  description: "Vision-detection execution provider (object detection, face detection, plate OCR). Inference always runs via the embedded Python runtime. Provider and hardware options are gated by what the platform probe reports as available on this host. Changes restart the addon.",
6719
- fields: [
6720
- this.field({
6721
- type: "text",
6722
- key: "probedBestEngine",
6723
- label: "Probed best",
6724
- description: "Auto-detected best engine for this host (format: provider/device, e.g. \"openvino/npu\"). Click the refresh icon to re-run the probe — the detected provider and hardware device are written back into the two fields below, overwriting any manual override.",
6725
- readonlyField: true,
6726
- default: "",
6727
- perNode: true,
6728
- actions: [{
6729
- action: "reprobe-engine",
6730
- icon: "refresh-cw",
6731
- tooltip: "Re-probe engine"
6732
- }]
6733
- }),
6734
- this.field({
6735
- type: "text",
6736
- key: "activeEngine",
6737
- label: "Active engine",
6738
- description: "The runtime/device the inference pool ACTUALLY loaded on this node, with its provisioning state (format: provider/device (state)). If this differs from the selected provider below — e.g. \"onnx/cpu (ready)\" while OpenVINO is selected — the chosen accelerator could not load on this host and the engine fell back to the ONNX-CPU baseline.",
6739
- readonlyField: true,
6740
- default: ""
6741
- }),
6742
- this.field({
6743
- type: "text",
6744
- key: "activeModelSubstitutions",
6745
- label: "Model substitutions",
6746
- 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.",
6747
- readonlyField: true,
6748
- default: "",
6749
- showWhen: {
6750
- field: "activeModelSubstitutions",
6751
- notEquals: ""
6752
- }
6753
- }),
6754
- this.field({
6755
- type: "select",
6756
- key: "engineBackend",
6757
- label: "Execution provider",
6758
- options: [{
6759
- value: "auto",
6760
- label: "Auto (best for this node)"
6761
- }, ...STATIC_BACKEND_OPTIONS],
6762
- default: DEFAULT_CONFIG.engineBackend,
6763
- immediate: true,
6764
- perNode: true
6765
- }),
6766
- this.field({
6767
- type: "select",
6768
- key: "engineDevice",
6769
- label: "Hardware device",
6770
- options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
6771
- default: DEFAULT_CONFIG.engineDevice,
6772
- immediate: true,
6773
- perNode: true
6774
- })
6775
- ]
6549
+ fields: [this.field({
6550
+ type: "text",
6551
+ key: "activeModelSubstitutions",
6552
+ label: "Model substitutions",
6553
+ 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.",
6554
+ readonlyField: true,
6555
+ default: "",
6556
+ showWhen: {
6557
+ field: "activeModelSubstitutions",
6558
+ notEquals: ""
6559
+ }
6560
+ })]
6776
6561
  }, {
6777
6562
  id: "inference-executor",
6778
6563
  title: "Inference executor",
@@ -6801,15 +6586,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
6801
6586
  label: "Window (cross-camera accumulator)"
6802
6587
  }
6803
6588
  ],
6804
- default: "",
6805
- showWhen: {
6806
- field: "engineBackend",
6807
- in: [
6808
- "coreml",
6809
- "onnx",
6810
- "cpu"
6811
- ]
6812
- }
6589
+ default: ""
6813
6590
  }),
6814
6591
  this.field({
6815
6592
  type: "slider",
@@ -6875,59 +6652,31 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
6875
6652
  }] });
6876
6653
  }
6877
6654
  /**
6878
- * Override to inject the hardware-driven backend + device options.
6655
+ * Override to inject the read-only model-substitution status + the
6656
+ * per-backend tuning-slider defaults.
6879
6657
  *
6880
- * The OFFERED backend list comes from `supportedRuntimes(env)` platform,
6881
- * arch and probed hardware ONLY, never from which Python packages are
6882
- * installed. A host's hardware is what it can physically run; install state
6883
- * is provisioned on demand (Phase 2). Device options come from
6884
- * `runtimeDevices(backend, hardware)` (probe-driven; default-only when the
6885
- * probe is unreachable). Stored backend / device snap back to the registry
6886
- * floor / default when they fall outside the offered set.
6658
+ * Engine SELECTION is no longer operator-facing (the elected single-engine
6659
+ * cascade was removed): each node runs a device-array of inference pools
6660
+ * balanced per-session, and the node-default pool is derived live from the
6661
+ * node's own hardware. So this override no longer offers a backend/device
6662
+ * dropdown or a probed/active-engine readout — it only:
6663
+ * 1. resolves the node's derived-default backend (via the probe) to suggest
6664
+ * the matrix-tuned slider defaults that won the bench for that backend, and
6665
+ * 2. surfaces the live `activeModelSubstitutions` readout.
6887
6666
  */
6888
6667
  async getGlobalSettings(overlay, _cap, nodeId) {
6889
6668
  const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
6890
6669
  const rawStored = await this.resolveUiSettingsStore();
6891
6670
  if (rawStored === null) return null;
6892
- const stored = projectNodeEngine(rawStored, targetNode);
6893
- const storedBackendRaw = stored["engineBackend"];
6894
- if (typeof storedBackendRaw !== "string" || storedBackendRaw === "") return null;
6895
6671
  const merged = overlay ? {
6896
- ...stored,
6672
+ ...rawStored,
6897
6673
  ...overlay
6898
- } : stored;
6899
- const env = await this.probeHardwareEnv(targetNode);
6900
- const hardware = env.hardware;
6901
- const offered = supportedRuntimes(env);
6902
- const runtimeBackends = [{
6903
- value: "auto",
6904
- label: "Auto (best for this node)"
6905
- }, ...offered.map((id) => ({
6906
- value: id,
6907
- label: runtimeLabel(id)
6908
- }))];
6909
- const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6910
- const isAuto = storedBackend === "auto";
6911
- const backend = (() => {
6912
- if (isAuto) return pickBestRuntime(env, null).runtimeId;
6913
- const rid = toRuntimeId(storedBackend);
6914
- if (rid === "onnx") return "onnx";
6915
- if (offered.includes(rid)) return rid;
6916
- return rid === "openvino" && env.platform !== "darwin" && env.arch === "x64" || rid === "coreml" && env.platform === "darwin" ? rid : offered[0] ?? "onnx";
6917
- })();
6918
- const deviceOptions = runtimeDevices(backend, hardware);
6919
- const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6920
- const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6921
- const prov = targetNode === this.localNodeId() ? this.provider?.getEngineProvisioning() : void 0;
6922
- const activeEngine = prov && prov.runtimeId ? `${prov.runtimeId}/${prov.device ?? "default"} (${prov.state})` : "";
6674
+ } : rawStored;
6675
+ const backend = pickBestRuntime(await this.probeHardwareEnv(targetNode), null).runtimeId;
6923
6676
  const substitutions = this.provider?.getActiveModelSubstitutions() ?? [];
6924
6677
  const activeModelSubstitutions = substitutions.length === 0 ? "" : substitutions.map((s) => `${s.addonId}: ${s.chosen}→${s.running}`).join(", ");
6925
- const displayBackend = isAuto ? "auto" : backend;
6926
6678
  const raw = {
6927
6679
  ...merged,
6928
- engineBackend: displayBackend,
6929
- engineDevice: device,
6930
- activeEngine,
6931
6680
  activeModelSubstitutions
6932
6681
  };
6933
6682
  const schema = this.globalSettingsSchema();
@@ -6939,18 +6688,6 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
6939
6688
  ...section,
6940
6689
  fields: section.fields.map((field) => {
6941
6690
  if (field.type === "select") {
6942
- if (field.key === "engineBackend") return {
6943
- ...field,
6944
- options: [...runtimeBackends]
6945
- };
6946
- if (field.key === "engineDevice") return {
6947
- ...field,
6948
- options: deviceOptions.map((d) => ({
6949
- value: d.value,
6950
- label: d.label
6951
- })),
6952
- default: defaultDeviceFor(backend)
6953
- };
6954
6691
  if (field.key === "batchMode" && tuning["batchMode"] !== void 0) return {
6955
6692
  ...field,
6956
6693
  default: tuning["batchMode"]
@@ -7083,7 +6820,7 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
7083
6820
  * a reason to disagree.
7084
6821
  */
7085
6822
  resolveBackendTuning() {
7086
- const t = tuningFor(this.config.engineBackend === "auto" ? this.provider?.resolvedEngineBackend() ?? "onnx" : toRuntimeId(this.config.engineBackend));
6823
+ const t = tuningFor(this.provider?.resolvedEngineBackend() ?? "onnx");
7087
6824
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
7088
6825
  const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
7089
6826
  return {
@@ -7146,9 +6883,6 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
7146
6883
  await this.provider.init();
7147
6884
  await this.provider.setApi(this.ctx);
7148
6885
  }
7149
- if (!this.config.probedBestEngine) await this.provider.reprobeEngine().catch((err) => {
7150
- this.ctx.logger.warn("auto-reprobe engine failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
7151
- });
7152
6886
  await this.provider.warmPool();
7153
6887
  this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
7154
6888
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -7262,21 +6996,18 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
7262
6996
  }
7263
6997
  /**
7264
6998
  * BaseAddon calls `onConfigChanged` after every settings write. This is the
7265
- * SOLE apply path for engine + tuning changes — none of those fields declare
6999
+ * SOLE apply path for tuning changes — none of those fields declare
7266
7000
  * `requiresRestart` anymore (an addon restart re-probes hardware + reloads
7267
- * every model on every set, which is exactly what we want to avoid). Instead:
7268
- * - Pool-bound tuning (`concurrency`/`batchMode`/`windowMs`/…) in-place
7269
- * pool respawn when the snapshot flips (`poolConfigChanged` below).
7270
- * - Engine cascade (`engineBackend`/`engineDevice`) the provider's
7271
- * `onEngineSelectionChanged` disposes the device-bound factory so the next
7272
- * runPipeline rebuilds the pool on the new selection, in place.
7273
- * Both apply optimistically — the inference gate stays closed for the short
7274
- * re-spin, frames are dropped (never crashed), no addon bounce.
7001
+ * every model on every set, which is exactly what we want to avoid): pool-bound
7002
+ * tuning (`concurrency`/`batchMode`/`windowMs`/…) drives an in-place pool
7003
+ * respawn when the snapshot flips (`poolConfigChanged` below). The engine
7004
+ * SELECTION is no longer operator-facing — the node-default backend is derived
7005
+ * once at boot (`setApi` `resolveAutoEngine`) from static hardware, so there
7006
+ * is no per-write engine re-select. The respawn applies optimistically — the
7007
+ * inference gate stays closed for the short re-spin, frames are dropped (never
7008
+ * crashed), no addon bounce.
7275
7009
  */
7276
7010
  async onConfigChanged() {
7277
- if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
7278
- this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
7279
- });
7280
7011
  if (!this.poolConfigChanged()) return;
7281
7012
  if (!this.provider) {
7282
7013
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -7301,4 +7032,4 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
7301
7032
  }
7302
7033
  };
7303
7034
  //#endregion
7304
- export { ALL_STEPS, DetectionPipelineProvider, backendToFormat, DetectionPipelineAddon as default, getDefaultModelForFormat, getStepDefinition, shouldInstallOpenvino };
7035
+ export { ALL_STEPS, DetectionPipelineProvider, DetectionPipelineAddon as default, getDefaultModelForFormat, getStepDefinition, shouldInstallOpenvino };