@camstack/addon-pipeline 1.0.5 → 1.0.6

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 +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  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 +704 -398
  8. package/dist/detection-pipeline/index.mjs +705 -398
  9. package/dist/{dist-v0PZCoV-.js → dist-BiUtYscO.js} +201 -2
  10. package/dist/{dist-CP2uP-D8.mjs → dist-DsDFrG0I.mjs} +201 -2
  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 +1 -1
  16. package/dist/recorder/index.mjs +1 -1
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Hzarxdhd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbTGCEnd.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-3STWM0yI.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-SlpG44Ip.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dsz9DmNr.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-BXhCtKAA.mjs → hostInit-BJ3QDdFs.mjs} +3 -3
  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-CQxn6ukH.js → MaskShapeCanvas-DI4BY7W2-B4oJIlgF.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-C1EEbk2V-BziDLK12.js → MotionZonesSettings-C1EEbk2V-CUopGB1R.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-D3KDk03_.js → PrivacyMaskSettings-APgPLF7p-CyTsHaor.js} +1 -1
  28. package/embed-dist/assets/{index-CvgJINQE.js → index-hwJEVIPM.js} +9 -9
  29. package/embed-dist/index.html +1 -1
  30. package/package.json +1 -1
  31. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-c9YxHYlI.mjs +0 -26
@@ -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-v0PZCoV-.js");
6
+ const require_dist = require("../dist-BiUtYscO.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");
@@ -3535,7 +3535,273 @@ function walkFieldsForDefaults(fields, out) {
3535
3535
  }
3536
3536
  }
3537
3537
  //#endregion
3538
+ //#region src/detection-pipeline/runtimes.ts
3539
+ var AUTO = {
3540
+ value: "auto",
3541
+ label: "Auto"
3542
+ };
3543
+ var CPU = {
3544
+ value: "cpu",
3545
+ label: "CPU"
3546
+ };
3547
+ var RUNTIMES = [
3548
+ {
3549
+ id: "onnx",
3550
+ label: "ONNX Runtime",
3551
+ supports: () => true,
3552
+ devices: (hw) => {
3553
+ if (!hw) return [CPU];
3554
+ const out = [CPU];
3555
+ if (hw.gpu?.type === "nvidia") out.push({
3556
+ value: "cuda",
3557
+ label: "CUDA"
3558
+ });
3559
+ if (hw.npu?.type === "apple-ane") out.push({
3560
+ value: "coreml",
3561
+ label: "CoreML EP"
3562
+ });
3563
+ return out;
3564
+ },
3565
+ defaultDevice: "cpu",
3566
+ modelFormat: "onnx",
3567
+ pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3568
+ tuning: {
3569
+ concurrency: 4,
3570
+ batchMode: "list",
3571
+ maxBatchSize: 8,
3572
+ intraOpThreads: 0
3573
+ }
3574
+ },
3575
+ {
3576
+ id: "openvino",
3577
+ label: "OpenVINO",
3578
+ supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
3579
+ devices: (hw) => {
3580
+ if (!hw) return [AUTO];
3581
+ const out = [AUTO, CPU];
3582
+ if (hw.gpu?.type === "intel") out.push({
3583
+ value: "gpu",
3584
+ label: "GPU"
3585
+ });
3586
+ if (hw.npu?.type === "intel-npu") out.push({
3587
+ value: "npu",
3588
+ label: "NPU"
3589
+ });
3590
+ return out;
3591
+ },
3592
+ defaultDevice: "auto",
3593
+ modelFormat: "openvino",
3594
+ pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3595
+ tuning: {
3596
+ concurrency: 1,
3597
+ batchMode: "none",
3598
+ numStreams: 0
3599
+ }
3600
+ },
3601
+ {
3602
+ id: "coreml",
3603
+ label: "CoreML",
3604
+ supports: (env) => env.platform === "darwin",
3605
+ devices: (hw) => {
3606
+ const all = {
3607
+ value: "all",
3608
+ label: "All (ANE + GPU + CPU)"
3609
+ };
3610
+ if (!hw) return [all];
3611
+ const out = [all];
3612
+ if (hw.npu?.type === "apple-ane") out.push({
3613
+ value: "ane",
3614
+ label: "Apple Neural Engine"
3615
+ });
3616
+ out.push({
3617
+ value: "gpu",
3618
+ label: "GPU"
3619
+ }, CPU);
3620
+ return out;
3621
+ },
3622
+ defaultDevice: "all",
3623
+ modelFormat: "coreml",
3624
+ pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3625
+ tuning: {
3626
+ concurrency: 1,
3627
+ batchMode: "none",
3628
+ windowMs: 8,
3629
+ maxBatchSize: 8,
3630
+ numWorkers: 1
3631
+ }
3632
+ }
3633
+ ];
3634
+ function def(id) {
3635
+ const d = RUNTIMES.find((r) => r.id === id);
3636
+ if (!d) throw new Error(`Unknown runtime: ${id}`);
3637
+ return d;
3638
+ }
3639
+ function supportedRuntimes(env) {
3640
+ return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3641
+ }
3642
+ function runtimeDevices(id, hardware) {
3643
+ return def(id).devices(hardware);
3644
+ }
3645
+ function defaultDeviceFor(id) {
3646
+ return def(id).defaultDevice;
3647
+ }
3648
+ function pythonRequirementsFor(id) {
3649
+ return def(id).pythonRequirements;
3650
+ }
3651
+ function modelFormatFor(id) {
3652
+ return def(id).modelFormat;
3653
+ }
3654
+ function tuningFor(id) {
3655
+ return def(id).tuning;
3656
+ }
3657
+ function runtimeLabel(id) {
3658
+ return def(id).label;
3659
+ }
3660
+ /**
3661
+ * Proactive-install hint kept for back-compat (re-exported from index.ts).
3662
+ * Intel on Linux warrants installing openvino; coremltools covers macOS;
3663
+ * openvino has no AMD/NVIDIA backend.
3664
+ */
3665
+ function shouldInstallOpenvino(env) {
3666
+ if (env.platform === "darwin") return false;
3667
+ return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3668
+ }
3669
+ //#endregion
3670
+ //#region src/detection-pipeline/engine-provisioner.ts
3671
+ /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3672
+ var BACKOFF_SCHEDULE_MS = [
3673
+ 5e3,
3674
+ 15e3,
3675
+ 3e4,
3676
+ 6e4,
3677
+ 12e4,
3678
+ 3e5
3679
+ ];
3680
+ var IDLE_STATE = {
3681
+ runtimeId: null,
3682
+ device: null,
3683
+ state: "idle"
3684
+ };
3685
+ var EngineProvisioner = class {
3686
+ fx;
3687
+ current = IDLE_STATE;
3688
+ /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
3689
+ generation = 0;
3690
+ cancelTimer = null;
3691
+ retryIndex = 0;
3692
+ constructor(fx) {
3693
+ this.fx = fx;
3694
+ }
3695
+ get state() {
3696
+ return this.current;
3697
+ }
3698
+ isReady() {
3699
+ return this.current.state === "ready";
3700
+ }
3701
+ select(runtimeId, device) {
3702
+ this.generation++;
3703
+ this.retryIndex = 0;
3704
+ this.clearTimer();
3705
+ this.transition({
3706
+ runtimeId,
3707
+ device,
3708
+ state: "installing",
3709
+ progress: 0
3710
+ });
3711
+ this.provision(this.generation, runtimeId, device);
3712
+ }
3713
+ dispose() {
3714
+ this.generation++;
3715
+ this.clearTimer();
3716
+ }
3717
+ clearTimer() {
3718
+ if (this.cancelTimer !== null) {
3719
+ this.cancelTimer();
3720
+ this.cancelTimer = null;
3721
+ }
3722
+ }
3723
+ transition(next) {
3724
+ this.current = next;
3725
+ this.fx.onChange(next);
3726
+ }
3727
+ async provision(gen, runtimeId, device) {
3728
+ try {
3729
+ await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
3730
+ if (gen !== this.generation) return;
3731
+ this.transition({
3732
+ runtimeId,
3733
+ device,
3734
+ state: "verifying",
3735
+ progress: 100
3736
+ });
3737
+ await this.fx.verify(runtimeId, device);
3738
+ if (gen !== this.generation) return;
3739
+ this.retryIndex = 0;
3740
+ this.transition({
3741
+ runtimeId,
3742
+ device,
3743
+ state: "ready"
3744
+ });
3745
+ } catch (err) {
3746
+ if (gen !== this.generation) return;
3747
+ const message = err instanceof Error ? err.message : String(err);
3748
+ const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
3749
+ this.retryIndex++;
3750
+ const nextRetryAt = this.fx.now() + delay;
3751
+ this.transition({
3752
+ runtimeId,
3753
+ device,
3754
+ state: "failed",
3755
+ error: message,
3756
+ nextRetryAt
3757
+ });
3758
+ this.cancelTimer = this.fx.setTimer(delay, () => {
3759
+ if (gen !== this.generation) return;
3760
+ this.cancelTimer = null;
3761
+ this.transition({
3762
+ runtimeId,
3763
+ device,
3764
+ state: "installing",
3765
+ progress: 0
3766
+ });
3767
+ this.provision(gen, runtimeId, device);
3768
+ });
3769
+ }
3770
+ }
3771
+ };
3772
+ //#endregion
3773
+ //#region src/detection-pipeline/auto-pick.ts
3774
+ var PREFERENCE = [
3775
+ "coreml",
3776
+ "openvino",
3777
+ "onnx"
3778
+ ];
3779
+ /**
3780
+ * Pure function — picks the best supported runtime for the given hardware env.
3781
+ *
3782
+ * Logic:
3783
+ * 1. If `bestBackendHint` is in the supported set, use it.
3784
+ * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
3785
+ * 3. Floor to `'onnx'` (always supported).
3786
+ *
3787
+ * Device is always `defaultDeviceFor(chosen)`.
3788
+ */
3789
+ function pickBestRuntime(env, bestBackendHint) {
3790
+ const supported = supportedRuntimes(env);
3791
+ const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
3792
+ return {
3793
+ runtimeId: chosen,
3794
+ device: defaultDeviceFor(chosen)
3795
+ };
3796
+ }
3797
+ //#endregion
3538
3798
  //#region src/detection-pipeline/provider.ts
