@camstack/addon-pipeline 1.1.67 → 1.1.69

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 (26) 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 +137 -27
  4. package/dist/detection-pipeline/index.mjs +137 -27
  5. package/dist/{dist-DkGHtAaK.mjs → dist-BT4Ug2Bf.mjs} +22 -2
  6. package/dist/{dist-BxxVKcnw.js → dist-DUcDVlg7.js} +22 -2
  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 +7 -3
  10. package/dist/pipeline-runner/index.mjs +7 -3
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{step-definitions-CAs0RD2N.js → step-definitions-4tt-nRM4.js} +114 -187
  14. package/dist/{step-definitions-Delhq28w.mjs → step-definitions-DO0hIDMx.mjs} +114 -187
  15. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CfFsU700.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CP4DpABx.mjs} +1 -1
  16. package/dist/stream-broker/{hostInit-D-S5fCDx.mjs → hostInit-Do2ymS0O.mjs} +1 -1
  17. package/dist/stream-broker/index.js +1 -1
  18. package/dist/stream-broker/index.mjs +1 -1
  19. package/dist/stream-broker/remoteEntry.js +1 -1
  20. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CRvzj7Of.js → MaskShapeCanvas-DI4BY7W2-DlI3udxg.js} +1 -1
  21. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-CBgvPUB-.js → MotionZonesSettings-NcxxQN8r-Du8J0kED.js} +1 -1
  22. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DH07fe1k.js → PrivacyMaskSettings-APgPLF7p-DSST47WI.js} +1 -1
  23. package/embed-dist/assets/{index-BkoPcGtQ.js → index-B2ux2xJ7.js} +4 -4
  24. package/embed-dist/index.html +1 -1
  25. package/package.json +1 -1
  26. package/python/inference_pool.py +13 -3
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-BxxVKcnw.js");
6
+ const require_dist = require("../dist-DUcDVlg7.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-DkGHtAaK.mjs";
2
+ import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-BT4Ug2Bf.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { i as downloadFile } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
5
5
  import * as fs from "node:fs";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-BxxVKcnw.js");
7
- const require_step_definitions = require("../step-definitions-CAs0RD2N.js");
6
+ const require_dist = require("../dist-DUcDVlg7.js");
7
+ const require_step_definitions = require("../step-definitions-4tt-nRM4.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
9
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
10
10
  let node_child_process = require("node:child_process");
@@ -318,16 +318,16 @@ function projectNodeEngine(store, nodeId) {
318
318
  /** The object-detection step id — the only slot this resolver applies to. */
319
319
  var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
320
320
  /**
321
- * Balanced default object-detection model per accelerator class (measured on
322
- * COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
323
- * own `defaultModelId` (`yolo26n`), the safe nano that does not collapse on
324
- * small objects. The three accelerated ids are reduced-input YOLO26 variants
325
- * shipped in the catalog (onnx + coreml + openvino builds each).
321
+ * Balanced default object-detection model per accelerator class. `'cpu'` maps to
322
+ * `null` → the caller substitutes the step's own `defaultModelId` (`yolo26n`).
323
+ * INTEL NPU + iGPU default to OUR YOLOv9 (int8 @320): yolo26 does NOT compile on
324
+ * the Intel NPU (arch incompat 0 frames), while yolov9 runs on it (~298 fps
325
+ * measured). Apple ANE stays on yolo26 until the CoreML yolov9 build lands.
326
326
  */
327
327
  var MODEL_BY_CLASS = {
328
- "apple-ane": "yolo26l-256",
329
- "intel-npu": "yolo26m-320",
330
- "intel-gpu": "yolo26m-256",
328
+ "apple-ane": "yolov9m-320",
329
+ "intel-npu": "yolov9m-320-int8",
330
+ "intel-gpu": "yolov9m-320-int8",
331
331
  cpu: null
332
332
  };
333
333
  /**
@@ -567,6 +567,7 @@ var PoolWorker = class {
567
567
  concurrency: this.opts.concurrency,
568
568
  models: initialModels.map((m) => serializeModelConfig(m))
569
569
  };
570
+ if (this.opts.device) config["device"] = this.opts.device;
570
571
  if (this.opts.tuning) {
571
572
  config["batch_mode"] = this.opts.tuning.batchMode;
572
573
  config["window_ms"] = this.opts.tuning.windowMs;
@@ -814,7 +815,9 @@ var SharedInferencePool = class {
814
815
  this.concurrency = Math.max(1, options.concurrency ?? 1);
815
816
  this.tuning = options.tuning ?? null;
816
817
  this.numWorkers = Math.max(1, options.numWorkers ?? 1);
818
+ this.device = options.device;
817
819
  }
820
+ device;
818
821
  /** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
819
822
  getPid() {
820
823
  return this.workers[0]?.getPid() ?? null;
@@ -836,7 +839,8 @@ var SharedInferencePool = class {
836
839
  concurrency: this.concurrency,
837
840
  tuning: this.tuning,
838
841
  logger: this.log,
839
- workerLabel: `w${i}`
842
+ workerLabel: `w${i}`,
843
+ ...this.device ? { device: this.device } : {}
840
844
  }));
841
845
  const t0 = performance.now();
842
846
  const results = await Promise.all(this.workers.map((w) => w.initialize(initialModels)));
@@ -1495,7 +1499,8 @@ var EngineFactory = class {
1495
1499
  this.pool = new SharedInferencePool(pythonPath, poolRuntime, this.log.child("pool"), {
1496
1500
  concurrency,
1497
1501
  tuning: resolvedTuning,
1498
- numWorkers
1502
+ numWorkers,
1503
+ ...this.opts.engine.device ? { device: this.opts.engine.device } : {}
1499
1504
  });
1500
1505
  this.poolManager = new PipelineModelManager(this.pool, (stepId, modelId) => this.buildPoolModelConfig(stepId, modelId, poolRuntime), this.log.child("model-mgr"));
1501
1506
  await this.pool.initialize([]);
@@ -1543,6 +1548,39 @@ var EngineFactory = class {
1543
1548
  }
1544
1549
  };
1545
1550
  //#endregion
1551
+ //#region src/detection-pipeline/engine/resolve-device-engine.ts
1552
+ /**
1553
+ * Backend → model format for a device pool. Mirrors the Phase-1 device
1554
+ * enumeration (`enumerateInferenceDevices`) and the pool-runtime mapping.
1555
+ */
1556
+ var BACKEND_FORMAT = {
1557
+ openvino: "openvino",
1558
+ edgetpu: "tflite",
1559
+ onnx: "onnx",
1560
+ cpu: "onnx",
1561
+ cuda: "onnx",
1562
+ coreml: "coreml"
1563
+ };
1564
+ /**
1565
+ * Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
1566
+ * {@link PipelineEngineChoice} used to build that device's inference pool.
1567
+ * Throws on an unknown backend token so a bad `deviceKey` fails loud rather
1568
+ * than silently building a wrong pool.
1569
+ */
1570
+ function resolveDeviceEngine(deviceKey) {
1571
+ const colon = deviceKey.indexOf(":");
1572
+ const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
1573
+ const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
1574
+ const format = BACKEND_FORMAT[backend];
1575
+ if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
1576
+ return {
1577
+ runtime: "python",
1578
+ backend,
1579
+ format,
1580
+ device
1581
+ };
1582
+ }
1583
+ //#endregion
1546
1584
  //#region src/detection-pipeline/engine-provisioner.ts
1547
1585
  /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
1548
1586
  var BACKOFF_SCHEDULE_MS = [
@@ -3938,6 +3976,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3938
3976
  detectionConfigSchemaSource;
3939
3977
  executorOptions;
3940
3978
  engineFactory = null;
3979
+ /**
3980
+ * Per-device engine factories (Phase 2 multi-device), keyed by deviceKey
3981
+ * (`<backend>:<device>`, e.g. `openvino:npu`, `edgetpu:usb`). Each is its own
3982
+ * inference pool, warm CONCURRENTLY — so the node runs detection on NPU + iGPU
3983
+ * + Coral in parallel. The default (node current-engine) pool stays
3984
+ * `this.engineFactory`; device routing is a LOCAL per-dispatch choice that
3985
+ * never mutates shared state, so concurrent multi-device sessions don't race.
3986
+ */
3987
+ factoriesByDevice = /* @__PURE__ */ new Map();
3941
3988
  executor = null;
3942
3989
  currentSteps = null;
3943
3990
  currentEngine;
@@ -5056,8 +5103,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5056
5103
  emit(`Image: ${imageWidth}×${imageHeight}`);
5057
5104
  }
5058
5105
  decodeMs = performance.now() - decodeT0;
5059
- const resolveFormat = input.engine?.format ?? this.currentEngine.format;
5060
- const resolveEngine = input.engine ? {
5106
+ const deviceEngine = input.deviceKey ? resolveDeviceEngine(input.deviceKey) : void 0;
5107
+ const resolveFormat = deviceEngine?.format ?? input.engine?.format ?? this.currentEngine.format;
5108
+ const resolveEngine = deviceEngine ? {
5109
+ backend: deviceEngine.backend,
5110
+ device: deviceEngine.device ?? null
5111
+ } : input.engine ? {
5061
5112
  backend: input.engine.backend,
5062
5113
  device: input.engine.device ?? null
5063
5114
  } : {
@@ -5158,7 +5209,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5158
5209
  logger: this.log
5159
5210
  });
5160
5211
  this.executor = executor;
5161
- const needed = enabledSteps.filter((s) => !this.engineFactory?.isLoadedWithModel(s.addonId, s.modelId));
5212
+ const dispatchEngine = input.deviceKey ? resolveDeviceEngine(input.deviceKey) : runEngine;
5213
+ const dispatchFactory = input.deviceKey ? await this.resolveDeviceFactory(input.deviceKey) : this.engineFactory;
5214
+ const needed = enabledSteps.filter((s) => !dispatchFactory.isLoadedWithModel(s.addonId, s.modelId));
5162
5215
  if (needed.length > 0) {
5163
5216
  this.log.info("Benchmark: models to load", { meta: {
5164
5217
  count: needed.length,
@@ -5169,11 +5222,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5169
5222
  addonId: s.addonId,
5170
5223
  modelId: s.modelId
5171
5224
  });
5172
- await this.ensureModelsForSteps(benchmarkSteps);
5225
+ await this.ensureModelsForSteps(benchmarkSteps, dispatchFactory, dispatchEngine.format);
5173
5226
  emit(`All models loaded`);
5174
5227
  }
5175
5228
  emit("Running inference...");
5176
- const tree = buildExecutableTree(benchmarkSteps, (stepId) => this.engineFactory.getEngine(stepId));
5229
+ const tree = buildExecutableTree(benchmarkSteps, (stepId) => dispatchFactory.getEngine(stepId));
5177
5230
  setupMs = performance.now() - wallT0 - decodeMs;
5178
5231
  const isRuntime = Boolean(runtimeFrame);
5179
5232
  const effectiveDeviceId = input.deviceId ?? 0;
@@ -5417,19 +5470,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5417
5470
  * before any of them committed via `stepToLoaded.set`.
5418
5471
  */
5419
5472
  modelLoadInFlight = null;
5420
- /** Ensure all models needed by steps are downloaded and loaded in the engine pool. */
5421
- async ensureModelsForSteps(steps) {
5473
+ /** Ensure all models needed by steps are downloaded and loaded in the engine pool.
5474
+ * `factory`/`format` default to the node's engine; a per-device dispatch passes
5475
+ * that device's factory + format (Phase 2 multi-device). */
5476
+ async ensureModelsForSteps(steps, factory = this.engineFactory, format = this.currentEngine?.format ?? "onnx") {
5422
5477
  while (this.modelLoadInFlight) await this.modelLoadInFlight;
5423
5478
  const allEnabled = flattenSteps(steps);
5424
5479
  const needed = [];
5425
5480
  for (const step of allEnabled) {
5426
5481
  if (!step.enabled) continue;
5427
- if (this.engineFactory?.isLoadedWithModel(step.addonId, step.modelId)) continue;
5482
+ if (factory?.isLoadedWithModel(step.addonId, step.modelId)) continue;
5428
5483
  needed.push(step);
5429
5484
  }
5430
5485
  if (needed.length === 0) return;
5431
5486
  const work = (async () => {
5432
- const format = this.currentEngine?.format ?? "onnx";
5433
5487
  for (const step of needed) {
5434
5488
  const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5435
5489
  if (modelEntry && !require_model_download_service_Cp9f4dk6.isModelDownloaded(this.modelsDir, modelEntry, format)) {
@@ -5445,7 +5499,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5445
5499
  count: needed.length,
5446
5500
  models: needed.map((s) => `${s.addonId}/${s.modelId}`)
5447
5501
  } });
5448
- await this.engineFactory.loadAdditional(needed);
5502
+ await factory.loadAdditional(needed);
5449
5503
  })();
5450
5504
  this.modelLoadInFlight = work;
5451
5505
  try {
@@ -5550,8 +5604,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5550
5604
  }
5551
5605
  async runPipelineBatchImpl(input) {
5552
5606
  if (input.frames.length === 0) return { results: [] };
5553
- const resolveFormat = input.engine?.format ?? this.currentEngine.format;
5554
- const resolveEngine = input.engine ? {
5607
+ const deviceEngine = input.deviceKey ? resolveDeviceEngine(input.deviceKey) : void 0;
5608
+ const resolveFormat = deviceEngine?.format ?? input.engine?.format ?? this.currentEngine.format;
5609
+ const resolveEngine = deviceEngine ? {
5610
+ backend: deviceEngine.backend,
5611
+ device: deviceEngine.device ?? null
5612
+ } : input.engine ? {
5555
5613
  backend: input.engine.backend,
5556
5614
  device: input.engine.device ?? null
5557
5615
  } : {
@@ -5580,9 +5638,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5580
5638
  await this.ensureEngineFactory();
5581
5639
  const restoreEngine = await this.applyEngineOverride(input.engine);
5582
5640
  try {
5583
- const factory = this.engineFactory;
5641
+ const factory = input.deviceKey ? await this.resolveDeviceFactory(input.deviceKey) : this.engineFactory;
5584
5642
  if (!factory) throw new Error("runPipelineBatch: factory not initialised");
5585
- if (enabledSteps.filter((s) => !factory.isLoadedWithModel(s.addonId, s.modelId)).length > 0) await this.ensureModelsForSteps(benchmarkSteps);
5643
+ if (enabledSteps.filter((s) => !factory.isLoadedWithModel(s.addonId, s.modelId)).length > 0) await this.ensureModelsForSteps(benchmarkSteps, factory, resolveFormat);
5586
5644
  const canFastPath = singleRoot && allRaw && uniformDims && factory.supportsBatch();
5587
5645
  this.log.info("runPipelineBatch path decision", { meta: {
5588
5646
  phase: "batch",
@@ -5596,7 +5654,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5596
5654
  steps: input.steps,
5597
5655
  frame,
5598
5656
  deviceId: input.deviceId,
5599
- sessionId: input.sessionId
5657
+ sessionId: input.sessionId,
5658
+ deviceKey: input.deviceKey
5600
5659
  }))) };
5601
5660
  const items = input.frames.map((frame) => ({
5602
5661
  raw: Buffer.from(frame.data),
@@ -5730,6 +5789,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5730
5789
  this.engineFactory = null;
5731
5790
  this.executor = null;
5732
5791
  }
5792
+ const deviceFactories = Array.from(this.factoriesByDevice.values());
5793
+ this.factoriesByDevice.clear();
5794
+ await Promise.all(deviceFactories.map((f) => f.dispose().catch(() => void 0)));
5733
5795
  for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5734
5796
  }
5735
5797
  /**
@@ -5849,6 +5911,40 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5849
5911
  await this.engineFactory.initialize([]);
5850
5912
  }
5851
5913
  /**
5914
+ * Get (or lazily build) the inference-pool factory for an explicit device
5915
+ * (`<backend>:<device>`, e.g. `openvino:npu`, `edgetpu:usb`) — Phase 2
5916
+ * multi-device. Each device runs its OWN pool concurrently. Never mutates
5917
+ * `this.engineFactory`; when the requested device equals the node's current
5918
+ * engine the default pool is reused (no duplicate pool for the same device).
5919
+ */
5920
+ async resolveDeviceFactory(deviceKey) {
5921
+ const engine = resolveDeviceEngine(deviceKey);
5922
+ if (enginesEqual(engine, this.currentEngine)) {
5923
+ await this.ensureEngineFactory();
5924
+ return this.engineFactory;
5925
+ }
5926
+ const existing = this.factoriesByDevice.get(deviceKey);
5927
+ if (existing) return existing;
5928
+ await this.ensureBackendDeps(engine);
5929
+ const factory = new EngineFactory({
5930
+ engine,
5931
+ modelsDir: this.modelsDir,
5932
+ logger: this.log.child(`engine:${deviceKey}`),
5933
+ pythonPath: this.executorOptions.pythonPath ?? "",
5934
+ concurrency: this.executorOptions.concurrency,
5935
+ tuning: this.executorOptions.tuning,
5936
+ numWorkers: this.executorOptions.numWorkers
5937
+ });
5938
+ try {
5939
+ await factory.initialize([]);
5940
+ } catch (err) {
5941
+ await factory.dispose().catch(() => void 0);
5942
+ throw err;
5943
+ }
5944
+ this.factoriesByDevice.set(deviceKey, factory);
5945
+ return factory;
5946
+ }
5947
+ /**
5852
5948
  * Phase 2b — resolve the engine from the addon's new schema-backed
5853
5949
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). The legacy
5854
5950
  * `pipelineEngine` JSON-blob fallback was removed with the executor seed
@@ -6039,6 +6135,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6039
6135
  idleTtlMs: DetectionPipelineProvider.OVERRIDE_CACHE_TTL_MS
6040
6136
  });
6041
6137
  }
6138
+ for (const [deviceKey, factory] of this.factoriesByDevice) {
6139
+ const eng = resolveDeviceEngine(deviceKey);
6140
+ const loaded = factory.listLoaded();
6141
+ out.push({
6142
+ engineKey: `${eng.runtime}/${eng.backend}/${eng.device ?? "default"} [${deviceKey}]`,
6143
+ engine: eng,
6144
+ modelsLoaded: loaded.map((l) => `${l.stepId}/${l.modelId}`),
6145
+ inUseByCameras: [],
6146
+ kind: "device-pool",
6147
+ poolPid: factory.getPoolPid(),
6148
+ idleMs: null,
6149
+ idleTtlMs: null
6150
+ });
6151
+ }
6042
6152
  return out;
6043
6153
  }
6044
6154
  async spinEngine(input) {
@@ -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-DkGHtAaK.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-Delhq28w.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";
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";
@@ -310,16 +310,16 @@ function projectNodeEngine(store, nodeId) {
310
310
  /** The object-detection step id — the only slot this resolver applies to. */
311
311
  var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
312
312
  /**
313
- * Balanced default object-detection model per accelerator class (measured on
314
- * COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
315
- * own `defaultModelId` (`yolo26n`), the safe nano that does not collapse on
316
- * small objects. The three accelerated ids are reduced-input YOLO26 variants
317
- * shipped in the catalog (onnx + coreml + openvino builds each).
313
+ * Balanced default object-detection model per accelerator class. `'cpu'` maps to
314
+ * `null` → the caller substitutes the step's own `defaultModelId` (`yolo26n`).
315
+ * INTEL NPU + iGPU default to OUR YOLOv9 (int8 @320): yolo26 does NOT compile on
316
+ * the Intel NPU (arch incompat 0 frames), while yolov9 runs on it (~298 fps
317
+ * measured). Apple ANE stays on yolo26 until the CoreML yolov9 build lands.
318
318
  */
319
319
  var MODEL_BY_CLASS = {
320
- "apple-ane": "yolo26l-256",
321
- "intel-npu": "yolo26m-320",
322
- "intel-gpu": "yolo26m-256",
320
+ "apple-ane": "yolov9m-320",
321
+ "intel-npu": "yolov9m-320-int8",
322
+ "intel-gpu": "yolov9m-320-int8",
323
323
  cpu: null
324
324
  };
325
325
  /**
@@ -559,6 +559,7 @@ var PoolWorker = class {
559
559
  concurrency: this.opts.concurrency,
560
560
  models: initialModels.map((m) => serializeModelConfig(m))
561
561
  };
562
+ if (this.opts.device) config["device"] = this.opts.device;
562
563
  if (this.opts.tuning) {
563
564
  config["batch_mode"] = this.opts.tuning.batchMode;
564
565
  config["window_ms"] = this.opts.tuning.windowMs;
@@ -806,7 +807,9 @@ var SharedInferencePool = class {
806
807
  this.concurrency = Math.max(1, options.concurrency ?? 1);
807
808
  this.tuning = options.tuning ?? null;
808
809
  this.numWorkers = Math.max(1, options.numWorkers ?? 1);
810
+ this.device = options.device;
809
811
  }
812
+ device;
810
813
  /** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
811
814
  getPid() {
812
815
  return this.workers[0]?.getPid() ?? null;
@@ -828,7 +831,8 @@ var SharedInferencePool = class {
828
831
  concurrency: this.concurrency,
829
832
  tuning: this.tuning,
830
833
  logger: this.log,
831
- workerLabel: `w${i}`
834
+ workerLabel: `w${i}`,
835
+ ...this.device ? { device: this.device } : {}
832
836
  }));
833
837
  const t0 = performance.now();
834
838
  const results = await Promise.all(this.workers.map((w) => w.initialize(initialModels)));
@@ -1487,7 +1491,8 @@ var EngineFactory = class {
1487
1491
  this.pool = new SharedInferencePool(pythonPath, poolRuntime, this.log.child("pool"), {
1488
1492
  concurrency,
1489
1493
  tuning: resolvedTuning,
1490
- numWorkers
1494
+ numWorkers,
1495
+ ...this.opts.engine.device ? { device: this.opts.engine.device } : {}
1491
1496
  });
1492
1497
  this.poolManager = new PipelineModelManager(this.pool, (stepId, modelId) => this.buildPoolModelConfig(stepId, modelId, poolRuntime), this.log.child("model-mgr"));
1493
1498
  await this.pool.initialize([]);
@@ -1535,6 +1540,39 @@ var EngineFactory = class {
1535
1540
  }
1536
1541
  };
1537
1542
  //#endregion
1543
+ //#region src/detection-pipeline/engine/resolve-device-engine.ts
1544
+ /**
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
+ * Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
1558
+ * {@link PipelineEngineChoice} used to build that device's inference pool.
1559
+ * Throws on an unknown backend token so a bad `deviceKey` fails loud rather
1560
+ * than silently building a wrong pool.
1561
+ */
1562
+ function resolveDeviceEngine(deviceKey) {
1563
+ const colon = deviceKey.indexOf(":");
1564
+ const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
1565
+ const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
1566
+ const format = BACKEND_FORMAT[backend];
1567
+ if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
1568
+ return {
1569
+ runtime: "python",
1570
+ backend,
1571
+ format,
1572
+ device
1573
+ };
1574
+ }
1575
+ //#endregion
1538
1576
  //#region src/detection-pipeline/engine-provisioner.ts
1539
1577
  /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
1540
1578
  var BACKOFF_SCHEDULE_MS = [
@@ -3930,6 +3968,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3930
3968
  detectionConfigSchemaSource;
3931
3969
  executorOptions;
3932
3970
  engineFactory = null;
3971
+ /**
3972
+ * Per-device engine factories (Phase 2 multi-device), keyed by deviceKey
3973
+ * (`<backend>:<device>`, e.g. `openvino:npu`, `edgetpu:usb`). Each is its own
3974
+ * inference pool, warm CONCURRENTLY — so the node runs detection on NPU + iGPU
3975
+ * + Coral in parallel. The default (node current-engine) pool stays
3976
+ * `this.engineFactory`; device routing is a LOCAL per-dispatch choice that
3977
+ * never mutates shared state, so concurrent multi-device sessions don't race.
3978
+ */
3979
+ factoriesByDevice = /* @__PURE__ */ new Map();
3933
3980
  executor = null;
3934
3981
  currentSteps = null;
3935
3982
  currentEngine;
@@ -5048,8 +5095,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5048
5095
  emit(`Image: ${imageWidth}×${imageHeight}`);
5049
5096
  }
5050
5097
  decodeMs = performance.now() - decodeT0;
5051
- const resolveFormat = input.engine?.format ?? this.currentEngine.format;
5052
- const resolveEngine = input.engine ? {
5098
+ const deviceEngine = input.deviceKey ? resolveDeviceEngine(input.deviceKey) : void 0;
5099
+ const resolveFormat = deviceEngine?.format ?? input.engine?.format ?? this.currentEngine.format;
5100
+ const resolveEngine = deviceEngine ? {
5101
+ backend: deviceEngine.backend,
5102
+ device: deviceEngine.device ?? null
5103
+ } : input.engine ? {
5053
5104
  backend: input.engine.backend,
5054
5105
  device: input.engine.device ?? null
5055
5106
  } : {
@@ -5150,7 +5201,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5150
5201
  logger: this.log
5151
5202
  });
5152
5203
  this.executor = executor;
5153
- const needed = enabledSteps.filter((s) => !this.engineFactory?.isLoadedWithModel(s.addonId, s.modelId));
5204
+ const dispatchEngine = input.deviceKey ? resolveDeviceEngine(input.deviceKey) : runEngine;
5205
+ const dispatchFactory = input.deviceKey ? await this.resolveDeviceFactory(input.deviceKey) : this.engineFactory;
5206
+ const needed = enabledSteps.filter((s) => !dispatchFactory.isLoadedWithModel(s.addonId, s.modelId));
5154
5207
  if (needed.length > 0) {
5155
5208
  this.log.info("Benchmark: models to load", { meta: {
5156
5209
  count: needed.length,
@@ -5161,11 +5214,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5161
5214
  addonId: s.addonId,
5162
5215
  modelId: s.modelId
5163
5216
  });
5164
- await this.ensureModelsForSteps(benchmarkSteps);
5217
+ await this.ensureModelsForSteps(benchmarkSteps, dispatchFactory, dispatchEngine.format);
5165
5218
  emit(`All models loaded`);
5166
5219
  }
5167
5220
  emit("Running inference...");
5168
- const tree = buildExecutableTree(benchmarkSteps, (stepId) => this.engineFactory.getEngine(stepId));
5221
+ const tree = buildExecutableTree(benchmarkSteps, (stepId) => dispatchFactory.getEngine(stepId));
5169
5222
  setupMs = performance.now() - wallT0 - decodeMs;
5170
5223
  const isRuntime = Boolean(runtimeFrame);
5171
5224
  const effectiveDeviceId = input.deviceId ?? 0;
@@ -5409,19 +5462,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5409
5462
  * before any of them committed via `stepToLoaded.set`.
5410
5463
  */
5411
5464
  modelLoadInFlight = null;
5412
- /** Ensure all models needed by steps are downloaded and loaded in the engine pool. */
5413
- async ensureModelsForSteps(steps) {
5465
+ /** Ensure all models needed by steps are downloaded and loaded in the engine pool.
5466
+ * `factory`/`format` default to the node's engine; a per-device dispatch passes
5467
+ * that device's factory + format (Phase 2 multi-device). */
5468
+ async ensureModelsForSteps(steps, factory = this.engineFactory, format = this.currentEngine?.format ?? "onnx") {
5414
5469
  while (this.modelLoadInFlight) await this.modelLoadInFlight;
5415
5470
  const allEnabled = flattenSteps(steps);
5416
5471
  const needed = [];
5417
5472
  for (const step of allEnabled) {
5418
5473
  if (!step.enabled) continue;
5419
- if (this.engineFactory?.isLoadedWithModel(step.addonId, step.modelId)) continue;
5474
+ if (factory?.isLoadedWithModel(step.addonId, step.modelId)) continue;
5420
5475
  needed.push(step);
5421
5476
  }
5422
5477
  if (needed.length === 0) return;
5423
5478
  const work = (async () => {
5424
- const format = this.currentEngine?.format ?? "onnx";
5425
5479
  for (const step of needed) {
5426
5480
  const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5427
5481
  if (modelEntry && !isModelDownloaded(this.modelsDir, modelEntry, format)) {
@@ -5437,7 +5491,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5437
5491
  count: needed.length,
5438
5492
  models: needed.map((s) => `${s.addonId}/${s.modelId}`)
5439
5493
  } });
5440
- await this.engineFactory.loadAdditional(needed);
5494
+ await factory.loadAdditional(needed);
5441
5495
  })();
5442
5496
  this.modelLoadInFlight = work;
5443
5497
  try {
@@ -5542,8 +5596,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5542
5596
  }
5543
5597
  async runPipelineBatchImpl(input) {
5544
5598
  if (input.frames.length === 0) return { results: [] };
5545
- const resolveFormat = input.engine?.format ?? this.currentEngine.format;
5546
- const resolveEngine = input.engine ? {
5599
+ const deviceEngine = input.deviceKey ? resolveDeviceEngine(input.deviceKey) : void 0;
5600
+ const resolveFormat = deviceEngine?.format ?? input.engine?.format ?? this.currentEngine.format;
5601
+ const resolveEngine = deviceEngine ? {
5602
+ backend: deviceEngine.backend,
5603
+ device: deviceEngine.device ?? null
5604
+ } : input.engine ? {
5547
5605
  backend: input.engine.backend,
5548
5606
  device: input.engine.device ?? null
5549
5607
  } : {
@@ -5572,9 +5630,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5572
5630
  await this.ensureEngineFactory();
5573
5631
  const restoreEngine = await this.applyEngineOverride(input.engine);
5574
5632
  try {
5575
- const factory = this.engineFactory;
5633
+ const factory = input.deviceKey ? await this.resolveDeviceFactory(input.deviceKey) : this.engineFactory;
5576
5634
  if (!factory) throw new Error("runPipelineBatch: factory not initialised");
5577
- if (enabledSteps.filter((s) => !factory.isLoadedWithModel(s.addonId, s.modelId)).length > 0) await this.ensureModelsForSteps(benchmarkSteps);
5635
+ if (enabledSteps.filter((s) => !factory.isLoadedWithModel(s.addonId, s.modelId)).length > 0) await this.ensureModelsForSteps(benchmarkSteps, factory, resolveFormat);
5578
5636
  const canFastPath = singleRoot && allRaw && uniformDims && factory.supportsBatch();
5579
5637
  this.log.info("runPipelineBatch path decision", { meta: {
5580
5638
  phase: "batch",
@@ -5588,7 +5646,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5588
5646
  steps: input.steps,
5589
5647
  frame,
5590
5648
  deviceId: input.deviceId,
5591
- sessionId: input.sessionId
5649
+ sessionId: input.sessionId,
5650
+ deviceKey: input.deviceKey
5592
5651
  }))) };
5593
5652
  const items = input.frames.map((frame) => ({
5594
5653
  raw: Buffer.from(frame.data),
@@ -5722,6 +5781,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5722
5781
  this.engineFactory = null;
5723
5782
  this.executor = null;
5724
5783
  }
5784
+ const deviceFactories = Array.from(this.factoriesByDevice.values());
5785
+ this.factoriesByDevice.clear();
5786
+ await Promise.all(deviceFactories.map((f) => f.dispose().catch(() => void 0)));
5725
5787
  for (const id of Array.from(this.deviceProxies.keys())) this.releaseDeviceProxy(id);
5726
5788
  }
5727
5789
  /**
@@ -5841,6 +5903,40 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5841
5903
  await this.engineFactory.initialize([]);
5842
5904
  }
5843
5905
  /**
5906
+ * Get (or lazily build) the inference-pool factory for an explicit device
5907
+ * (`<backend>:<device>`, e.g. `openvino:npu`, `edgetpu:usb`) — Phase 2
5908
+ * multi-device. Each device runs its OWN pool concurrently. Never mutates
5909
+ * `this.engineFactory`; when the requested device equals the node's current
5910
+ * engine the default pool is reused (no duplicate pool for the same device).
5911
+ */
5912
+ async resolveDeviceFactory(deviceKey) {
5913
+ const engine = resolveDeviceEngine(deviceKey);
5914
+ if (enginesEqual(engine, this.currentEngine)) {
5915
+ await this.ensureEngineFactory();
5916
+ return this.engineFactory;
5917
+ }
5918
+ 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;
5935
+ }
5936
+ this.factoriesByDevice.set(deviceKey, factory);
5937
+ return factory;
5938
+ }
5939
+ /**
5844
5940
  * Phase 2b — resolve the engine from the addon's new schema-backed
5845
5941
  * fields (`engineRuntime`, `engineBackend`, `engineDevice`). The legacy
5846
5942
  * `pipelineEngine` JSON-blob fallback was removed with the executor seed
@@ -6031,6 +6127,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6031
6127
  idleTtlMs: DetectionPipelineProvider.OVERRIDE_CACHE_TTL_MS
6032
6128
  });
6033
6129
  }
6130
+ for (const [deviceKey, factory] of this.factoriesByDevice) {
6131
+ const eng = resolveDeviceEngine(deviceKey);
6132
+ const loaded = factory.listLoaded();
6133
+ out.push({
6134
+ engineKey: `${eng.runtime}/${eng.backend}/${eng.device ?? "default"} [${deviceKey}]`,
6135
+ engine: eng,
6136
+ modelsLoaded: loaded.map((l) => `${l.stepId}/${l.modelId}`),
6137
+ inUseByCameras: [],
6138
+ kind: "device-pool",
6139
+ poolPid: factory.getPoolPid(),
6140
+ idleMs: null,
6141
+ idleTtlMs: null
6142
+ });
6143
+ }
6034
6144
  return out;
6035
6145
  }
6036
6146
  async spinEngine(input) {