@camstack/addon-pipeline 1.0.1 → 1.0.3

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 (31) hide show
  1. package/dist/audio-analyzer/index.js +5 -5
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/audio-codec-nodeav/index.js +1 -1
  4. package/dist/audio-codec-nodeav/index.mjs +1 -1
  5. package/dist/decoder-nodeav/index.js +1 -1
  6. package/dist/decoder-nodeav/index.mjs +1 -1
  7. package/dist/detection-pipeline/index.js +276 -61
  8. package/dist/detection-pipeline/index.mjs +268 -55
  9. package/dist/{dist-7ewQjTle.js → dist-C1goFC50.js} +4 -4
  10. package/dist/{dist-C5jnNl0n.mjs → dist-XRXnZrVC.mjs} +4 -4
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +1 -1
  14. package/dist/pipeline-runner/index.mjs +1 -1
  15. package/dist/recorder/index.js +3 -3
  16. package/dist/recorder/index.mjs +2 -2
  17. package/dist/stream-broker/_stub.js +1 -1
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-qX99--rF.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dGO6_Xee.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B0Z2W5UM.mjs +26 -0
  20. package/dist/stream-broker/{hostInit-Bx41KdYV.mjs → hostInit-ClKL4kbX.mjs} +3 -3
  21. package/dist/stream-broker/index.js +3 -3
  22. package/dist/stream-broker/index.mjs +2 -2
  23. package/dist/stream-broker/remoteEntry.js +1 -1
  24. package/embed-dist/assets/MaskShapeCanvas-DI4BY7W2-DEYqSd2k.js +33 -0
  25. package/embed-dist/assets/MotionZonesSettings-C1EEbk2V-CRvYkSju.js +1 -0
  26. package/embed-dist/assets/PrivacyMaskSettings-APgPLF7p-CIq73-7y.js +1 -0
  27. package/embed-dist/assets/index-C5Az4io-.js +80 -0
  28. package/embed-dist/index.html +1 -1
  29. package/package.json +3 -3
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C9WX5HNw.mjs +0 -26
  31. package/embed-dist/assets/index-B8VlSD0-.js +0 -150