3799
+ /**
3800
+ * DetectionPipelineProvider — implements IPipelineExecutorProvider.
3801
+ *
3802
+ * This is the main provider that consumers (DetectionWiring, Benchmark, tRPC)
3803
+ * interact with. It manages the engine factory, pipeline executor, and config persistence.
3804
+ */
3539
3805
  var KEY_STEPS = "pipelineSteps";
3540
3806
  var KEY_ENGINE = "pipelineEngine";
3541
3807
  var KEY_TEMPLATES = "pipelineTemplates";
@@ -3714,6 +3980,22 @@ function parseWavToAudioChunk(filePath) {
3714
3980
  function enginesEqual(a, b) {
3715
3981
  return a.runtime === b.runtime && a.backend === b.backend && a.format === b.format && (a.device ?? null) === (b.device ?? null);
3716
3982
  }
3983
+ /** Build a `RuntimeEnv` from the running process + probed hardware. */
3984
+ function runtimeEnvFromProcess(hardware) {
3985
+ return {
3986
+ platform: process.platform,
3987
+ arch: process.arch,
3988
+ hardware
3989
+ };
3990
+ }
3991
+ /** Normalize the loose probe hardware shape into a typed `ProbedHardware`. */
3992
+ function toProbedHardware(hw) {
3993
+ if (!hw) return null;
3994
+ return {
3995
+ npu: hw.npu ? { type: hw.npu.type } : null,
3996
+ gpu: hw.gpu ? { type: hw.gpu.type } : null
3997
+ };
3998
+ }
3717
3999
  var DetectionPipelineProvider = class DetectionPipelineProvider {
3718
4000
  modelsDir;
3719
4001
  eventBus;
@@ -3754,6 +4036,24 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3754
4036
  */
3755
4037
  ready = false;
3756
4038
  /**
4039
+ * Lazy detection-engine runtime provisioner (Phase 2). Owns the
4040
+ * idle → installing → verifying → ready state machine for the
4041
+ * currently-selected engine and is the SOLE authority the inference
4042
+ * gate (`assertEngineReady`) and the cap snapshot
4043
+ * (`getEngineProvisioning`) read from. Built lazily once the addon
4044
+ * context is wired (real effects need `ctx.deps` + the python dir),
4045
+ * so it is null on a freshly-constructed provider — in which case the
4046
+ * gate reports the cold `idle` state and refuses inference.
4047
+ */
4048
+ provisioner = null;
4049
+ /**
4050
+ * True when `init()` found no persisted engine choice (first boot).
4051
+ * `setApi()` reads this flag and auto-picks the best supported runtime
4052
+ * using probe data (hardware + bestScore hint), persists the selection,
4053
+ * then clears the flag.
4054
+ */
4055
+ needsAutoPick = false;
4056
+ /**
3757
4057
  * Warm cache for benchmark engine-override runs.
3758
4058
  *
3759
4059
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -3802,6 +4102,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3802
4102
  backend: stored.backend,
3803
4103
  format: stored.format
3804
4104
  } });
4105
+ } else {
4106
+ this.needsAutoPick = true;
4107
+ this.log.info("No persisted engine — auto-pick deferred to setApi()");
3805
4108
  }
3806
4109
  }
3807
4110
  /**
@@ -3838,6 +4141,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3838
4141
  * on a warmed pool.
3839
4142
  */
3840
4143
  async warmPool() {
4144
+ if (this.getEngineProvisioning().state !== "ready") {
4145
+ this.log.info("warmPool deferred — engine not provisioned yet", { meta: { state: this.getEngineProvisioning().state } });
4146
+ return;
4147
+ }
3841
4148
  await this.ensureEngineFactory();
3842
4149
  }
3843
4150
  /** True when the engine + model pool are fully warmed and inference-ready. */
@@ -3895,108 +4202,281 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3895
4202
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
3896
4203
  async setApi(addonCtx) {
3897
4204
  this.addonCtx = addonCtx;
4205
+ if (this.needsAutoPick) {
4206
+ await this.autoPickAndPersist();
4207
+ this.needsAutoPick = false;
4208
+ }
4209
+ this.startProvisioningForCurrentEngine();
4210
+ }
4211
+ /**
4212
+ * Auto-pick the best supported runtime at first boot (no stored engine).
4213
+ * Uses the platform-probe cap's hardware + bestScore hint when available;
4214
+ * falls back to platform/arch when the probe cap is not yet reachable.
4215
+ * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4216
+ * boots load it via `loadEngine()` and skip this path.
4217
+ */
4218
+ async autoPickAndPersist() {
4219
+ let hardware = null;
4220
+ let bestBackendHint = null;
4221
+ try {
4222
+ const api = this.addonCtx?.api;
4223
+ if (api) {
4224
+ const caps = await api.platformProbe.getCapabilities.query();
4225
+ hardware = caps?.hardware ?? null;
4226
+ const bs = caps?.bestScore;
4227
+ if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
4228
+ }
4229
+ } catch {}
4230
+ const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), bestBackendHint);
4231
+ const engine = {
4232
+ runtime: "python",
4233
+ backend: pick.runtimeId,
4234
+ format: modelFormatFor(pick.runtimeId),
4235
+ device: pick.device
4236
+ };
4237
+ this.currentEngine = engine;
4238
+ await this.writeStore({
4239
+ engineBackend: pick.runtimeId,
4240
+ engineDevice: pick.device
4241
+ });
4242
+ this.log.info("Auto-picked engine at first boot", { meta: {
4243
+ backend: pick.runtimeId,
4244
+ device: pick.device,
4245
+ hint: bestBackendHint ?? "none"
4246
+ } });
4247
+ }
4248
+ /** Map a backend string to a known RuntimeId, flooring to onnx. */
4249
+ toRuntimeId(backend) {
4250
+ return [
4251
+ "onnx",
4252
+ "openvino",
4253
+ "coreml"
4254
+ ].find((id) => id === backend) ?? "onnx";
4255
+ }
4256
+ /**
4257
+ * Build the {@link EngineProvisioner} with REAL effects. Built lazily
4258
+ * (effects need `addonCtx.deps` + the python dir), so the machine only
4259
+ * exists once the context is wired. The machine is the SOLE authority
4260
+ * the inference gate and the cap snapshot read.
4261
+ */
4262
+ buildProvisioner() {
4263
+ return new EngineProvisioner({
4264
+ requirementsFor: pythonRequirementsFor,
4265
+ modelFormatFor,
4266
+ installRequirements: async (files) => {
4267
+ const pythonAddonDir = this.executorOptions.pythonAddonDir;
4268
+ if (!pythonAddonDir || !this.addonCtx) return;
4269
+ for (const basename of files) {
4270
+ const file = node_path.join(pythonAddonDir, basename);
4271
+ if (!node_fs.existsSync(file)) continue;
4272
+ await this.addonCtx.deps.installPythonRequirements(file);
4273
+ }
4274
+ },
4275
+ ensureModelForFormat: async (format) => {
4276
+ await this.ensureModelsForCurrentSteps(format);
4277
+ },
4278
+ verify: async (backend, device) => {
4279
+ await this.verifyEngine(backend, device);
4280
+ },
4281
+ now: () => Date.now(),
4282
+ setTimer: (ms, cb) => {
4283
+ const t = setTimeout(cb, ms);
4284
+ return () => clearTimeout(t);
4285
+ },
4286
+ onChange: (state) => {
4287
+ this.log.info("engine provisioning", { meta: { ...state } });
4288
+ this.emitEngineProvisioning(state);
4289
+ }
4290
+ });
4291
+ }
4292
+ /**
4293
+ * (Re)build the provisioner and `select()` the engine currently held in
4294
+ * `this.currentEngine`. Idempotent: a fresh `select()` bumps the
4295
+ * machine's generation so any in-flight provisioning for a superseded
4296
+ * engine is ignored. No-op for non-python engines (nothing to install /
4297
+ * verify — the gate treats them as ready immediately below).
4298
+ */
4299
+ startProvisioningForCurrentEngine() {
4300
+ if (!this.provisioner) this.provisioner = this.buildProvisioner();
4301
+ const engine = this.currentEngine;
4302
+ const runtimeId = this.toRuntimeId(engine.backend);
4303
+ const device = engine.device ?? "cpu";
4304
+ const snapshot = this.provisioner.state;
4305
+ if (snapshot.runtimeId === runtimeId && snapshot.device === device && snapshot.state !== "idle") return;
4306
+ this.provisioner.select(runtimeId, device);
4307
+ }
4308
+ /**
4309
+ * Re-trigger provisioning after the operator changes the engine
4310
+ * cascade (`engineBackend`/`engineDevice`). Reloads the persisted
4311
+ * selection into `this.currentEngine`, then `select()`s it — the
4312
+ * machine cancels any superseded provisioning and drives the new
4313
+ * runtime through installing → verifying → ready.
4314
+ */
4315
+ async onEngineSelectionChanged() {
4316
+ const stored = await this.loadEngine();
4317
+ if (stored) this.currentEngine = stored;
4318
+ this.startProvisioningForCurrentEngine();
4319
+ }
4320
+ /**
4321
+ * Per-node engine-provisioning snapshot. The cap routes `{ nodeId }` for
4322
+ * provider resolution only (the router strips it); each node returns its
4323
+ * own local machine state. Cold (pre-`setApi`) provider → `idle`.
4324
+ */
4325
+ getEngineProvisioning() {
4326
+ return this.provisioner?.state ?? {
4327
+ runtimeId: null,
4328
+ device: null,
4329
+ state: "idle"
4330
+ };
4331
+ }
4332
+ /**
4333
+ * Inference gate. Rejects with the current phase/error unless the engine
4334
+ * provisioning machine is `ready`. Callers (`ensureExecutor`) surface the
4335
+ * state and skip inference for the cycle rather than falling back — the
4336
+ * cross-process frame caller (`pipeline-runner.runInference`) already
4337
+ * catches and drops the frame, so a throw here never crash-loops.
4338
+ */
4339
+ async assertEngineReady() {
4340
+ const s = this.getEngineProvisioning();
4341
+ if (s.state !== "ready") throw new Error(`Detection engine not ready: ${s.state}${s.error ? ` — ${s.error}` : ""}`);
4342
+ }
4343
+ /**
4344
+ * Download the model artifacts for `format` needed by the currently
4345
+ * configured steps. Reuses the existing model-download path
4346
+ * (`ensureModelsForSteps`/`downloadWithRetry`), specialised to the
4347
+ * provisioning format by temporarily projecting the current engine onto
4348
+ * `format` for the download fan-out.
4349
+ */
4350
+ async ensureModelsForCurrentSteps(format) {
4351
+ const steps = await this.getGlobalSteps();
4352
+ if (!steps || steps.length === 0) return;
4353
+ for (const step of flattenSteps(steps)) {
4354
+ if (!step.enabled) continue;
4355
+ const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4356
+ if (!modelEntry) continue;
4357
+ if ((0, _camstack_system.isModelDownloaded)(this.modelsDir, modelEntry, format)) continue;
4358
+ await this.downloadWithRetry(modelEntry, format, 3);
4359
+ }
4360
+ }
4361
+ /**
4362
+ * 1-shot import + compile probe for `(backend, device)`. Spawns a
4363
+ * throwaway Python inference pool with EMPTY steps via the existing
4364
+ * `EngineFactory.initialize([])` path — this imports the selected
4365
+ * runtime (onnxruntime / openvino / coremltools) and throws on a
4366
+ * `ModuleNotFoundError` or a runtime-load/compile error — then disposes
4367
+ * the pool. No models are loaded; the probe only proves the runtime is
4368
+ * importable on this host. No-op (resolves) for non-python engines.
4369
+ */
4370
+ async verifyEngine(backend, device) {
4371
+ if (!this.executorOptions.pythonPath) throw new Error("verifyEngine: pythonPath not resolved — embedded Python unavailable");
4372
+ const probe = new EngineFactory({
4373
+ engine: {
4374
+ runtime: "python",
4375
+ backend,
4376
+ format: modelFormatFor(backend),
4377
+ device
4378
+ },
4379
+ modelsDir: this.modelsDir,
4380
+ logger: this.log.child("engine-verify"),
4381
+ pythonPath: this.executorOptions.pythonPath,
4382
+ concurrency: 1,
4383
+ numWorkers: 1
4384
+ });
4385
+ try {
4386
+ await probe.initialize([]);
4387
+ } finally {
4388
+ await probe.dispose().catch(() => void 0);
4389
+ }
4390
+ }
4391
+ /**
4392
+ * Emit the {@link EventCategory.PipelineEngineProvisioning} telemetry
4393
+ * event on every machine transition so the Pipeline page can drive a
4394
+ * live per-node indicator without polling the cap snapshot.
4395
+ */
4396
+ emitEngineProvisioning(state) {
4397
+ const eventBus = this.addonCtx?.eventBus ?? this.eventBus;
4398
+ if (!eventBus) return;
4399
+ const rawNodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
4400
+ const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
4401
+ eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineEngineProvisioning, {
4402
+ type: "node",
4403
+ id: nodeId,
4404
+ nodeId
4405
+ }, { ...state }));
3898
4406
  }