@@ -3,12 +3,12 @@ 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-7ewQjTle.js");
6
+ const require_dist = require("../dist-C1goFC50.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
10
10
  node_path = require_chunk.__toESM(node_path);
11
- let _camstack_core = require("@camstack/core");
11
+ let _camstack_system = require("@camstack/system");
12
12
  //#region src/audio-analyzer/audio-pipeline.ts
13
13
  /**
14
14
  * Create the appropriate audio pipeline.
@@ -27,7 +27,7 @@ async function createAudioPipeline(modelsDir, logger, options) {
27
27
  * Canonical model URLs on the camstack HuggingFace mirror. Mirrors the
28
28
  * convention every detection model follows (single point of truth =
29
29
  * `HF_BASE_URL` from `@camstack/types`); the auto-download path uses
30
- * `downloadFile` from `@camstack/core`, the SAME helper detection-
30
+ * `downloadFile` from `@camstack/system`, the SAME helper detection-
31
31
  * pipeline uses to materialise its YOLO/face/plate models.
32
32
  *
33
33
  * {domain}/{family}/{format}/{filename}
@@ -83,7 +83,7 @@ var YamnetPythonPipeline = class {
83
83
  url: YAMNET_MODEL_URL,
84
84
  dest: modelPath
85
85
  } });
86
- await (0, _camstack_core.downloadFile)(YAMNET_MODEL_URL, modelPath);
86
+ await (0, _camstack_system.downloadFile)(YAMNET_MODEL_URL, modelPath);
87
87
  this.log.info("YAMNet ONNX model downloaded", { meta: { sizeBytes: node_fs.statSync(modelPath).size } });
88
88
  }
89
89
  if (!node_fs.existsSync(labelsPath)) {
@@ -91,7 +91,7 @@ var YamnetPythonPipeline = class {
91
91
  url: YAMNET_LABELS_URL,
92
92
  dest: labelsPath
93
93
  } });
94
- await (0, _camstack_core.downloadFile)(YAMNET_LABELS_URL, labelsPath);
94
+ await (0, _camstack_system.downloadFile)(YAMNET_LABELS_URL, labelsPath);
95
95
  }
96
96
  const pythonDir = resolveAudioPythonDir();
97
97
  if (this.installPythonRequirements) await this.installPythonRequirements(node_path.join(pythonDir, "requirements-audio.txt"));
@@ -1,8 +1,8 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { L as mapAudioLabelToMacro, P as hydrateSchema, S as audioAnalyzerCapability, c as DEFAULT_AUDIO_ANALYZER_CONFIG, i as BaseAddon, j as errMsg, m as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, x as audioAnalysisCapability } from "../dist-C5jnNl0n.mjs";
2
+ import { L as mapAudioLabelToMacro, P as hydrateSchema, S as audioAnalyzerCapability, c as DEFAULT_AUDIO_ANALYZER_CONFIG, i as BaseAddon, j as errMsg, m as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, x as audioAnalysisCapability } from "../dist-XRXnZrVC.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
5
- import { downloadFile } from "@camstack/core";
5
+ import { downloadFile } from "@camstack/system";
6
6
  //#region src/audio-analyzer/audio-pipeline.ts
7
7
  /**
8
8
  * Create the appropriate audio pipeline.
@@ -21,7 +21,7 @@ async function createAudioPipeline(modelsDir, logger, options) {
21
21
  * Canonical model URLs on the camstack HuggingFace mirror. Mirrors the
22
22
  * convention every detection model follows (single point of truth =
23
23
  * `HF_BASE_URL` from `@camstack/types`); the auto-download path uses
24
- * `downloadFile` from `@camstack/core`, the SAME helper detection-
24
+ * `downloadFile` from `@camstack/system`, the SAME helper detection-
25
25
  * pipeline uses to materialise its YOLO/face/plate models.
26
26
  *
27
27
  * {domain}/{family}/{format}/{filename}
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-7ewQjTle.js");
5
+ const require_dist = require("../dist-C1goFC50.js");
6
6
  const require_codec_runtime = require("../codec-runtime-BOk-13PN.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/audio-codec-nodeav/addon/index.ts
@@ -1,4 +1,4 @@
1
- import { C as audioCodecCapability, i as BaseAddon, j as errMsg } from "../dist-C5jnNl0n.mjs";
1
+ import { C as audioCodecCapability, i as BaseAddon, j as errMsg } from "../dist-XRXnZrVC.mjs";
2
2
  import { t as DecodeRuntime } from "../codec-runtime-BsqlEjPi.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  //#region src/audio-codec-nodeav/addon/index.ts
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-7ewQjTle.js");
5
+ const require_dist = require("../dist-C1goFC50.js");
6
6
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/decoder-nodeav/frame-ring-sink.ts
@@ -1,4 +1,4 @@
1
- import { O as decoderCapability, _ as RingBuffer, h as HWACCEL_OPTIONS, i as BaseAddon, j as errMsg, l as DEFAULT_DECODER_HWACCEL_CONFIG } from "../dist-C5jnNl0n.mjs";
1
+ import { O as decoderCapability, _ as RingBuffer, h as HWACCEL_OPTIONS, i as BaseAddon, j as errMsg, l as DEFAULT_DECODER_HWACCEL_CONFIG } from "../dist-XRXnZrVC.mjs";
2
2
  import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
3
3
  import { randomUUID } from "node:crypto";
4
4
  //#region src/decoder-nodeav/frame-ring-sink.ts
@@ -3,14 +3,14 @@ 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-7ewQjTle.js");
6
+ const require_dist = require("../dist-C1goFC50.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_path = require("node:path");
10
10
  node_path = require_chunk.__toESM(node_path);
11
11
  let node_os = require("node:os");
12
12
  node_os = require_chunk.__toESM(node_os);
13
- let _camstack_core = require("@camstack/core");
13
+ let _camstack_system = require("@camstack/system");
14
14
  let node_child_process = require("node:child_process");
15
15
  let sharp = require("sharp");
16
16
  sharp = require_chunk.__toESM(sharp);
@@ -3873,6 +3873,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3873
3873
  device: "cuda"
3874
3874
  };
3875
3875
  } catch {}
3876
+ try {
3877
+ const fsmod = require("node:fs");
3878
+ const isIntel = require("node:os").cpus()[0]?.model?.includes("Intel") ?? false;
3879
+ const hasIgpu = fsmod.existsSync("/dev/dri/renderD128");
3880
+ const hasNpu = fsmod.existsSync("/dev/accel/accel0") || fsmod.existsSync("/dev/accel");
3881
+ if (isIntel && (hasIgpu || hasNpu)) return {
3882
+ runtime: "python",
3883
+ backend: "openvino",
3884
+ format: "openvino",
3885
+ device: "auto"
3886
+ };
3887
+ } catch {}
3876
3888
  return {
3877
3889
  runtime: "python",
3878
3890
  backend: "onnx",
@@ -3884,22 +3896,68 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3884
3896
  async setApi(addonCtx) {
3885
3897
  this.addonCtx = addonCtx;
3886
3898
  }
3899
+ /**
3900
+ * Fetch platform-probe data for engine + device gating. Returns null for
3901
+ * both fields when the probe cap is not yet reachable (cold-start / probe
3902
+ * addon not installed), so callers fall back to the full static catalog.
3903
+ */
3904
+ async fetchProbeGatingData() {
3905
+ try {
3906
+ const api = this.addonCtx?.api;
3907
+ if (!api) return {
3908
+ availableBackends: null,
3909
+ hardware: null
3910
+ };
3911
+ const caps = await api.platformProbe.getCapabilities.query();
3912
+ if (!caps?.scores) return {
3913
+ availableBackends: null,
3914
+ hardware: null
3915
+ };
3916
+ return {
3917
+ availableBackends: caps.scores.filter((s) => s.runtime === "python" && s.available).map((s) => s.backend),
3918
+ hardware: caps.hardware ?? null
3919
+ };
3920
+ } catch {
3921
+ return {
3922
+ availableBackends: null,
3923
+ hardware: null
3924
+ };
3925
+ }
3926
+ }
3887
3927
  async getSchema(engine) {
3888
3928
  if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
3889
3929
  const format = engine.format;
3890
3930
  const slots = buildSchemaSlots(format, this.modelsDir);
3931
+ const { availableBackends, hardware } = await this.fetchProbeGatingData();
3891
3932
  return {
3892
- availableEngines: this.getAvailableEnginesWithDevices(),
3933
+ availableEngines: this.getAvailableEnginesWithDevices(availableBackends, hardware),
3893
3934
  selectedEngine: { ...engine },
3894
3935
  slots
3895
3936
  };
3896
3937
  }
3897
3938
  async getAvailableEngines() {
3898
- return this.getAvailableEnginesWithDevices().map((e) => e.engine);
3939
+ const { availableBackends, hardware } = await this.fetchProbeGatingData();
3940
+ return this.getAvailableEnginesWithDevices(availableBackends, hardware).map((e) => e.engine);
3899
3941
  }
3900
- getAvailableEnginesWithDevices() {
3901
- const engines = [];
3902
- if (process.platform === "darwin") engines.push({
3942
+ /**
3943
+ * Build the engine catalog filtered by probe data.
3944
+ *
3945
+ * When `availableBackends` is not null, only engines whose backend is
3946
+ * reported as available by the platform-probe cap are included. This
3947
+ * prevents the benchmark picker from offering (e.g.) CoreML on an Intel
3948
+ * Linux host where the Python coremltools package is absent.
3949
+ *
3950
+ * When `hardware` is not null, the per-engine device list is narrowed to
3951
+ * entries that are valid on this host's hardware (no Intel NPU → no 'npu'
3952
+ * device for OpenVINO, no NVIDIA GPU → no 'cuda' for ONNX, etc.).
3953
+ *
3954
+ * When either argument is null (probe unreachable / cold-start), the full
3955
+ * static catalog is returned unchanged — conservative fallback so the UI
3956
+ * still renders all options rather than going blank.
3957
+ */
3958
+ getAvailableEnginesWithDevices(availableBackends, hardware) {
3959
+ const allEngines = [];
3960
+ if (process.platform === "darwin") allEngines.push({
3903
3961
  engine: {
3904
3962
  runtime: "python",
3905
3963
  backend: "coreml",
@@ -3908,7 +3966,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3908
3966
  devices: COREML_DEVICES,
3909
3967
  defaultDevice: "all"
3910
3968
  });
3911
- engines.push({
3969
+ allEngines.push({
3912
3970
  engine: {
3913
3971
  runtime: "python",
3914
3972
  backend: "openvino",
@@ -3917,7 +3975,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3917
3975
  devices: OPENVINO_DEVICES,
3918
3976
  defaultDevice: "auto"
3919
3977
  });
3920
- engines.push({
3978
+ allEngines.push({
3921
3979
  engine: {
3922
3980
  runtime: "python",
3923
3981
  backend: "onnx",
@@ -3926,7 +3984,19 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3926
3984
  devices: ONNX_PYTHON_DEVICES,
3927
3985
  defaultDevice: "cpu"
3928
3986
  });
3929
- return engines;
3987
+ if (!availableBackends) return allEngines;
3988
+ return allEngines.filter((e) => availableBackends.includes(e.engine.backend)).map((e) => {
3989
+ if (!hardware) return e;
3990
+ const filtered = filterDeviceOptionsByHardware(e.devices.map((d) => ({
3991
+ value: d.id,
3992
+ label: d.label
3993
+ })), e.engine.backend, hardware);
3994
+ const allowedIds = new Set(filtered.map((o) => o.value));
3995
+ return {
3996
+ ...e,
3997
+ devices: e.devices.filter((d) => allowedIds.has(d.id))
3998
+ };
3999
+ });
3930
4000
  }
3931
4001
  async getDefaultSteps(engine) {
3932
4002
  return buildDefaultStepTree(engine.format);
@@ -4001,7 +4071,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4001
4071
  const formats = {};
4002
4072
  for (const [formatKey, entry] of Object.entries(m.formats)) {
4003
4073
  if (!entry) continue;
4004
- const downloaded = (0, _camstack_core.isModelDownloaded)(this.modelsDir, m, formatKey);
4074
+ const downloaded = (0, _camstack_system.isModelDownloaded)(this.modelsDir, m, formatKey);
4005
4075
  formats[formatKey] = {
4006
4076
  url: entry.url,
4007
4077
  sizeMB: entry.sizeMB,
@@ -4018,7 +4088,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4018
4088
  };
4019
4089
  })
4020
4090
  }));
4021
- const engines = this.getAvailableEnginesWithDevices();
4091
+ const { availableBackends, hardware } = await this.fetchProbeGatingData();
4092
+ const engines = this.getAvailableEnginesWithDevices(availableBackends, hardware);
4022
4093
  const nodeBackends = [];
4023
4094
  const pythonBackends = [];
4024
4095
  for (const e of engines) if (e.engine.runtime === "node") nodeBackends.push({
@@ -4128,7 +4199,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4128
4199
  const { modelId, format, addonId } = input;
4129
4200
  const modelEntry = getStepDefinition(addonId).models.find((m) => m.id === modelId);
4130
4201
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
4131
- if (!(0, _camstack_core.deleteModelFromDisk)(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4202
+ if (!(0, _camstack_system.deleteModelFromDisk)(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
4132
4203
  this.log.info("Model deleted from disk", { meta: {
4133
4204
  modelId,
4134
4205
  format
@@ -4583,7 +4654,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4583
4654
  const format = this.currentEngine?.format ?? "onnx";
4584
4655
  for (const step of needed) {
4585
4656
  const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4586
- if (modelEntry && !(0, _camstack_core.isModelDownloaded)(this.modelsDir, modelEntry, format)) {
4657
+ if (modelEntry && !(0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) {
4587
4658
  this.log.info("Downloading model for step", { meta: {
4588
4659
  modelId: step.modelId,
4589
4660
  format,
@@ -4608,7 +4679,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4608
4679
  /** Download a model with retry + exponential backoff */
4609
4680
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
4610
4681
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
4611
- await (0, _camstack_core.ensureModel)(this.modelsDir, entry, format, onProgress);
4682
+ await (0, _camstack_system.ensureModel)(this.modelsDir, entry, format, onProgress);
4612
4683
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
4613
4684
  return;
4614
4685
  } catch (err) {
@@ -5083,11 +5154,11 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5083
5154
  } });
5084
5155
  continue;
5085
5156
  }
5086
- if (!(0, _camstack_core.isModelDownloaded)(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5157
+ if (!(0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) this.log.info("Downloading model", { meta: {
5087
5158
  modelId: step.modelId,
5088
5159
  format
5089
5160
  } });
5090
- downloads.push((0, _camstack_core.ensureModel)(this.modelsDir, modelEntry, format).then(() => {}));
5161
+ downloads.push((0, _camstack_system.ensureModel)(this.modelsDir, modelEntry, format).then(() => {}));
5091
5162
  }
5092
5163
  await Promise.all(downloads);
5093
5164
  await this.ensureBackendDeps(this.currentEngine);
@@ -5119,19 +5190,20 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5119
5190
  */
5120
5191
  async loadEngine() {
5121
5192
  const store = await this.readStore();
5122
- const runtime = store["engineRuntime"];
5123
- const backend = store["engineBackend"];
5124
- if (typeof runtime === "string" && typeof backend === "string" && runtime && backend) {
5193
+ const storedRuntime = store["engineRuntime"];
5194
+ const storedBackend = store["engineBackend"];
5195
+ if (typeof storedBackend === "string" && storedBackend.length > 0) {
5196
+ const backend = storedBackend;
5125
5197
  const storedDevice = typeof store["engineDevice"] === "string" ? String(store["engineDevice"]) : "";
5126
5198
  const detected = DetectionPipelineProvider.detectBestEngine();
5127
- if (runtime === "python" && !DetectionPipelineProvider.isPythonBackendAvailable(backend, this.executorOptions.pythonPath ?? "")) {
5199
+ const migratedBackend = typeof storedRuntime === "string" && storedRuntime === "node" && backend === "cpu" ? "onnx" : backend;
5200
+ if (!DetectionPipelineProvider.isPythonBackendAvailable(migratedBackend, this.executorOptions.pythonPath ?? "")) {
5128
5201
  this.log.warn("Stored engine backend unavailable on this node — falling back to detected best", { meta: {
5129
- stored: `${runtime}/${backend}`,
5130
- fallback: `${detected.runtime}/${detected.backend}`
5202
+ stored: migratedBackend,
5203
+ fallback: `${detected.backend}`
5131
5204
  } });
5132
5205
  return detected;
5133
5206
  }
5134
- const migratedBackend = runtime === "node" && backend === "cpu" ? "onnx" : backend;
5135
5207
  const device = storedDevice || detected.device;
5136
5208
  return {
5137
5209
  runtime: "python",
@@ -5301,11 +5373,42 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5301
5373
  return { success: true };
5302
5374
  }
5303
5375
  async reprobeEngine() {
5304
- const best = DetectionPipelineProvider.detectBestEngine();
5305
- const probedLabel = `${best.runtime}/${best.backend}/${best.device ?? "default"}`;
5306
- await this.writeStore({ probedBestEngine: probedLabel });
5307
- this.log.info("Re-probed engine", { meta: {
5308
- runtime: best.runtime,
5376
+ const api = this.addonCtx?.api;
5377
+ let best;
5378
+ if (api) try {
5379
+ const caps = await api.platformProbe.getCapabilities.query();
5380
+ const bs = caps?.bestScore;
5381
+ if (bs && bs.runtime === "python") {
5382
+ const probeBackend = bs.backend;
5383
+ const probeDevice = (() => {
5384
+ const hw = caps.hardware;
5385
+ if (probeBackend === "coreml") return hw?.npu?.type === "apple-ane" ? "ane" : "all";
5386
+ if (probeBackend === "openvino") {
5387
+ if (hw?.npu?.type === "intel-npu") return "npu";
5388
+ if (hw?.gpu?.type === "intel") return "gpu";
5389
+ return "auto";
5390
+ }
5391
+ if (probeBackend === "onnx") return hw?.gpu?.type === "nvidia" ? "cuda" : "cpu";
5392
+ return "cpu";
5393
+ })();
5394
+ best = {
5395
+ runtime: "python",
5396
+ backend: probeBackend,
5397
+ format: backendToFormat(probeBackend),
5398
+ device: probeDevice
5399
+ };
5400
+ } else best = DetectionPipelineProvider.detectBestEngine();
5401
+ } catch {
5402
+ best = DetectionPipelineProvider.detectBestEngine();
5403
+ }
5404
+ else best = DetectionPipelineProvider.detectBestEngine();
5405
+ const probedLabel = `${best.backend}/${best.device ?? "default"}`;
5406
+ await this.writeStore({
5407
+ probedBestEngine: probedLabel,
5408
+ engineBackend: best.backend,
5409
+ engineDevice: best.device ?? "cpu"
5410
+ });
5411
+ this.log.info("Re-probed engine — wrote back engineBackend + engineDevice", { meta: {
5309
5412
  backend: best.backend,
5310
5413
  device: best.device ?? null,
5311
5414
  probedBestEngine: probedLabel
@@ -5467,7 +5570,7 @@ function buildSchemaSlots(format, modelsDir) {
5467
5570
  id: m.id,
5468
5571
  name: m.name,
5469
5572
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
5470
- downloaded: (0, _camstack_core.isModelDownloaded)(modelsDir, m, f),
5573
+ downloaded: (0, _camstack_system.isModelDownloaded)(modelsDir, m, f),
5471
5574
  sizeMB: entry.sizeMB
5472
5575
  }]))
5473
5576
  })),
@@ -5701,10 +5804,34 @@ function resolveAddonPythonDir() {
5701
5804
  for (const c of candidates) if (node_fs.existsSync(node_path.join(c, "inference_pool.py"))) return c;
5702
5805
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
5703
5806
  }
5704
- var RUNTIMES = [{
5705
- value: "python",
5706
- label: "Python (CoreML / OpenVINO / ONNX Runtime)"
5707
- }];
5807
+ /**
5808
+ * Returns true when proactive OpenVINO installation is warranted.
5809
+ *
5810
+ * Gate: Linux host + Intel iGPU or Intel NPU detected.
5811
+ *
5812
+ * Intentionally addon-local — addons are self-contained and cannot import
5813
+ * `@camstack/system` internals (see architecture invariant: no cross-addon
5814
+ * imports). This mirrors the logic in `resolveRuntimePackages` from that
5815
+ * package without importing it.
5816
+ *
5817
+ * darwin is never true: coremltools handles Apple Silicon + Intel Mac.
5818
+ * linux-amd (or any non-Intel linux GPU) is never true: the openvino
5819
+ * package has no AMD backend and would fail at import time.
5820
+ *
5821
+ * @internal exported only for unit tests in the same package
5822
+ */
5823
+ function shouldInstallOpenvino(hardware) {
5824
+ if (hardware.platform !== "linux") return false;
5825
+ const hasIntelGpu = hardware.gpu?.type === "intel";
5826
+ const hasIntelNpu = hardware.npu?.type === "intel-npu";
5827
+ return hasIntelGpu || hasIntelNpu;
5828
+ }
5829
+ /**
5830
+ * Full catalog of execution providers. The settings UI only shows the subset
5831
+ * reported as available by the platform-probe cap (`getGlobalSettings` gates
5832
+ * options by probe scores). Kept as the static universe so the static schema
5833
+ * (returned before a live ctx is available) still lists all fields.
5834
+ */
5708
5835
  var BACKENDS_BY_RUNTIME = { python: [
5709
5836
  {
5710
5837
  value: "coreml",
@@ -5775,6 +5902,53 @@ var DEVICES_BY_BACKEND = {
5775
5902
  label: "CPU"
5776
5903
  }]
5777
5904
  };
5905
+ /**
5906
+ * Filter the per-backend device option list by what the platform probe
5907
+ * reports as available hardware on this host. Rules (derived from the
5908
+ * confirmed target model):
5909
+ *
5910
+ * coreml:
5911
+ * - `ane` only when `hardware.npu?.type === 'apple-ane'`
5912
+ * - `gpu` and `all` always shown (CPU+GPU present on every Mac)
5913
+ * - `cpu` always shown
5914
+ *
5915
+ * openvino:
5916
+ * - `npu` only when `hardware.npu?.type === 'intel-npu'`
5917
+ * - `gpu` only when `hardware.gpu?.type === 'intel'`
5918
+ * - `auto` and `cpu` always shown
5919
+ *
5920
+ * onnx:
5921
+ * - `cuda` only when `hardware.gpu?.type === 'nvidia'`
5922
+ * - `coreml` only when `hardware.npu?.type === 'apple-ane'` (CoreML EP)
5923
+ * - `cpu` always shown
5924
+ *
5925
+ * When `hardware` is null (probe unreachable), the full catalog is returned
5926
+ * unchanged so the UI still renders all options.
5927
+ */
5928
+ function filterDeviceOptionsByHardware(options, backend, hardware) {
5929
+ if (!hardware) return options;
5930
+ const hasAppleAne = hardware.npu?.type === "apple-ane";
5931
+ const hasIntelNpu = hardware.npu?.type === "intel-npu";
5932
+ const hasIntelGpu = hardware.gpu?.type === "intel";
5933
+ const hasNvidiaGpu = hardware.gpu?.type === "nvidia";
5934
+ switch (backend) {
5935
+ case "coreml": return options.filter((o) => {
5936
+ if (o.value === "ane") return hasAppleAne;
5937
+ return true;
5938
+ });
5939
+ case "openvino": return options.filter((o) => {
5940
+ if (o.value === "npu") return hasIntelNpu;
5941
+ if (o.value === "gpu") return hasIntelGpu;
5942
+ return true;
5943
+ });
5944
+ case "onnx": return options.filter((o) => {
5945
+ if (o.value === "cuda") return hasNvidiaGpu;
5946
+ if (o.value === "coreml") return hasAppleAne;
5947
+ return true;
5948
+ });
5949
+ default: return options;
5950
+ }
5951
+ }
5778
5952
  var BACKEND_TO_FORMAT = {
5779
5953
  cpu: "onnx",
5780
5954
  coreml: "coreml",
@@ -5906,13 +6080,13 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
5906
6080
  title: "Object detection",
5907
6081
  tab: "engine",
5908
6082
  order: 0,
5909
- description: "Vision-detection backend (object detection, face detection, plate OCR). Chain: runtime backend hardware. Backend options depend on runtime; hardware options depend on backend. All three restart the addon on change.",
6083
+ 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.",
5910
6084
  fields: [
5911
6085
  this.field({
5912
6086
  type: "text",
5913
6087
  key: "probedBestEngine",
5914
6088
  label: "Probed best",
5915
- description: "Auto-detected best engine for this host (format: runtime/backend/device). Click the refresh icon to re-run the probe — the detected values get written back into the three fields below.",
6089
+ 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.",
5916
6090
  readonlyField: true,
5917
6091
  default: "",
5918
6092
  actions: [{
@@ -5921,19 +6095,10 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
5921
6095
  tooltip: "Re-probe engine"
5922
6096
  }]
5923
6097
  }),
5924
- this.field({
5925
- type: "select",
5926
- key: "engineRuntime",
5927
- label: "Runtime",
5928
- options: [...RUNTIMES],
5929
- default: DEFAULT_CONFIG.engineRuntime,
5930
- immediate: true,
5931
- requiresRestart: true
5932
- }),
5933
6098
  this.field({
5934
6099
  type: "select",
5935
6100
  key: "engineBackend",
5936
- label: "Backend",
6101
+ label: "Execution provider",
5937
6102
  options: [...BACKENDS_BY_RUNTIME.python],
5938
6103
  default: DEFAULT_CONFIG.engineBackend,
5939
6104
  immediate: true,
@@ -6071,14 +6236,16 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6071
6236
  ...overlay
6072
6237
  } : stored;
6073
6238
  const runtime = "python";
6074
- const availableBackends = await this.resolveAvailableBackends(ctx, runtime);
6239
+ const probeResult = await this.resolveProbeData(ctx, runtime);
6240
+ const availableBackends = probeResult.availableBackends;
6241
+ const hardware = probeResult.hardware;
6075
6242
  const runtimeBackends = availableBackends.length > 0 ? BACKENDS_BY_RUNTIME[runtime].filter((b) => availableBackends.includes(b.value)) : BACKENDS_BY_RUNTIME[runtime];
6076
6243
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6077
- const backend = runtimeBackends.find((b) => b.value === storedBackend)?.value ?? runtimeBackends[0]?.value ?? "coreml";
6078
- const deviceOptions = DEVICES_BY_BACKEND[backend] ?? [{
6244
+ const backend = runtimeBackends.find((b) => b.value === storedBackend)?.value ?? probeResult.bestBackend ?? runtimeBackends[0]?.value ?? "coreml";
6245
+ const deviceOptions = filterDeviceOptionsByHardware(DEVICES_BY_BACKEND[backend] ?? [{
6079
6246
  value: "cpu",
6080
6247
  label: "CPU"
6081
- }];
6248
+ }], backend, hardware);
6082
6249
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6083
6250
  const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? deviceOptions[0]?.value ?? "cpu";
6084
6251
  const raw = {
@@ -6128,28 +6295,47 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6128
6295
  }, raw);
6129
6296
  }
6130
6297
  /**
6131
- * Ask the platform-probe cap which backends are actually installable
6132
- * on this node (checks for `openvino`, `coremltools`, `onnxruntime`
6133
- * Python modules + hardware presence). Returns a subset of the static
6134
- * catalog; an empty list signals the probe wasn't available so the
6135
- * caller should fall back to the full catalog rather than hiding
6136
- * every option.
6298
+ * Fetch the platform-probe capabilities and return:
6299
+ * - `availableBackends`: backends the probe reports as available for `runtime`
6300
+ * (empty caller falls back to full catalog).
6301
+ * - `hardware`: the probed hardware info (null when probe not reachable).
6302
+ * - `bestBackend`: the backend from the probe's `bestScore` (null when unavailable).
6303
+ *
6304
+ * A single probe call is made so both backend AND device gating use the
6305
+ * same snapshot without doubling the cap round-trip.
6137
6306
  */
6138
- async resolveAvailableBackends(ctx, runtime) {
6307
+ async resolveProbeData(ctx, runtime) {
6139
6308
  try {
6140
6309
  const api = ctx?.api;
6141
- if (!api) return [];
6310
+ if (!api) return {
6311
+ availableBackends: [],
6312
+ hardware: null,
6313
+ bestBackend: null
6314
+ };
6142
6315
  const caps = await api.platformProbe.getCapabilities.query();
6143
- if (!caps?.scores) return [];
6316
+ if (!caps?.scores) return {
6317
+ availableBackends: [],
6318
+ hardware: null,
6319
+ bestBackend: null
6320
+ };
6144
6321
  const out = /* @__PURE__ */ new Set();
6145
6322
  for (const s of caps.scores) {
6146
6323
  if (s.runtime !== runtime) continue;
6147
6324
  if (!s.available) continue;
6148
6325
  out.add(s.backend);
6149
6326
  }
6150
- return [...out];
6327
+ const bestBackend = caps.bestScore?.runtime === runtime ? caps.bestScore.backend ?? null : null;
6328
+ return {
6329
+ availableBackends: [...out],
6330
+ hardware: caps.hardware ?? null,
6331
+ bestBackend
6332
+ };
6151
6333
  } catch {
6152
- return [];
6334
+ return {
6335
+ availableBackends: [],
6336
+ hardware: null,
6337
+ bestBackend: null
6338
+ };
6153
6339
  }
6154
6340
  }
6155
6341
  /**
@@ -6190,6 +6376,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6190
6376
  const pythonPath = await this.ctx.deps.ensurePython();
6191
6377
  if (pythonPath) this.pythonPath = pythonPath;
6192
6378
  else this.ctx.logger.warn("Embedded Python unavailable — runtime=\"python\" pipelines will fail until the download succeeds.");
6379
+ await this.proactivelyInstallOpenvino();
6193
6380
  const effectiveTuning = this.resolveBackendTuning();
6194
6381
  this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
6195
6382
  concurrency: effectiveTuning.concurrency,
@@ -6255,6 +6442,32 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6255
6442
  getModelCatalog() {
6256
6443
  return ALL_STEPS.flatMap((s) => [...s.models]);
6257
6444
  }
6445
+ /**
6446
+ * Proactively install the OpenVINO Python package when Intel hardware
6447
+ * (iGPU or NPU) is detected. Called once from `onInitialize`, before the
6448
+ * provider is constructed, so the module is available when the platform
6449
+ * probe runs its next `import openvino.runtime` check.
6450
+ *
6451
+ * Failure is non-fatal: a warning is logged and the addon continues with
6452
+ * the onnx-cpu baseline. The hardware query itself is also best-effort —
6453
+ * if the platform-probe cap isn't wired yet the error is caught here.
6454
+ */
6455
+ async proactivelyInstallOpenvino() {
6456
+ if (!this.pythonAddonDir || !this.pythonPath) return;
6457
+ try {
6458
+ const hardware = await this.ctx.api.platformProbe.getHardware.query();
6459
+ if (!shouldInstallOpenvino(hardware)) return;
6460
+ this.ctx.logger.info("Intel hardware detected — proactively installing OpenVINO Python package", { meta: {
6461
+ gpu: hardware.gpu?.type ?? null,
6462
+ npu: hardware.npu?.type ?? null
6463
+ } });
6464
+ const requirementsFile = node_path.join(this.pythonAddonDir, "requirements-openvino.txt");
6465
+ await this.ctx.deps.installPythonRequirements(requirementsFile);
6466
+ this.ctx.logger.info("Proactive OpenVINO install complete");
6467
+ } catch (err) {
6468
+ this.ctx.logger.warn("Proactive OpenVINO install failed — falling back to onnx-cpu baseline", { meta: { error: err instanceof Error ? err.message : String(err) } });
6469
+ }
6470
+ }
6258
6471
  async onShutdown() {
6259
6472
  if (this.engineMetricsTimer) {
6260
6473
  clearInterval(this.engineMetricsTimer);
@@ -6347,5 +6560,7 @@ exports.ALL_STEPS = ALL_STEPS;
6347
6560
  exports.DetectionPipelineProvider = DetectionPipelineProvider;
6348
6561
  exports.backendToFormat = backendToFormat;
6349
6562
  exports.default = DetectionPipelineAddon;
6563
+ exports.filterDeviceOptionsByHardware = filterDeviceOptionsByHardware;
6350
6564
  exports.getDefaultModelForFormat = getDefaultModelForFormat;
6351
6565
  exports.getStepDefinition = getStepDefinition;
6566
+ exports.shouldInstallOpenvino = shouldInstallOpenvino;