3899
4407
  /**
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.
4408
+ * Fetch the probed HARDWARE for engine + device gating. Returns null when the
4409
+ * probe cap is not yet reachable (cold-start / probe addon not installed), so
4410
+ * callers fall back to the registry's safe minimum. The engine OFFER derives
4411
+ * from hardware ONLY — install state (probe `scores`) never gates it.
3903
4412
  */
3904
4413
  async fetchProbeGatingData() {
3905
4414
  try {
3906
4415
  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
- };
4416
+ if (!api) return { hardware: null };
4417
+ return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
3920
4418
  } catch {
3921
- return {
3922
- availableBackends: null,
3923
- hardware: null
3924
- };
4419
+ return { hardware: null };
3925
4420
  }
3926
4421
  }
3927
4422
  async getSchema(engine) {
3928
4423
  if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
3929
4424
  const format = engine.format;
3930
4425
  const slots = buildSchemaSlots(format, this.modelsDir);
3931
- const { availableBackends, hardware } = await this.fetchProbeGatingData();
4426
+ const { hardware } = await this.fetchProbeGatingData();
4427
+ const env = runtimeEnvFromProcess(toProbedHardware(hardware));
3932
4428
  return {
3933
- availableEngines: this.getAvailableEnginesWithDevices(availableBackends, hardware),
4429
+ availableEngines: this.getAvailableEnginesWithDevices(env).map((e) => this.toAvailableEngine(e)),
3934
4430
  selectedEngine: { ...engine },
3935
4431
  slots
3936
4432
  };
3937
4433
  }
3938
4434
  async getAvailableEngines() {
3939
- const { availableBackends, hardware } = await this.fetchProbeGatingData();
3940
- return this.getAvailableEnginesWithDevices(availableBackends, hardware).map((e) => e.engine);
4435
+ const { hardware } = await this.fetchProbeGatingData();
4436
+ const env = runtimeEnvFromProcess(toProbedHardware(hardware));
4437
+ return this.getAvailableEnginesWithDevices(env).map((e) => ({ ...e.engine }));
3941
4438
  }
3942
4439
  /**
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.).
4440
+ * Build the engine catalog from the HARDWARE-supported runtime set.
3953
4441
  *
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.
4442
+ * The offered backend list comes from `supportedRuntimes(env)` platform,
4443
+ * arch and probed hardware ONLY, never from which Python packages are
4444
+ * installed (install state is provisioned on demand in Phase 2). Each
4445
+ * engine's device list is `runtimeDevices(backend, env.hardware)` (probe-
4446
+ * driven; default-only when `env.hardware` is null). The result is
4447
+ * conservative when the probe is unreachable: onnx floor + default device.
3957
4448
  */
3958
- getAvailableEnginesWithDevices(availableBackends, hardware) {
3959
- const allEngines = [];
3960
- if (process.platform === "darwin") allEngines.push({
4449
+ getAvailableEnginesWithDevices(env) {
4450
+ return supportedRuntimes(env).map((id) => ({
3961
4451
  engine: {
3962
4452
  runtime: "python",
3963
- backend: "coreml",
3964
- format: "coreml"
4453
+ backend: id,
4454
+ format: modelFormatFor(id),
4455
+ device: defaultDeviceFor(id)
3965
4456
  },
3966
- devices: COREML_DEVICES,
3967
- defaultDevice: "all"
3968
- });
3969
- allEngines.push({
3970
- engine: {
3971
- runtime: "python",
3972
- backend: "openvino",
3973
- format: "openvino"
3974
- },
3975
- devices: OPENVINO_DEVICES,
3976
- defaultDevice: "auto"
3977
- });
3978
- allEngines.push({
4457
+ devices: runtimeDevices(id, env.hardware).map((d) => ({
4458
+ id: d.value,
4459
+ label: d.label
4460
+ }))
4461
+ }));
4462
+ }
4463
+ /**
4464
+ * Adapt an {@link EngineWithDevices} (registry shape) to the wire
4465
+ * {@link AvailableEngine} consumed by `PipelineSchema.availableEngines`.
4466
+ */
4467
+ toAvailableEngine(e) {
4468
+ return {
3979
4469
  engine: {
3980
- runtime: "python",
3981
- backend: "onnx",
3982
- format: "onnx"
4470
+ runtime: e.engine.runtime,
4471
+ backend: e.engine.backend,
4472
+ format: e.engine.format
3983
4473
  },
3984
- devices: ONNX_PYTHON_DEVICES,
3985
- defaultDevice: "cpu"
3986
- });
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,
4474
+ devices: e.devices.map((d) => ({
4475
+ id: d.id,
3992
4476
  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
- });
4477
+ })),
4478
+ defaultDevice: e.engine.device
4479
+ };
4000
4480
  }
4001
4481
  async getDefaultSteps(engine) {
4002
4482
  return buildDefaultStepTree(engine.format);
@@ -4088,21 +4568,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4088
4568
  };
4089
4569
  })
4090
4570
  }));
4091
- const { availableBackends, hardware } = await this.fetchProbeGatingData();
4092
- const engines = this.getAvailableEnginesWithDevices(availableBackends, hardware);
4571
+ const { hardware } = await this.fetchProbeGatingData();
4572
+ const env = runtimeEnvFromProcess(toProbedHardware(hardware));
4573
+ const engines = this.getAvailableEnginesWithDevices(env);
4093
4574
  const nodeBackends = [];
4094
4575
  const pythonBackends = [];
4095
- for (const e of engines) if (e.engine.runtime === "node") nodeBackends.push({
4576
+ for (const e of engines) pythonBackends.push({
4096
4577
  id: e.engine.backend,
4097
4578
  label: e.engine.backend.toUpperCase(),
4098
4579
  available: true,
4099
- device: e.defaultDevice ?? e.devices[0]?.id ?? "cpu"
4100
- });
4101
- else pythonBackends.push({
4102
- id: e.engine.backend,
4103
- label: e.engine.backend.toUpperCase(),
4104
- available: true,
4105
- device: e.defaultDevice ?? e.devices[0]?.id ?? "cpu",
4580
+ device: e.engine.device || e.devices[0]?.id || "cpu",
4106
4581
  modelFormat: e.engine.format,
4107
4582
  pythonModule: pythonModuleForBackend(e.engine.backend)
4108
4583
  });
@@ -5109,6 +5584,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5109
5584
  async ensureEngineFactory() {
5110
5585
  if (this.initPromise) await this.initPromise;
5111
5586
  if (this.engineFactory) return;
5587
+ await this.assertEngineReady();
5112
5588
  await this.ensureBackendDeps(this.currentEngine);
5113
5589
  this.engineFactory = new EngineFactory({
5114
5590
  engine: this.currentEngine,
@@ -5124,6 +5600,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5124
5600
  async ensureExecutor() {
5125
5601
  if (this.initPromise) await this.initPromise;
5126
5602
  if (!this.engineFactory) {
5603
+ await this.assertEngineReady();
5127
5604
  this.initPromise = this.doInitialize();
5128
5605
  try {
5129
5606
  await this.initPromise;
@@ -5722,70 +6199,45 @@ function applyDeviceOverridesToTree(tree, rootStepId, overrides) {
5722
6199
  }
5723
6200
  } : root) };
5724
6201
  }
5725
- var COREML_DEVICES = [
5726
- {
5727
- id: "all",
5728
- label: "All (CPU + GPU + Neural Engine)",
5729
- description: "CoreML auto-selects the best available compute unit"
5730
- },
5731
- {
5732
- id: "ane",
5733
- label: "Neural Engine",
5734
- description: "Apple Neural Engine — fastest, lowest power"
5735
- },
5736
- {
5737
- id: "gpu",
5738
- label: "GPU",
5739
- description: "Apple GPU — good for larger models"
5740
- },
5741
- {
5742
- id: "cpu",
5743
- label: "CPU only",
5744
- description: "CPU fallback — slowest but always available"
5745
- }
5746
- ];
5747
- var OPENVINO_DEVICES = [
5748
- {
5749
- id: "auto",
5750
- label: "Auto",
5751
- description: "OpenVINO auto-selects best device (recommended)"
5752
- },
5753
- {
5754
- id: "cpu",
5755
- label: "CPU",
5756
- description: "Intel CPU with AVX/SSE optimizations"
5757
- },
5758
- {
5759
- id: "gpu",
5760
- label: "GPU (Intel)",
5761
- description: "Intel integrated/discrete GPU via OpenCL"
5762
- },
5763
- {
5764
- id: "npu",
5765
- label: "NPU (Intel)",
5766
- description: "Intel Neural Processing Unit (Meteor Lake+)"
5767
- }
5768
- ];
5769
- var ONNX_PYTHON_DEVICES = [
5770
- {
5771
- id: "cpu",
5772
- label: "CPU",
5773
- description: "ONNX Runtime CPU provider"
5774
- },
5775
- {
5776
- id: "cuda",
5777
- label: "CUDA (NVIDIA)",
5778
- description: "NVIDIA GPU via CUDA"
5779
- },
5780
- {
5781
- id: "coreml",
5782
- label: "CoreML",
5783
- description: "Apple CoreML execution provider (macOS)"
6202
+ //#endregion
6203
+ //#region src/detection-pipeline/python-readiness.ts
6204
+ /**
6205
+ * Resolve the embedded Python once, turning the null/throw failure modes into
6206
+ * an explicit, logged result. Python is a hard prerequisite for every runtime;
6207
+ * a silent miss here is what produced the "probe empty → bad offer" symptom.
6208
+ */
6209
+ async function ensurePythonReady(deps, log) {
6210
+ try {
6211
+ const pythonPath = await deps.ensurePython();
6212
+ if (!pythonPath) {
6213
+ log.error("Embedded Python unavailable — inference runtimes cannot be provisioned", {});
6214
+ return {
6215
+ pythonPath: null,
6216
+ ok: false
6217
+ };
6218
+ }
6219
+ log.info("Embedded Python ready", { meta: { pythonPath } });
6220
+ return {
6221
+ pythonPath,
6222
+ ok: true
6223
+ };
6224
+ } catch (err) {
6225
+ log.error("Embedded Python provisioning threw", { meta: { error: err instanceof Error ? err.message : String(err) } });
6226
+ return {
6227
+ pythonPath: null,
6228
+ ok: false
6229
+ };
5784
6230
  }
5785
- ];
6231
+ }
5786
6232
  //#endregion
5787
6233
  //#region src/detection-pipeline/index.ts
5788
6234
  /**
6235
+ * addon-detection-pipeline — unified detection pipeline for CamStack.
6236
+ *
6237
+ * Single addon replaces 13 separate vision addon classes.
6238
+ * Each pipeline step is a class implementing IPipelineStep.
6239
+ */
6240
+ /**
5789
6241
  * Locate the addon's bundled `python/` dir at runtime. Mirrors the
5790
6242
  * resolution done in `shared-inference-pool.ts:resolveScriptPath` —
5791
6243
  * tries the published package first (resolves through node_modules),
@@ -5804,151 +6256,6 @@ function resolveAddonPythonDir() {
5804
6256
  for (const c of candidates) if (node_fs.existsSync(node_path.join(c, "inference_pool.py"))) return c;
5805
6257
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
5806
6258
  }
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
- */
5835
- var BACKENDS_BY_RUNTIME = { python: [
5836
- {
5837
- value: "coreml",
5838
- label: "CoreML"
5839
- },
5840
- {
5841
- value: "openvino",
5842
- label: "OpenVINO"
5843
- },
5844
- {
5845
- value: "onnx",
5846
- label: "ONNX Runtime"
5847
- }
5848
- ] };
5849
- var DEVICES_BY_BACKEND = {
5850
- coreml: [
5851
- {
5852
- value: "all",
5853
- label: "All (ANE + GPU + CPU)"
5854
- },
5855
- {
5856
- value: "ane",
5857
- label: "Apple Neural Engine"
5858
- },
5859
- {
5860
- value: "gpu",
5861
- label: "GPU"
5862
- },
5863
- {
5864
- value: "cpu",
5865
- label: "CPU"
5866
- }
5867
- ],
5868
- openvino: [
5869
- {
5870
- value: "auto",
5871
- label: "Auto"
5872
- },
5873
- {
5874
- value: "cpu",
5875
- label: "CPU"
5876
- },
5877
- {
5878
- value: "gpu",
5879
- label: "GPU"
5880
- },
5881
- {
5882
- value: "npu",
5883
- label: "NPU"
5884
- }
5885
- ],
5886
- onnx: [
5887
- {
5888
- value: "cpu",
5889
- label: "CPU"
5890
- },
5891
- {
5892
- value: "cuda",
5893
- label: "CUDA"
5894
- },
5895
- {
5896
- value: "coreml",
5897
- label: "CoreML EP"
5898
- }
5899
- ],
5900
- cpu: [{
5901
- value: "cpu",
5902
- label: "CPU"
5903
- }]
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
- }
5952
6259
  var BACKEND_TO_FORMAT = {
5953
6260
  cpu: "onnx",
5954
6261
  coreml: "coreml",
@@ -5956,37 +6263,38 @@ var BACKEND_TO_FORMAT = {
5956
6263
  onnx: "onnx"
5957
6264
  };
5958
6265
  /**
5959
- * Per-backend tuning defaults derived from the matrix sweep
5960
- * (`scripts/bench-pool-matrix.py` + `bench-coreml-patterns.py`).
5961
- * Applied by `getGlobalSettings(overlay)` so the UI defaults track
5962
- * the currently-selected backend instead of a single global value.
6266
+ * Every runtime the registry knows about, in offer order. Used only to build
6267
+ * the STATIC base schema (returned by `globalSettingsSchema()` before a live
6268
+ * ctx exists, e.g. addon-registry boot introspection). The live, hardware-
6269
+ * gated subset is injected by `getGlobalSettings()` via `supportedRuntimes(env)`.
5963
6270
  */
5964
- var TUNING_DEFAULTS = {
5965
- coreml: {
5966
- concurrency: 1,
5967
- batchMode: "none",
5968
- windowMs: 8,
5969
- maxBatchSize: 8,
5970
- numWorkers: 1
5971
- },
5972
- openvino: {
5973
- concurrency: 1,
5974
- batchMode: "none",
5975
- numStreams: 0
5976
- },
5977
- onnx: {
5978
- concurrency: 4,
5979
- batchMode: "list",
5980
- maxBatchSize: 8,
5981
- intraOpThreads: 0
5982
- },
5983
- cpu: {
5984
- concurrency: 4,
5985
- batchMode: "list",
5986
- maxBatchSize: 8,
5987
- intraOpThreads: 0
5988
- }
5989
- };
6271
+ var ALL_RUNTIME_IDS = [
6272
+ "onnx",
6273
+ "openvino",
6274
+ "coreml"
6275
+ ];
6276
+ /**
6277
+ * Narrow an arbitrary stored backend string to a known `RuntimeId`. Legacy or
6278
+ * unknown values (e.g. the removed `'cpu'` backend) fall back to the `onnx`
6279
+ * floor so the registry lookup never throws.
6280
+ */
6281
+ function toRuntimeId(backend) {
6282
+ return ALL_RUNTIME_IDS.find((id) => id === backend) ?? "onnx";
6283
+ }
6284
+ /** Static universe of backend options (every runtime). Labels from the registry. */
6285
+ var STATIC_BACKEND_OPTIONS = ALL_RUNTIME_IDS.map((id) => ({
6286
+ value: id,
6287
+ label: runtimeLabel(id)
6288
+ }));
6289
+ /**
6290
+ * Static device options for the schema's default backend. The live device
6291
+ * list per the selected backend + probed hardware is injected by
6292
+ * `getGlobalSettings()` via `runtimeDevices(backend, hardware)`.
6293
+ */
6294
+ var STATIC_DEFAULT_DEVICE_OPTIONS = runtimeDevices(ALL_RUNTIME_IDS[0], null).map((d) => ({
6295
+ value: d.value,
6296
+ label: d.label
6297
+ }));
5990
6298
  var DEFAULT_CONFIG = {
5991
6299
  concurrency: 0,
5992
6300
  batchMode: "",
@@ -5996,8 +6304,8 @@ var DEFAULT_CONFIG = {
5996
6304
  intraOpThreads: 0,
5997
6305
  numWorkers: 0,
5998
6306
  engineRuntime: "python",
5999
- engineBackend: "coreml",
6000
- engineDevice: "all",
6307
+ engineBackend: "onnx",
6308
+ engineDevice: "cpu",
6001
6309
  probedBestEngine: ""
6002
6310
  };
6003
6311
  /** Derive the model-format from a backend value. Called by the provider. */
@@ -6099,7 +6407,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6099
6407
  type: "select",
6100
6408
  key: "engineBackend",
6101
6409
  label: "Execution provider",
6102
- options: [...BACKENDS_BY_RUNTIME.python],
6410
+ options: [...STATIC_BACKEND_OPTIONS],
6103
6411
  default: DEFAULT_CONFIG.engineBackend,
6104
6412
  immediate: true,
6105
6413
  requiresRestart: true
@@ -6108,7 +6416,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6108
6416
  type: "select",
6109
6417
  key: "engineDevice",
6110
6418
  label: "Hardware device",
6111
- options: [...DEVICES_BY_BACKEND.coreml],
6419
+ options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
6112
6420
  default: DEFAULT_CONFIG.engineDevice,
6113
6421
  immediate: true,
6114
6422
  requiresRestart: true
@@ -6221,12 +6529,15 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6221
6529
  }] });
6222
6530
  }
6223
6531
  /**
6224
- * Override to inject dynamic backend + device options based on the
6225
- * currently-stored runtime + backend. The base schema ships with
6226
- * placeholder lists; here we replace them with the valid subset for
6227
- * whatever the operator has picked. Combined with `immediate: true`
6228
- * on the selects, the UI can refetch after each change and see the
6229
- * next level's options narrow down.
6532
+ * Override to inject the hardware-driven backend + device options.
6533
+ *
6534
+ * The OFFERED backend list comes from `supportedRuntimes(env)` platform,
6535
+ * arch and probed hardware ONLY, never from which Python packages are
6536
+ * installed. A host's hardware is what it can physically run; install state
6537
+ * is provisioned on demand (Phase 2). Device options come from
6538
+ * `runtimeDevices(backend, hardware)` (probe-driven; default-only when the
6539
+ * probe is unreachable). Stored backend / device snap back to the registry
6540
+ * floor / default when they fall outside the offered set.
6230
6541
  */
6231
6542
  async getGlobalSettings(overlay) {
6232
6543
  const ctx = this.ctxIfReady;
@@ -6235,19 +6546,18 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6235
6546
  ...stored,
6236
6547
  ...overlay
6237
6548
  } : stored;
6238
- const runtime = "python";
6239
- const probeResult = await this.resolveProbeData(ctx, runtime);
6240
- const availableBackends = probeResult.availableBackends;
6241
- const hardware = probeResult.hardware;
6242
- const runtimeBackends = availableBackends.length > 0 ? BACKENDS_BY_RUNTIME[runtime].filter((b) => availableBackends.includes(b.value)) : BACKENDS_BY_RUNTIME[runtime];
6549
+ const env = await this.probeHardwareEnv();
6550
+ const hardware = env.hardware;
6551
+ const offered = supportedRuntimes(env);
6552
+ const runtimeBackends = offered.map((id) => ({
6553
+ value: id,
6554
+ label: runtimeLabel(id)
6555
+ }));
6243
6556
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6244
- const backend = runtimeBackends.find((b) => b.value === storedBackend)?.value ?? probeResult.bestBackend ?? runtimeBackends[0]?.value ?? "coreml";
6245
- const deviceOptions = filterDeviceOptionsByHardware(DEVICES_BY_BACKEND[backend] ?? [{
6246
- value: "cpu",
6247
- label: "CPU"
6248
- }], backend, hardware);
6557
+ const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6558
+ const deviceOptions = runtimeDevices(backend, hardware);
6249
6559
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6250
- const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? deviceOptions[0]?.value ?? "cpu";
6560
+ const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6251
6561
  const raw = {
6252
6562
  ...merged,
6253
6563
  engineBackend: backend,
@@ -6255,7 +6565,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6255
6565
  };
6256
6566
  const schema = this.globalSettingsSchema();
6257
6567
  if (!schema) return { sections: [] };
6258
- const tuning = TUNING_DEFAULTS[backend] ?? {};
6568
+ const tuning = tuningFor(backend);
6259
6569
  return require_dist.hydrateSchema({
6260
6570
  ...schema,
6261
6571
  sections: schema.sections.map((section) => ({
@@ -6268,12 +6578,15 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6268
6578
  };
6269
6579
  if (field.key === "engineDevice") return {
6270
6580
  ...field,
6271
- options: [...deviceOptions],
6272
- default: deviceOptions[0]?.value ?? "cpu"
6581
+ options: deviceOptions.map((d) => ({
6582
+ value: d.value,
6583
+ label: d.label
6584
+ })),
6585
+ default: defaultDeviceFor(backend)
6273
6586
  };
6274
- if (field.key === "batchMode" && tuning.batchMode !== void 0) return {
6587
+ if (field.key === "batchMode" && tuning["batchMode"] !== void 0) return {
6275
6588
  ...field,
6276
- default: tuning.batchMode
6589
+ default: tuning["batchMode"]
6277
6590
  };
6278
6591
  }
6279
6592
  if (field.type === "slider" && "key" in field) {
@@ -6295,55 +6608,47 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6295
6608
  }, raw);
6296
6609
  }
6297
6610
  /**
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).
6611
+ * Resolve the runtime env (platform + arch + probed hardware) used to derive
6612
+ * the OFFERED backend list. The hardware comes from the platform-probe cap
6613
+ * (`getCapabilities`); when the cap isn't reachable yet (cold-start / probe
6614
+ * addon not installed) `hardware` is null, which the registry collapses to
6615
+ * the safe minimum (onnx floor; default-only devices).
6303
6616
  *
6304
- * A single probe call is made so both backend AND device gating use the
6305
- * same snapshot without doubling the cap round-trip.
6617
+ * Protected seam overridable by tests (canned hardware) and reused by the
6618
+ * Phase 2 auto-pick path. NEVER reads install state.
6619
+ */
6620
+ async probeHardwareEnv() {
6621
+ const hardware = await this.resolveProbeHardware();
6622
+ return {
6623
+ platform: process.platform,
6624
+ arch: process.arch,
6625
+ hardware
6626
+ };
6627
+ }
6628
+ /**
6629
+ * Fetch the probed hardware from the platform-probe cap. Returns null when
6630
+ * the cap is not reachable (caller falls back to the registry's safe minimum).
6306
6631
  */
6307
- async resolveProbeData(ctx, runtime) {
6632
+ async resolveProbeHardware() {
6308
6633
  try {
6309
- const api = ctx?.api;
6310
- if (!api) return {
6311
- availableBackends: [],
6312
- hardware: null,
6313
- bestBackend: null
6314
- };
6315
- const caps = await api.platformProbe.getCapabilities.query();
6316
- if (!caps?.scores) return {
6317
- availableBackends: [],
6318
- hardware: null,
6319
- bestBackend: null
6320
- };
6321
- const out = /* @__PURE__ */ new Set();
6322
- for (const s of caps.scores) {
6323
- if (s.runtime !== runtime) continue;
6324
- if (!s.available) continue;
6325
- out.add(s.backend);
6326
- }
6327
- const bestBackend = caps.bestScore?.runtime === runtime ? caps.bestScore.backend ?? null : null;
6634
+ const api = this.ctxIfReady?.api;
6635
+ if (!api) return null;
6636
+ const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6637
+ if (!hw) return null;
6328
6638
  return {
6329
- availableBackends: [...out],
6330
- hardware: caps.hardware ?? null,
6331
- bestBackend
6639
+ npu: hw.npu ? { type: hw.npu.type } : null,
6640
+ gpu: hw.gpu ? { type: hw.gpu.type } : null
6332
6641
  };
6333
6642
  } catch {
6334
- return {
6335
- availableBackends: [],
6336
- hardware: null,
6337
- bestBackend: null
6338
- };
6643
+ return null;
6339
6644
  }
6340
6645
  }
6341
6646
  /**
6342
6647
  * Resolve the effective pool tuning for the configured backend.
6343
6648
  *
6344
- * Reads `TUNING_DEFAULTS[backend]` and ignores any persisted override
6345
- * for `concurrency / batchMode / windowMs / maxBatchSize / numStreams /
6346
- * intraOpThreads`. Stored values are quietly discarded so an old
6649
+ * Reads the registry's `tuningFor(backend)` and ignores any persisted
6650
+ * override for `concurrency / batchMode / windowMs / maxBatchSize /
6651
+ * numStreams / intraOpThreads`. Stored values are quietly discarded so an old
6347
6652
  * suboptimal user-override (saved when the UI exposed these knobs)
6348
6653
  * cannot resurrect itself after a restart.
6349
6654
  *
@@ -6352,18 +6657,17 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6352
6657
  * a reason to disagree.
6353
6658
  */
6354
6659
  resolveBackendTuning() {
6355
- const t = TUNING_DEFAULTS[this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend] ?? TUNING_DEFAULTS["onnx"];
6356
- const tAny = t;
6660
+ const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6357
6661
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6358
- const str = (v, dflt) => typeof v === "string" && v !== "" ? v : dflt;
6662
+ const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6359
6663
  return {
6360
- concurrency: num(t.concurrency, 1),
6361
- batchMode: str(t.batchMode, "window"),
6362
- windowMs: num(t.windowMs, 2),
6363
- maxBatchSize: num(t.maxBatchSize, 8),
6364
- numStreams: num(t.numStreams, 0),
6365
- intraOpThreads: num(t.intraOpThreads, 0),
6366
- numWorkers: num(tAny["numWorkers"], 1)
6664
+ concurrency: num(t["concurrency"], 1),
6665
+ batchMode: batch(t["batchMode"], "window"),
6666
+ windowMs: num(t["windowMs"], 2),
6667
+ maxBatchSize: num(t["maxBatchSize"], 8),
6668
+ numStreams: num(t["numStreams"], 0),
6669
+ intraOpThreads: num(t["intraOpThreads"], 0),
6670
+ numWorkers: num(t["numWorkers"], 1)
6367
6671
  };
6368
6672
  }
6369
6673
  async onInitialize() {
@@ -6373,9 +6677,9 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6373
6677
  }).catch(() => "camstack-data/models");
6374
6678
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6375
6679
  this.pythonAddonDir = resolveAddonPythonDir();
6376
- const pythonPath = await this.ctx.deps.ensurePython();
6377
- if (pythonPath) this.pythonPath = pythonPath;
6378
- else this.ctx.logger.warn("Embedded Python unavailable runtime=\"python\" pipelines will fail until the download succeeds.");
6680
+ const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6681
+ if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
6682
+ else this.ctx.logger.warn("Detection engine boot continues without Python selection will provision on demand", {});
6379
6683
  await this.proactivelyInstallOpenvino();
6380
6684
  const effectiveTuning = this.resolveBackendTuning();
6381
6685
  this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
@@ -6482,7 +6786,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6482
6786
  * Snapshot the pool-bound subset of the EFFECTIVE tuning (post-
6483
6787
  * `resolveBackendTuning`). Stored config values for these fields are
6484
6788
  * ignored, so this snapshot only changes when `engineBackend` flips
6485
- * onto a different `TUNING_DEFAULTS` row.
6789
+ * onto a different `tuningFor` row.
6486
6790
  */
6487
6791
  snapshotPoolConfig() {
6488
6792
  const t = this.resolveBackendTuning();
@@ -6515,6 +6819,9 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
6515
6819
  * lifecycle (engineFactory rebuild on next runPipeline).
6516
6820
  */
6517
6821
  async onConfigChanged() {
6822
+ if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6823
+ this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6824
+ });
6518
6825
  if (!this.poolConfigChanged()) return;
6519
6826
  if (!this.provider) {
6520
6827
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -6560,7 +6867,6 @@ exports.ALL_STEPS = ALL_STEPS;
6560
6867
  exports.DetectionPipelineProvider = DetectionPipelineProvider;
6561
6868
  exports.backendToFormat = backendToFormat;
6562
6869
  exports.default = DetectionPipelineAddon;
6563
- exports.filterDeviceOptionsByHardware = filterDeviceOptionsByHardware;
6564
6870
  exports.getDefaultModelForFormat = getDefaultModelForFormat;
6565
6871
  exports.getStepDefinition = getStepDefinition;
6566
6872
  exports.shouldInstallOpenvino = shouldInstallOpenvino;