@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
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { A as detectionPipelineCapability, E as createEvent, M as evaluateZoneRules, N as hfModelUrl, P as hydrateSchema, U as pipelineExecutorCapability, V as parseJsonUnknown, i as BaseAddon, j as errMsg, o as COCO_80_LABELS, p as EventCategory, q as sleep, r as AUDIO_MACRO_LABELS, s as COCO_TO_MACRO, t as APPLE_SA_TO_MACRO, v as YAMNET_TO_MACRO } from "../dist-CP2uP-D8.mjs";
2
+ import { A as detectionPipelineCapability, E as createEvent, M as evaluateZoneRules, N as hfModelUrl, P as hydrateSchema, U as pipelineExecutorCapability, V as parseJsonUnknown, i as BaseAddon, j as errMsg, o as COCO_80_LABELS, p as EventCategory, q as sleep, r as AUDIO_MACRO_LABELS, s as COCO_TO_MACRO, t as APPLE_SA_TO_MACRO, v as YAMNET_TO_MACRO } from "../dist-DsDFrG0I.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
5
5
  import * as os from "node:os";
@@ -3527,7 +3527,273 @@ function walkFieldsForDefaults(fields, out) {
3527
3527
  }
3528
3528
  }
3529
3529
  //#endregion
3530
+ //#region src/detection-pipeline/runtimes.ts
3531
+ var AUTO = {
3532
+ value: "auto",
3533
+ label: "Auto"
3534
+ };
3535
+ var CPU = {
3536
+ value: "cpu",
3537
+ label: "CPU"
3538
+ };
3539
+ var RUNTIMES = [
3540
+ {
3541
+ id: "onnx",
3542
+ label: "ONNX Runtime",
3543
+ supports: () => true,
3544
+ devices: (hw) => {
3545
+ if (!hw) return [CPU];
3546
+ const out = [CPU];
3547
+ if (hw.gpu?.type === "nvidia") out.push({
3548
+ value: "cuda",
3549
+ label: "CUDA"
3550
+ });
3551
+ if (hw.npu?.type === "apple-ane") out.push({
3552
+ value: "coreml",
3553
+ label: "CoreML EP"
3554
+ });
3555
+ return out;
3556
+ },
3557
+ defaultDevice: "cpu",
3558
+ modelFormat: "onnx",
3559
+ pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
3560
+ tuning: {
3561
+ concurrency: 4,
3562
+ batchMode: "list",
3563
+ maxBatchSize: 8,
3564
+ intraOpThreads: 0
3565
+ }
3566
+ },
3567
+ {
3568
+ id: "openvino",
3569
+ label: "OpenVINO",
3570
+ supports: (env) => env.arch === "x64" && env.platform !== "darwin" && env.hardware?.gpu?.type === "intel",
3571
+ devices: (hw) => {
3572
+ if (!hw) return [AUTO];
3573
+ const out = [AUTO, CPU];
3574
+ if (hw.gpu?.type === "intel") out.push({
3575
+ value: "gpu",
3576
+ label: "GPU"
3577
+ });
3578
+ if (hw.npu?.type === "intel-npu") out.push({
3579
+ value: "npu",
3580
+ label: "NPU"
3581
+ });
3582
+ return out;
3583
+ },
3584
+ defaultDevice: "auto",
3585
+ modelFormat: "openvino",
3586
+ pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
3587
+ tuning: {
3588
+ concurrency: 1,
3589
+ batchMode: "none",
3590
+ numStreams: 0
3591
+ }
3592
+ },
3593
+ {
3594
+ id: "coreml",
3595
+ label: "CoreML",
3596
+ supports: (env) => env.platform === "darwin",
3597
+ devices: (hw) => {
3598
+ const all = {
3599
+ value: "all",
3600
+ label: "All (ANE + GPU + CPU)"
3601
+ };
3602
+ if (!hw) return [all];
3603
+ const out = [all];
3604
+ if (hw.npu?.type === "apple-ane") out.push({
3605
+ value: "ane",
3606
+ label: "Apple Neural Engine"
3607
+ });
3608
+ out.push({
3609
+ value: "gpu",
3610
+ label: "GPU"
3611
+ }, CPU);
3612
+ return out;
3613
+ },
3614
+ defaultDevice: "all",
3615
+ modelFormat: "coreml",
3616
+ pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
3617
+ tuning: {
3618
+ concurrency: 1,
3619
+ batchMode: "none",
3620
+ windowMs: 8,
3621
+ maxBatchSize: 8,
3622
+ numWorkers: 1
3623
+ }
3624
+ }
3625
+ ];
3626
+ function def(id) {
3627
+ const d = RUNTIMES.find((r) => r.id === id);
3628
+ if (!d) throw new Error(`Unknown runtime: ${id}`);
3629
+ return d;
3630
+ }
3631
+ function supportedRuntimes(env) {
3632
+ return RUNTIMES.filter((r) => r.supports(env)).map((r) => r.id);
3633
+ }
3634
+ function runtimeDevices(id, hardware) {
3635
+ return def(id).devices(hardware);
3636
+ }
3637
+ function defaultDeviceFor(id) {
3638
+ return def(id).defaultDevice;
3639
+ }
3640
+ function pythonRequirementsFor(id) {
3641
+ return def(id).pythonRequirements;
3642
+ }
3643
+ function modelFormatFor(id) {
3644
+ return def(id).modelFormat;
3645
+ }
3646
+ function tuningFor(id) {
3647
+ return def(id).tuning;
3648
+ }
3649
+ function runtimeLabel(id) {
3650
+ return def(id).label;
3651
+ }
3652
+ /**
3653
+ * Proactive-install hint kept for back-compat (re-exported from index.ts).
3654
+ * Intel on Linux warrants installing openvino; coremltools covers macOS;
3655
+ * openvino has no AMD/NVIDIA backend.
3656
+ */
3657
+ function shouldInstallOpenvino(env) {
3658
+ if (env.platform === "darwin") return false;
3659
+ return env.gpu?.type === "intel" || env.npu?.type === "intel-npu";
3660
+ }
3661
+ //#endregion
3662
+ //#region src/detection-pipeline/engine-provisioner.ts
3663
+ /** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
3664
+ var BACKOFF_SCHEDULE_MS = [
3665
+ 5e3,
3666
+ 15e3,
3667
+ 3e4,
3668
+ 6e4,
3669
+ 12e4,
3670
+ 3e5
3671
+ ];
3672
+ var IDLE_STATE = {
3673
+ runtimeId: null,
3674
+ device: null,
3675
+ state: "idle"
3676
+ };
3677
+ var EngineProvisioner = class {
3678
+ fx;
3679
+ current = IDLE_STATE;
3680
+ /** Bumped on every select/dispose — stale async results (old generation) are ignored. */
3681
+ generation = 0;
3682
+ cancelTimer = null;
3683
+ retryIndex = 0;
3684
+ constructor(fx) {
3685
+ this.fx = fx;
3686
+ }
3687
+ get state() {
3688
+ return this.current;
3689
+ }
3690
+ isReady() {
3691
+ return this.current.state === "ready";
3692
+ }
3693
+ select(runtimeId, device) {
3694
+ this.generation++;
3695
+ this.retryIndex = 0;
3696
+ this.clearTimer();
3697
+ this.transition({
3698
+ runtimeId,
3699
+ device,
3700
+ state: "installing",
3701
+ progress: 0
3702
+ });
3703
+ this.provision(this.generation, runtimeId, device);
3704
+ }
3705
+ dispose() {
3706
+ this.generation++;
3707
+ this.clearTimer();
3708
+ }
3709
+ clearTimer() {
3710
+ if (this.cancelTimer !== null) {
3711
+ this.cancelTimer();
3712
+ this.cancelTimer = null;
3713
+ }
3714
+ }
3715
+ transition(next) {
3716
+ this.current = next;
3717
+ this.fx.onChange(next);
3718
+ }
3719
+ async provision(gen, runtimeId, device) {
3720
+ try {
3721
+ await Promise.all([this.fx.installRequirements(this.fx.requirementsFor(runtimeId)), this.fx.ensureModelForFormat(this.fx.modelFormatFor(runtimeId))]);
3722
+ if (gen !== this.generation) return;
3723
+ this.transition({
3724
+ runtimeId,
3725
+ device,
3726
+ state: "verifying",
3727
+ progress: 100
3728
+ });
3729
+ await this.fx.verify(runtimeId, device);
3730
+ if (gen !== this.generation) return;
3731
+ this.retryIndex = 0;
3732
+ this.transition({
3733
+ runtimeId,
3734
+ device,
3735
+ state: "ready"
3736
+ });
3737
+ } catch (err) {
3738
+ if (gen !== this.generation) return;
3739
+ const message = err instanceof Error ? err.message : String(err);
3740
+ const delay = BACKOFF_SCHEDULE_MS[Math.min(this.retryIndex, BACKOFF_SCHEDULE_MS.length - 1)];
3741
+ this.retryIndex++;
3742
+ const nextRetryAt = this.fx.now() + delay;
3743
+ this.transition({
3744
+ runtimeId,
3745
+ device,
3746
+ state: "failed",
3747
+ error: message,
3748
+ nextRetryAt
3749
+ });
3750
+ this.cancelTimer = this.fx.setTimer(delay, () => {
3751
+ if (gen !== this.generation) return;
3752
+ this.cancelTimer = null;
3753
+ this.transition({
3754
+ runtimeId,
3755
+ device,
3756
+ state: "installing",
3757
+ progress: 0
3758
+ });
3759
+ this.provision(gen, runtimeId, device);
3760
+ });
3761
+ }
3762
+ }
3763
+ };
3764
+ //#endregion
3765
+ //#region src/detection-pipeline/auto-pick.ts
3766
+ var PREFERENCE = [
3767
+ "coreml",
3768
+ "openvino",
3769
+ "onnx"
3770
+ ];
3771
+ /**
3772
+ * Pure function — picks the best supported runtime for the given hardware env.
3773
+ *
3774
+ * Logic:
3775
+ * 1. If `bestBackendHint` is in the supported set, use it.
3776
+ * 2. Otherwise, walk PREFERENCE order and pick the first supported runtime.
3777
+ * 3. Floor to `'onnx'` (always supported).
3778
+ *
3779
+ * Device is always `defaultDeviceFor(chosen)`.
3780
+ */
3781
+ function pickBestRuntime(env, bestBackendHint) {
3782
+ const supported = supportedRuntimes(env);
3783
+ const chosen = supported.find((id) => id === bestBackendHint) ?? PREFERENCE.find((id) => supported.includes(id)) ?? "onnx";
3784
+ return {
3785
+ runtimeId: chosen,
3786
+ device: defaultDeviceFor(chosen)
3787
+ };
3788
+ }
3789
+ //#endregion
3530
3790
  //#region src/detection-pipeline/provider.ts
3791
+ /**
3792
+ * DetectionPipelineProvider — implements IPipelineExecutorProvider.
3793
+ *
3794
+ * This is the main provider that consumers (DetectionWiring, Benchmark, tRPC)
3795
+ * interact with. It manages the engine factory, pipeline executor, and config persistence.
3796
+ */
3531
3797
  var KEY_STEPS = "pipelineSteps";
3532
3798
  var KEY_ENGINE = "pipelineEngine";
3533
3799
  var KEY_TEMPLATES = "pipelineTemplates";
@@ -3706,6 +3972,22 @@ function parseWavToAudioChunk(filePath) {
3706
3972
  function enginesEqual(a, b) {
3707
3973
  return a.runtime === b.runtime && a.backend === b.backend && a.format === b.format && (a.device ?? null) === (b.device ?? null);
3708
3974
  }
3975
+ /** Build a `RuntimeEnv` from the running process + probed hardware. */
3976
+ function runtimeEnvFromProcess(hardware) {
3977
+ return {
3978
+ platform: process.platform,
3979
+ arch: process.arch,
3980
+ hardware
3981
+ };
3982
+ }
3983
+ /** Normalize the loose probe hardware shape into a typed `ProbedHardware`. */
3984
+ function toProbedHardware(hw) {
3985
+ if (!hw) return null;
3986
+ return {
3987
+ npu: hw.npu ? { type: hw.npu.type } : null,
3988
+ gpu: hw.gpu ? { type: hw.gpu.type } : null
3989
+ };
3990
+ }
3709
3991
  var DetectionPipelineProvider = class DetectionPipelineProvider {
3710
3992
  modelsDir;
3711
3993
  eventBus;
@@ -3746,6 +4028,24 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3746
4028
  */
3747
4029
  ready = false;
3748
4030
  /**
4031
+ * Lazy detection-engine runtime provisioner (Phase 2). Owns the
4032
+ * idle → installing → verifying → ready state machine for the
4033
+ * currently-selected engine and is the SOLE authority the inference
4034
+ * gate (`assertEngineReady`) and the cap snapshot
4035
+ * (`getEngineProvisioning`) read from. Built lazily once the addon
4036
+ * context is wired (real effects need `ctx.deps` + the python dir),
4037
+ * so it is null on a freshly-constructed provider — in which case the
4038
+ * gate reports the cold `idle` state and refuses inference.
4039
+ */
4040
+ provisioner = null;
4041
+ /**
4042
+ * True when `init()` found no persisted engine choice (first boot).
4043
+ * `setApi()` reads this flag and auto-picks the best supported runtime
4044
+ * using probe data (hardware + bestScore hint), persists the selection,
4045
+ * then clears the flag.
4046
+ */
4047
+ needsAutoPick = false;
4048
+ /**
3749
4049
  * Warm cache for benchmark engine-override runs.
3750
4050
  *
3751
4051
  * Each override rebuild costs a full Python pool spin-up (~300-500ms)
@@ -3794,6 +4094,9 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3794
4094
  backend: stored.backend,
3795
4095
  format: stored.format
3796
4096
  } });
4097
+ } else {
4098
+ this.needsAutoPick = true;
4099
+ this.log.info("No persisted engine — auto-pick deferred to setApi()");
3797
4100
  }
3798
4101
  }
3799
4102
  /**
@@ -3830,6 +4133,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3830
4133
  * on a warmed pool.
3831
4134
  */
3832
4135
  async warmPool() {
4136
+ if (this.getEngineProvisioning().state !== "ready") {
4137
+ this.log.info("warmPool deferred — engine not provisioned yet", { meta: { state: this.getEngineProvisioning().state } });
4138
+ return;
4139
+ }
3833
4140
  await this.ensureEngineFactory();
3834
4141
  }
3835
4142
  /** True when the engine + model pool are fully warmed and inference-ready. */
@@ -3887,108 +4194,281 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
3887
4194
  /** Store the addon context. ctx.api is a lazy getter resolved at call time. */
3888
4195
  async setApi(addonCtx) {
3889
4196
  this.addonCtx = addonCtx;
4197
+ if (this.needsAutoPick) {
4198
+ await this.autoPickAndPersist();
4199
+ this.needsAutoPick = false;
4200
+ }
4201
+ this.startProvisioningForCurrentEngine();
4202
+ }
4203
+ /**
4204
+ * Auto-pick the best supported runtime at first boot (no stored engine).
4205
+ * Uses the platform-probe cap's hardware + bestScore hint when available;
4206
+ * falls back to platform/arch when the probe cap is not yet reachable.
4207
+ * Persists the selection as `engineBackend` + `engineDevice` so subsequent
4208
+ * boots load it via `loadEngine()` and skip this path.
4209
+ */
4210
+ async autoPickAndPersist() {
4211
+ let hardware = null;
4212
+ let bestBackendHint = null;
4213
+ try {
4214
+ const api = this.addonCtx?.api;
4215
+ if (api) {
4216
+ const caps = await api.platformProbe.getCapabilities.query();
4217
+ hardware = caps?.hardware ?? null;
4218
+ const bs = caps?.bestScore;
4219
+ if (bs && bs.runtime === "python") bestBackendHint = bs.backend;
4220
+ }
4221
+ } catch {}
4222
+ const pick = pickBestRuntime(runtimeEnvFromProcess(toProbedHardware(hardware)), bestBackendHint);
4223
+ const engine = {
4224
+ runtime: "python",
4225
+ backend: pick.runtimeId,
4226
+ format: modelFormatFor(pick.runtimeId),
4227
+ device: pick.device
4228
+ };
4229
+ this.currentEngine = engine;
4230
+ await this.writeStore({
4231
+ engineBackend: pick.runtimeId,
4232
+ engineDevice: pick.device
4233
+ });
4234
+ this.log.info("Auto-picked engine at first boot", { meta: {
4235
+ backend: pick.runtimeId,
4236
+ device: pick.device,
4237
+ hint: bestBackendHint ?? "none"
4238
+ } });
4239
+ }
4240
+ /** Map a backend string to a known RuntimeId, flooring to onnx. */
4241
+ toRuntimeId(backend) {
4242
+ return [
4243
+ "onnx",
4244
+ "openvino",
4245
+ "coreml"
4246
+ ].find((id) => id === backend) ?? "onnx";
4247
+ }
4248
+ /**
4249
+ * Build the {@link EngineProvisioner} with REAL effects. Built lazily
4250
+ * (effects need `addonCtx.deps` + the python dir), so the machine only
4251
+ * exists once the context is wired. The machine is the SOLE authority
4252
+ * the inference gate and the cap snapshot read.
4253
+ */
4254
+ buildProvisioner() {
4255
+ return new EngineProvisioner({
4256
+ requirementsFor: pythonRequirementsFor,
4257
+ modelFormatFor,
4258
+ installRequirements: async (files) => {
4259
+ const pythonAddonDir = this.executorOptions.pythonAddonDir;
4260
+ if (!pythonAddonDir || !this.addonCtx) return;
4261
+ for (const basename of files) {
4262
+ const file = path$1.join(pythonAddonDir, basename);
4263
+ if (!fs.existsSync(file)) continue;
4264
+ await this.addonCtx.deps.installPythonRequirements(file);
4265
+ }
4266
+ },
4267
+ ensureModelForFormat: async (format) => {
4268
+ await this.ensureModelsForCurrentSteps(format);
4269
+ },
4270
+ verify: async (backend, device) => {
4271
+ await this.verifyEngine(backend, device);
4272
+ },
4273
+ now: () => Date.now(),
4274
+ setTimer: (ms, cb) => {
4275
+ const t = setTimeout(cb, ms);
4276
+ return () => clearTimeout(t);
4277
+ },
4278
+ onChange: (state) => {
4279
+ this.log.info("engine provisioning", { meta: { ...state } });
4280
+ this.emitEngineProvisioning(state);
4281
+ }
4282
+ });
4283
+ }
4284
+ /**
4285
+ * (Re)build the provisioner and `select()` the engine currently held in
4286
+ * `this.currentEngine`. Idempotent: a fresh `select()` bumps the
4287
+ * machine's generation so any in-flight provisioning for a superseded
4288
+ * engine is ignored. No-op for non-python engines (nothing to install /
4289
+ * verify — the gate treats them as ready immediately below).
4290
+ */
4291
+ startProvisioningForCurrentEngine() {
4292
+ if (!this.provisioner) this.provisioner = this.buildProvisioner();
4293
+ const engine = this.currentEngine;
4294
+ const runtimeId = this.toRuntimeId(engine.backend);
4295
+ const device = engine.device ?? "cpu";
4296
+ const snapshot = this.provisioner.state;
4297
+ if (snapshot.runtimeId === runtimeId && snapshot.device === device && snapshot.state !== "idle") return;
4298
+ this.provisioner.select(runtimeId, device);
4299
+ }
4300
+ /**
4301
+ * Re-trigger provisioning after the operator changes the engine
4302
+ * cascade (`engineBackend`/`engineDevice`). Reloads the persisted
4303
+ * selection into `this.currentEngine`, then `select()`s it — the
4304
+ * machine cancels any superseded provisioning and drives the new
4305
+ * runtime through installing → verifying → ready.
4306
+ */
4307
+ async onEngineSelectionChanged() {
4308
+ const stored = await this.loadEngine();
4309
+ if (stored) this.currentEngine = stored;
4310
+ this.startProvisioningForCurrentEngine();
4311
+ }
4312
+ /**
4313
+ * Per-node engine-provisioning snapshot. The cap routes `{ nodeId }` for
4314
+ * provider resolution only (the router strips it); each node returns its
4315
+ * own local machine state. Cold (pre-`setApi`) provider → `idle`.
4316
+ */
4317
+ getEngineProvisioning() {
4318
+ return this.provisioner?.state ?? {
4319
+ runtimeId: null,
4320
+ device: null,
4321
+ state: "idle"
4322
+ };
4323
+ }
4324
+ /**
4325
+ * Inference gate. Rejects with the current phase/error unless the engine
4326
+ * provisioning machine is `ready`. Callers (`ensureExecutor`) surface the
4327
+ * state and skip inference for the cycle rather than falling back — the
4328
+ * cross-process frame caller (`pipeline-runner.runInference`) already
4329
+ * catches and drops the frame, so a throw here never crash-loops.
4330
+ */
4331
+ async assertEngineReady() {
4332
+ const s = this.getEngineProvisioning();
4333
+ if (s.state !== "ready") throw new Error(`Detection engine not ready: ${s.state}${s.error ? ` — ${s.error}` : ""}`);
4334
+ }
4335
+ /**
4336
+ * Download the model artifacts for `format` needed by the currently
4337
+ * configured steps. Reuses the existing model-download path
4338
+ * (`ensureModelsForSteps`/`downloadWithRetry`), specialised to the
4339
+ * provisioning format by temporarily projecting the current engine onto
4340
+ * `format` for the download fan-out.
4341
+ */
4342
+ async ensureModelsForCurrentSteps(format) {
4343
+ const steps = await this.getGlobalSteps();
4344
+ if (!steps || steps.length === 0) return;
4345
+ for (const step of flattenSteps(steps)) {
4346
+ if (!step.enabled) continue;
4347
+ const modelEntry = getStepDefinition(step.addonId).models.find((m) => m.id === step.modelId);
4348
+ if (!modelEntry) continue;
4349
+ if (isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
4350
+ await this.downloadWithRetry(modelEntry, format, 3);
4351
+ }
4352
+ }
4353
+ /**
4354
+ * 1-shot import + compile probe for `(backend, device)`. Spawns a
4355
+ * throwaway Python inference pool with EMPTY steps via the existing
4356
+ * `EngineFactory.initialize([])` path — this imports the selected
4357
+ * runtime (onnxruntime / openvino / coremltools) and throws on a
4358
+ * `ModuleNotFoundError` or a runtime-load/compile error — then disposes
4359
+ * the pool. No models are loaded; the probe only proves the runtime is
4360
+ * importable on this host. No-op (resolves) for non-python engines.
4361
+ */
4362
+ async verifyEngine(backend, device) {
4363
+ if (!this.executorOptions.pythonPath) throw new Error("verifyEngine: pythonPath not resolved — embedded Python unavailable");
4364
+ const probe = new EngineFactory({
4365
+ engine: {
4366
+ runtime: "python",
4367
+ backend,
4368
+ format: modelFormatFor(backend),
4369
+ device
4370
+ },
4371
+ modelsDir: this.modelsDir,
4372
+ logger: this.log.child("engine-verify"),
4373
+ pythonPath: this.executorOptions.pythonPath,
4374
+ concurrency: 1,
4375
+ numWorkers: 1
4376
+ });
4377
+ try {
4378
+ await probe.initialize([]);
4379
+ } finally {
4380
+ await probe.dispose().catch(() => void 0);
4381
+ }
4382
+ }
4383
+ /**
4384
+ * Emit the {@link EventCategory.PipelineEngineProvisioning} telemetry
4385
+ * event on every machine transition so the Pipeline page can drive a
4386
+ * live per-node indicator without polling the cap snapshot.
4387
+ */
4388
+ emitEngineProvisioning(state) {
4389
+ const eventBus = this.addonCtx?.eventBus ?? this.eventBus;
4390
+ if (!eventBus) return;
4391
+ const rawNodeId = this.addonCtx?.kernel?.localNodeId ?? "hub";
4392
+ const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
4393
+ eventBus.emit(createEvent(EventCategory.PipelineEngineProvisioning, {
4394
+ type: "node",
4395
+ id: nodeId,
4396
+ nodeId
4397
+ }, { ...state }));
3890
4398
  }
3891
4399
  /**
3892
- * Fetch platform-probe data for engine + device gating. Returns null for
3893
- * both fields when the probe cap is not yet reachable (cold-start / probe
3894
- * addon not installed), so callers fall back to the full static catalog.
4400
+ * Fetch the probed HARDWARE for engine + device gating. Returns null when the
4401
+ * probe cap is not yet reachable (cold-start / probe addon not installed), so
4402
+ * callers fall back to the registry's safe minimum. The engine OFFER derives
4403
+ * from hardware ONLY — install state (probe `scores`) never gates it.
3895
4404
  */
3896
4405
  async fetchProbeGatingData() {
3897
4406
  try {
3898
4407
  const api = this.addonCtx?.api;
3899
- if (!api) return {
3900
- availableBackends: null,
3901
- hardware: null
3902
- };
3903
- const caps = await api.platformProbe.getCapabilities.query();
3904
- if (!caps?.scores) return {
3905
- availableBackends: null,
3906
- hardware: null
3907
- };
3908
- return {
3909
- availableBackends: caps.scores.filter((s) => s.runtime === "python" && s.available).map((s) => s.backend),
3910
- hardware: caps.hardware ?? null
3911
- };
4408
+ if (!api) return { hardware: null };
4409
+ return { hardware: (await api.platformProbe.getCapabilities.query())?.hardware ?? null };
3912
4410
  } catch {
3913
- return {
3914
- availableBackends: null,
3915
- hardware: null
3916
- };
4411
+ return { hardware: null };
3917
4412
  }
3918
4413
  }
3919
4414
  async getSchema(engine) {
3920
4415
  if (!engine || !engine.runtime) engine = await this.getSelectedEngine();
3921
4416
  const format = engine.format;
3922
4417
  const slots = buildSchemaSlots(format, this.modelsDir);
3923
- const { availableBackends, hardware } = await this.fetchProbeGatingData();
4418
+ const { hardware } = await this.fetchProbeGatingData();
4419
+ const env = runtimeEnvFromProcess(toProbedHardware(hardware));
3924
4420
  return {
3925
- availableEngines: this.getAvailableEnginesWithDevices(availableBackends, hardware),
4421
+ availableEngines: this.getAvailableEnginesWithDevices(env).map((e) => this.toAvailableEngine(e)),
3926
4422
  selectedEngine: { ...engine },
3927
4423
  slots
3928
4424
  };
3929
4425
  }
3930
4426
  async getAvailableEngines() {
3931
- const { availableBackends, hardware } = await this.fetchProbeGatingData();
3932
- return this.getAvailableEnginesWithDevices(availableBackends, hardware).map((e) => e.engine);
4427
+ const { hardware } = await this.fetchProbeGatingData();
4428
+ const env = runtimeEnvFromProcess(toProbedHardware(hardware));
4429
+ return this.getAvailableEnginesWithDevices(env).map((e) => ({ ...e.engine }));
3933
4430
  }
3934
4431
  /**
3935
- * Build the engine catalog filtered by probe data.
3936
- *
3937
- * When `availableBackends` is not null, only engines whose backend is
3938
- * reported as available by the platform-probe cap are included. This
3939
- * prevents the benchmark picker from offering (e.g.) CoreML on an Intel
3940
- * Linux host where the Python coremltools package is absent.
3941
- *
3942
- * When `hardware` is not null, the per-engine device list is narrowed to
3943
- * entries that are valid on this host's hardware (no Intel NPU → no 'npu'
3944
- * device for OpenVINO, no NVIDIA GPU → no 'cuda' for ONNX, etc.).
4432
+ * Build the engine catalog from the HARDWARE-supported runtime set.
3945
4433
  *
3946
- * When either argument is null (probe unreachable / cold-start), the full
3947
- * static catalog is returned unchanged conservative fallback so the UI
3948
- * still renders all options rather than going blank.
4434
+ * The offered backend list comes from `supportedRuntimes(env)` platform,
4435
+ * arch and probed hardware ONLY, never from which Python packages are
4436
+ * installed (install state is provisioned on demand in Phase 2). Each
4437
+ * engine's device list is `runtimeDevices(backend, env.hardware)` (probe-
4438
+ * driven; default-only when `env.hardware` is null). The result is
4439
+ * conservative when the probe is unreachable: onnx floor + default device.
3949
4440
  */
3950
- getAvailableEnginesWithDevices(availableBackends, hardware) {
3951
- const allEngines = [];
3952
- if (process.platform === "darwin") allEngines.push({
4441
+ getAvailableEnginesWithDevices(env) {
4442
+ return supportedRuntimes(env).map((id) => ({
3953
4443
  engine: {
3954
4444
  runtime: "python",
3955
- backend: "coreml",
3956
- format: "coreml"
4445
+ backend: id,
4446
+ format: modelFormatFor(id),
4447
+ device: defaultDeviceFor(id)
3957
4448
  },
3958
- devices: COREML_DEVICES,
3959
- defaultDevice: "all"
3960
- });
3961
- allEngines.push({
3962
- engine: {
3963
- runtime: "python",
3964
- backend: "openvino",
3965
- format: "openvino"
3966
- },
3967
- devices: OPENVINO_DEVICES,
3968
- defaultDevice: "auto"
3969
- });
3970
- allEngines.push({
4449
+ devices: runtimeDevices(id, env.hardware).map((d) => ({
4450
+ id: d.value,
4451
+ label: d.label
4452
+ }))
4453
+ }));
4454
+ }
4455
+ /**
4456
+ * Adapt an {@link EngineWithDevices} (registry shape) to the wire
4457
+ * {@link AvailableEngine} consumed by `PipelineSchema.availableEngines`.
4458
+ */
4459
+ toAvailableEngine(e) {
4460
+ return {
3971
4461
  engine: {
3972
- runtime: "python",
3973
- backend: "onnx",
3974
- format: "onnx"
4462
+ runtime: e.engine.runtime,
4463
+ backend: e.engine.backend,
4464
+ format: e.engine.format
3975
4465
  },
3976
- devices: ONNX_PYTHON_DEVICES,
3977
- defaultDevice: "cpu"
3978
- });
3979
- if (!availableBackends) return allEngines;
3980
- return allEngines.filter((e) => availableBackends.includes(e.engine.backend)).map((e) => {
3981
- if (!hardware) return e;
3982
- const filtered = filterDeviceOptionsByHardware(e.devices.map((d) => ({
3983
- value: d.id,
4466
+ devices: e.devices.map((d) => ({
4467
+ id: d.id,
3984
4468
  label: d.label
3985
- })), e.engine.backend, hardware);
3986
- const allowedIds = new Set(filtered.map((o) => o.value));
3987
- return {
3988
- ...e,
3989
- devices: e.devices.filter((d) => allowedIds.has(d.id))
3990
- };
3991
- });
4469
+ })),
4470
+ defaultDevice: e.engine.device
4471
+ };
3992
4472
  }
3993
4473
  async getDefaultSteps(engine) {
3994
4474
  return buildDefaultStepTree(engine.format);
@@ -4080,21 +4560,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4080
4560
  };
4081
4561
  })
4082
4562
  }));
4083
- const { availableBackends, hardware } = await this.fetchProbeGatingData();
4084
- const engines = this.getAvailableEnginesWithDevices(availableBackends, hardware);
4563
+ const { hardware } = await this.fetchProbeGatingData();
4564
+ const env = runtimeEnvFromProcess(toProbedHardware(hardware));
4565
+ const engines = this.getAvailableEnginesWithDevices(env);
4085
4566
  const nodeBackends = [];
4086
4567
  const pythonBackends = [];
4087
- for (const e of engines) if (e.engine.runtime === "node") nodeBackends.push({
4568
+ for (const e of engines) pythonBackends.push({
4088
4569
  id: e.engine.backend,
4089
4570
  label: e.engine.backend.toUpperCase(),
4090
4571
  available: true,
4091
- device: e.defaultDevice ?? e.devices[0]?.id ?? "cpu"
4092
- });
4093
- else pythonBackends.push({
4094
- id: e.engine.backend,
4095
- label: e.engine.backend.toUpperCase(),
4096
- available: true,
4097
- device: e.defaultDevice ?? e.devices[0]?.id ?? "cpu",
4572
+ device: e.engine.device || e.devices[0]?.id || "cpu",
4098
4573
  modelFormat: e.engine.format,
4099
4574
  pythonModule: pythonModuleForBackend(e.engine.backend)
4100
4575
  });
@@ -5101,6 +5576,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5101
5576
  async ensureEngineFactory() {
5102
5577
  if (this.initPromise) await this.initPromise;
5103
5578
  if (this.engineFactory) return;
5579
+ await this.assertEngineReady();
5104
5580
  await this.ensureBackendDeps(this.currentEngine);
5105
5581
  this.engineFactory = new EngineFactory({
5106
5582
  engine: this.currentEngine,
@@ -5116,6 +5592,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5116
5592
  async ensureExecutor() {
5117
5593
  if (this.initPromise) await this.initPromise;
5118
5594
  if (!this.engineFactory) {
5595
+ await this.assertEngineReady();
5119
5596
  this.initPromise = this.doInitialize();
5120
5597
  try {
5121
5598
  await this.initPromise;
@@ -5714,70 +6191,45 @@ function applyDeviceOverridesToTree(tree, rootStepId, overrides) {
5714
6191
  }
5715
6192
  } : root) };
5716
6193
  }
5717
- var COREML_DEVICES = [
5718
- {
5719
- id: "all",
5720
- label: "All (CPU + GPU + Neural Engine)",
5721
- description: "CoreML auto-selects the best available compute unit"
5722
- },
5723
- {
5724
- id: "ane",
5725
- label: "Neural Engine",
5726
- description: "Apple Neural Engine — fastest, lowest power"
5727
- },
5728
- {
5729
- id: "gpu",
5730
- label: "GPU",
5731
- description: "Apple GPU — good for larger models"
5732
- },
5733
- {
5734
- id: "cpu",
5735
- label: "CPU only",
5736
- description: "CPU fallback — slowest but always available"
5737
- }
5738
- ];
5739
- var OPENVINO_DEVICES = [
5740
- {
5741
- id: "auto",
5742
- label: "Auto",
5743
- description: "OpenVINO auto-selects best device (recommended)"
5744
- },
5745
- {
5746
- id: "cpu",
5747
- label: "CPU",
5748
- description: "Intel CPU with AVX/SSE optimizations"
5749
- },
5750
- {
5751
- id: "gpu",
5752
- label: "GPU (Intel)",
5753
- description: "Intel integrated/discrete GPU via OpenCL"
5754
- },
5755
- {
5756
- id: "npu",
5757
- label: "NPU (Intel)",
5758
- description: "Intel Neural Processing Unit (Meteor Lake+)"
5759
- }
5760
- ];
5761
- var ONNX_PYTHON_DEVICES = [
5762
- {
5763
- id: "cpu",
5764
- label: "CPU",
5765
- description: "ONNX Runtime CPU provider"
5766
- },
5767
- {
5768
- id: "cuda",
5769
- label: "CUDA (NVIDIA)",
5770
- description: "NVIDIA GPU via CUDA"
5771
- },
5772
- {
5773
- id: "coreml",
5774
- label: "CoreML",
5775
- description: "Apple CoreML execution provider (macOS)"
6194
+ //#endregion
6195
+ //#region src/detection-pipeline/python-readiness.ts
6196
+ /**
6197
+ * Resolve the embedded Python once, turning the null/throw failure modes into
6198
+ * an explicit, logged result. Python is a hard prerequisite for every runtime;
6199
+ * a silent miss here is what produced the "probe empty → bad offer" symptom.
6200
+ */
6201
+ async function ensurePythonReady(deps, log) {
6202
+ try {
6203
+ const pythonPath = await deps.ensurePython();
6204
+ if (!pythonPath) {
6205
+ log.error("Embedded Python unavailable — inference runtimes cannot be provisioned", {});
6206
+ return {
6207
+ pythonPath: null,
6208
+ ok: false
6209
+ };
6210
+ }
6211
+ log.info("Embedded Python ready", { meta: { pythonPath } });
6212
+ return {
6213
+ pythonPath,
6214
+ ok: true
6215
+ };
6216
+ } catch (err) {
6217
+ log.error("Embedded Python provisioning threw", { meta: { error: err instanceof Error ? err.message : String(err) } });
6218
+ return {
6219
+ pythonPath: null,
6220
+ ok: false
6221
+ };
5776
6222
  }
5777
- ];
6223
+ }
5778
6224
  //#endregion
5779
6225
  //#region src/detection-pipeline/index.ts
5780
6226
  /**
6227
+ * addon-detection-pipeline — unified detection pipeline for CamStack.
6228
+ *
6229
+ * Single addon replaces 13 separate vision addon classes.
6230
+ * Each pipeline step is a class implementing IPipelineStep.
6231
+ */
6232
+ /**
5781
6233
  * Locate the addon's bundled `python/` dir at runtime. Mirrors the
5782
6234
  * resolution done in `shared-inference-pool.ts:resolveScriptPath` —
5783
6235
  * tries the published package first (resolves through node_modules),
@@ -5796,151 +6248,6 @@ function resolveAddonPythonDir() {
5796
6248
  for (const c of candidates) if (fs.existsSync(path$1.join(c, "inference_pool.py"))) return c;
5797
6249
  throw new Error(`addon-pipeline/detection-pipeline: python/ dir not found. Searched:\n${candidates.join("\n")}`);
5798
6250
  }
5799
- /**
5800
- * Returns true when proactive OpenVINO installation is warranted.
5801
- *
5802
- * Gate: Linux host + Intel iGPU or Intel NPU detected.
5803
- *
5804
- * Intentionally addon-local — addons are self-contained and cannot import
5805
- * `@camstack/system` internals (see architecture invariant: no cross-addon
5806
- * imports). This mirrors the logic in `resolveRuntimePackages` from that
5807
- * package without importing it.
5808
- *
5809
- * darwin is never true: coremltools handles Apple Silicon + Intel Mac.
5810
- * linux-amd (or any non-Intel linux GPU) is never true: the openvino
5811
- * package has no AMD backend and would fail at import time.
5812
- *
5813
- * @internal exported only for unit tests in the same package
5814
- */
5815
- function shouldInstallOpenvino(hardware) {
5816
- if (hardware.platform !== "linux") return false;
5817
- const hasIntelGpu = hardware.gpu?.type === "intel";
5818
- const hasIntelNpu = hardware.npu?.type === "intel-npu";
5819
- return hasIntelGpu || hasIntelNpu;
5820
- }
5821
- /**
5822
- * Full catalog of execution providers. The settings UI only shows the subset
5823
- * reported as available by the platform-probe cap (`getGlobalSettings` gates
5824
- * options by probe scores). Kept as the static universe so the static schema
5825
- * (returned before a live ctx is available) still lists all fields.
5826
- */
5827
- var BACKENDS_BY_RUNTIME = { python: [
5828
- {
5829
- value: "coreml",
5830
- label: "CoreML"
5831
- },
5832
- {
5833
- value: "openvino",
5834
- label: "OpenVINO"
5835
- },
5836
- {
5837
- value: "onnx",
5838
- label: "ONNX Runtime"
5839
- }
5840
- ] };
5841
- var DEVICES_BY_BACKEND = {
5842
- coreml: [
5843
- {
5844
- value: "all",
5845
- label: "All (ANE + GPU + CPU)"
5846
- },
5847
- {
5848
- value: "ane",
5849
- label: "Apple Neural Engine"
5850
- },
5851
- {
5852
- value: "gpu",
5853
- label: "GPU"
5854
- },
5855
- {
5856
- value: "cpu",
5857
- label: "CPU"
5858
- }
5859
- ],
5860
- openvino: [
5861
- {
5862
- value: "auto",
5863
- label: "Auto"
5864
- },
5865
- {
5866
- value: "cpu",
5867
- label: "CPU"
5868
- },
5869
- {
5870
- value: "gpu",
5871
- label: "GPU"
5872
- },
5873
- {
5874
- value: "npu",
5875
- label: "NPU"
5876
- }
5877
- ],
5878
- onnx: [
5879
- {
5880
- value: "cpu",
5881
- label: "CPU"
5882
- },
5883
- {
5884
- value: "cuda",
5885
- label: "CUDA"
5886
- },
5887
- {
5888
- value: "coreml",
5889
- label: "CoreML EP"
5890
- }
5891
- ],
5892
- cpu: [{
5893
- value: "cpu",
5894
- label: "CPU"
5895
- }]
5896
- };
5897
- /**
5898
- * Filter the per-backend device option list by what the platform probe
5899
- * reports as available hardware on this host. Rules (derived from the
5900
- * confirmed target model):
5901
- *
5902
- * coreml:
5903
- * - `ane` only when `hardware.npu?.type === 'apple-ane'`
5904
- * - `gpu` and `all` always shown (CPU+GPU present on every Mac)
5905
- * - `cpu` always shown
5906
- *
5907
- * openvino:
5908
- * - `npu` only when `hardware.npu?.type === 'intel-npu'`
5909
- * - `gpu` only when `hardware.gpu?.type === 'intel'`
5910
- * - `auto` and `cpu` always shown
5911
- *
5912
- * onnx:
5913
- * - `cuda` only when `hardware.gpu?.type === 'nvidia'`
5914
- * - `coreml` only when `hardware.npu?.type === 'apple-ane'` (CoreML EP)
5915
- * - `cpu` always shown
5916
- *
5917
- * When `hardware` is null (probe unreachable), the full catalog is returned
5918
- * unchanged so the UI still renders all options.
5919
- */
5920
- function filterDeviceOptionsByHardware(options, backend, hardware) {
5921
- if (!hardware) return options;
5922
- const hasAppleAne = hardware.npu?.type === "apple-ane";
5923
- const hasIntelNpu = hardware.npu?.type === "intel-npu";
5924
- const hasIntelGpu = hardware.gpu?.type === "intel";
5925
- const hasNvidiaGpu = hardware.gpu?.type === "nvidia";
5926
- switch (backend) {
5927
- case "coreml": return options.filter((o) => {
5928
- if (o.value === "ane") return hasAppleAne;
5929
- return true;
5930
- });
5931
- case "openvino": return options.filter((o) => {
5932
- if (o.value === "npu") return hasIntelNpu;
5933
- if (o.value === "gpu") return hasIntelGpu;
5934
- return true;
5935
- });
5936
- case "onnx": return options.filter((o) => {
5937
- if (o.value === "cuda") return hasNvidiaGpu;
5938
- if (o.value === "coreml") return hasAppleAne;
5939
- return true;
5940
- });
5941
- default: return options;
5942
- }
5943
- }
5944
6251
  var BACKEND_TO_FORMAT = {
5945
6252
  cpu: "onnx",
5946
6253
  coreml: "coreml",
@@ -5948,37 +6255,38 @@ var BACKEND_TO_FORMAT = {
5948
6255
  onnx: "onnx"
5949
6256
  };
5950
6257
  /**
5951
- * Per-backend tuning defaults derived from the matrix sweep
5952
- * (`scripts/bench-pool-matrix.py` + `bench-coreml-patterns.py`).
5953
- * Applied by `getGlobalSettings(overlay)` so the UI defaults track
5954
- * the currently-selected backend instead of a single global value.
6258
+ * Every runtime the registry knows about, in offer order. Used only to build
6259
+ * the STATIC base schema (returned by `globalSettingsSchema()` before a live
6260
+ * ctx exists, e.g. addon-registry boot introspection). The live, hardware-
6261
+ * gated subset is injected by `getGlobalSettings()` via `supportedRuntimes(env)`.
5955
6262
  */
5956
- var TUNING_DEFAULTS = {
5957
- coreml: {
5958
- concurrency: 1,
5959
- batchMode: "none",
5960
- windowMs: 8,
5961
- maxBatchSize: 8,
5962
- numWorkers: 1
5963
- },
5964
- openvino: {
5965
- concurrency: 1,
5966
- batchMode: "none",
5967
- numStreams: 0
5968
- },
5969
- onnx: {
5970
- concurrency: 4,
5971
- batchMode: "list",
5972
- maxBatchSize: 8,
5973
- intraOpThreads: 0
5974
- },
5975
- cpu: {
5976
- concurrency: 4,
5977
- batchMode: "list",
5978
- maxBatchSize: 8,
5979
- intraOpThreads: 0
5980
- }
5981
- };
6263
+ var ALL_RUNTIME_IDS = [
6264
+ "onnx",
6265
+ "openvino",
6266
+ "coreml"
6267
+ ];
6268
+ /**
6269
+ * Narrow an arbitrary stored backend string to a known `RuntimeId`. Legacy or
6270
+ * unknown values (e.g. the removed `'cpu'` backend) fall back to the `onnx`
6271
+ * floor so the registry lookup never throws.
6272
+ */
6273
+ function toRuntimeId(backend) {
6274
+ return ALL_RUNTIME_IDS.find((id) => id === backend) ?? "onnx";
6275
+ }
6276
+ /** Static universe of backend options (every runtime). Labels from the registry. */
6277
+ var STATIC_BACKEND_OPTIONS = ALL_RUNTIME_IDS.map((id) => ({
6278
+ value: id,
6279
+ label: runtimeLabel(id)
6280
+ }));
6281
+ /**
6282
+ * Static device options for the schema's default backend. The live device
6283
+ * list per the selected backend + probed hardware is injected by
6284
+ * `getGlobalSettings()` via `runtimeDevices(backend, hardware)`.
6285
+ */
6286
+ var STATIC_DEFAULT_DEVICE_OPTIONS = runtimeDevices(ALL_RUNTIME_IDS[0], null).map((d) => ({
6287
+ value: d.value,
6288
+ label: d.label
6289
+ }));
5982
6290
  var DEFAULT_CONFIG = {
5983
6291
  concurrency: 0,
5984
6292
  batchMode: "",
@@ -5988,8 +6296,8 @@ var DEFAULT_CONFIG = {
5988
6296
  intraOpThreads: 0,
5989
6297
  numWorkers: 0,
5990
6298
  engineRuntime: "python",
5991
- engineBackend: "coreml",
5992
- engineDevice: "all",
6299
+ engineBackend: "onnx",
6300
+ engineDevice: "cpu",
5993
6301
  probedBestEngine: ""
5994
6302
  };
5995
6303
  /** Derive the model-format from a backend value. Called by the provider. */
@@ -6091,7 +6399,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6091
6399
  type: "select",
6092
6400
  key: "engineBackend",
6093
6401
  label: "Execution provider",
6094
- options: [...BACKENDS_BY_RUNTIME.python],
6402
+ options: [...STATIC_BACKEND_OPTIONS],
6095
6403
  default: DEFAULT_CONFIG.engineBackend,
6096
6404
  immediate: true,
6097
6405
  requiresRestart: true
@@ -6100,7 +6408,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6100
6408
  type: "select",
6101
6409
  key: "engineDevice",
6102
6410
  label: "Hardware device",
6103
- options: [...DEVICES_BY_BACKEND.coreml],
6411
+ options: [...STATIC_DEFAULT_DEVICE_OPTIONS],
6104
6412
  default: DEFAULT_CONFIG.engineDevice,
6105
6413
  immediate: true,
6106
6414
  requiresRestart: true
@@ -6213,12 +6521,15 @@ var DetectionPipelineAddon = class extends BaseAddon {
6213
6521
  }] });
6214
6522
  }
6215
6523
  /**
6216
- * Override to inject dynamic backend + device options based on the
6217
- * currently-stored runtime + backend. The base schema ships with
6218
- * placeholder lists; here we replace them with the valid subset for
6219
- * whatever the operator has picked. Combined with `immediate: true`
6220
- * on the selects, the UI can refetch after each change and see the
6221
- * next level's options narrow down.
6524
+ * Override to inject the hardware-driven backend + device options.
6525
+ *
6526
+ * The OFFERED backend list comes from `supportedRuntimes(env)` platform,
6527
+ * arch and probed hardware ONLY, never from which Python packages are
6528
+ * installed. A host's hardware is what it can physically run; install state
6529
+ * is provisioned on demand (Phase 2). Device options come from
6530
+ * `runtimeDevices(backend, hardware)` (probe-driven; default-only when the
6531
+ * probe is unreachable). Stored backend / device snap back to the registry
6532
+ * floor / default when they fall outside the offered set.
6222
6533
  */
6223
6534
  async getGlobalSettings(overlay) {
6224
6535
  const ctx = this.ctxIfReady;
@@ -6227,19 +6538,18 @@ var DetectionPipelineAddon = class extends BaseAddon {
6227
6538
  ...stored,
6228
6539
  ...overlay
6229
6540
  } : stored;
6230
- const runtime = "python";
6231
- const probeResult = await this.resolveProbeData(ctx, runtime);
6232
- const availableBackends = probeResult.availableBackends;
6233
- const hardware = probeResult.hardware;
6234
- const runtimeBackends = availableBackends.length > 0 ? BACKENDS_BY_RUNTIME[runtime].filter((b) => availableBackends.includes(b.value)) : BACKENDS_BY_RUNTIME[runtime];
6541
+ const env = await this.probeHardwareEnv();
6542
+ const hardware = env.hardware;
6543
+ const offered = supportedRuntimes(env);
6544
+ const runtimeBackends = offered.map((id) => ({
6545
+ value: id,
6546
+ label: runtimeLabel(id)
6547
+ }));
6235
6548
  const storedBackend = typeof merged.engineBackend === "string" ? merged.engineBackend : "";
6236
- const backend = runtimeBackends.find((b) => b.value === storedBackend)?.value ?? probeResult.bestBackend ?? runtimeBackends[0]?.value ?? "coreml";
6237
- const deviceOptions = filterDeviceOptionsByHardware(DEVICES_BY_BACKEND[backend] ?? [{
6238
- value: "cpu",
6239
- label: "CPU"
6240
- }], backend, hardware);
6549
+ const backend = offered.find((id) => id === storedBackend) ?? offered[0] ?? "onnx";
6550
+ const deviceOptions = runtimeDevices(backend, hardware);
6241
6551
  const storedDevice = typeof merged.engineDevice === "string" ? merged.engineDevice : "";
6242
- const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? deviceOptions[0]?.value ?? "cpu";
6552
+ const device = deviceOptions.find((d) => d.value === storedDevice)?.value ?? defaultDeviceFor(backend);
6243
6553
  const raw = {
6244
6554
  ...merged,
6245
6555
  engineBackend: backend,
@@ -6247,7 +6557,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6247
6557
  };
6248
6558
  const schema = this.globalSettingsSchema();
6249
6559
  if (!schema) return { sections: [] };
6250
- const tuning = TUNING_DEFAULTS[backend] ?? {};
6560
+ const tuning = tuningFor(backend);
6251
6561
  return hydrateSchema({
6252
6562
  ...schema,
6253
6563
  sections: schema.sections.map((section) => ({
@@ -6260,12 +6570,15 @@ var DetectionPipelineAddon = class extends BaseAddon {
6260
6570
  };
6261
6571
  if (field.key === "engineDevice") return {
6262
6572
  ...field,
6263
- options: [...deviceOptions],
6264
- default: deviceOptions[0]?.value ?? "cpu"
6573
+ options: deviceOptions.map((d) => ({
6574
+ value: d.value,
6575
+ label: d.label
6576
+ })),
6577
+ default: defaultDeviceFor(backend)
6265
6578
  };
6266
- if (field.key === "batchMode" && tuning.batchMode !== void 0) return {
6579
+ if (field.key === "batchMode" && tuning["batchMode"] !== void 0) return {
6267
6580
  ...field,
6268
- default: tuning.batchMode
6581
+ default: tuning["batchMode"]
6269
6582
  };
6270
6583
  }
6271
6584
  if (field.type === "slider" && "key" in field) {
@@ -6287,55 +6600,47 @@ var DetectionPipelineAddon = class extends BaseAddon {
6287
6600
  }, raw);
6288
6601
  }
6289
6602
  /**
6290
- * Fetch the platform-probe capabilities and return:
6291
- * - `availableBackends`: backends the probe reports as available for `runtime`
6292
- * (empty caller falls back to full catalog).
6293
- * - `hardware`: the probed hardware info (null when probe not reachable).
6294
- * - `bestBackend`: the backend from the probe's `bestScore` (null when unavailable).
6603
+ * Resolve the runtime env (platform + arch + probed hardware) used to derive
6604
+ * the OFFERED backend list. The hardware comes from the platform-probe cap
6605
+ * (`getCapabilities`); when the cap isn't reachable yet (cold-start / probe
6606
+ * addon not installed) `hardware` is null, which the registry collapses to
6607
+ * the safe minimum (onnx floor; default-only devices).
6295
6608
  *
6296
- * A single probe call is made so both backend AND device gating use the
6297
- * same snapshot without doubling the cap round-trip.
6609
+ * Protected seam overridable by tests (canned hardware) and reused by the
6610
+ * Phase 2 auto-pick path. NEVER reads install state.
6611
+ */
6612
+ async probeHardwareEnv() {
6613
+ const hardware = await this.resolveProbeHardware();
6614
+ return {
6615
+ platform: process.platform,
6616
+ arch: process.arch,
6617
+ hardware
6618
+ };
6619
+ }
6620
+ /**
6621
+ * Fetch the probed hardware from the platform-probe cap. Returns null when
6622
+ * the cap is not reachable (caller falls back to the registry's safe minimum).
6298
6623
  */
6299
- async resolveProbeData(ctx, runtime) {
6624
+ async resolveProbeHardware() {
6300
6625
  try {
6301
- const api = ctx?.api;
6302
- if (!api) return {
6303
- availableBackends: [],
6304
- hardware: null,
6305
- bestBackend: null
6306
- };
6307
- const caps = await api.platformProbe.getCapabilities.query();
6308
- if (!caps?.scores) return {
6309
- availableBackends: [],
6310
- hardware: null,
6311
- bestBackend: null
6312
- };
6313
- const out = /* @__PURE__ */ new Set();
6314
- for (const s of caps.scores) {
6315
- if (s.runtime !== runtime) continue;
6316
- if (!s.available) continue;
6317
- out.add(s.backend);
6318
- }
6319
- const bestBackend = caps.bestScore?.runtime === runtime ? caps.bestScore.backend ?? null : null;
6626
+ const api = this.ctxIfReady?.api;
6627
+ if (!api) return null;
6628
+ const hw = (await api.platformProbe.getCapabilities.query())?.hardware;
6629
+ if (!hw) return null;
6320
6630
  return {
6321
- availableBackends: [...out],
6322
- hardware: caps.hardware ?? null,
6323
- bestBackend
6631
+ npu: hw.npu ? { type: hw.npu.type } : null,
6632
+ gpu: hw.gpu ? { type: hw.gpu.type } : null
6324
6633
  };
6325
6634
  } catch {
6326
- return {
6327
- availableBackends: [],
6328
- hardware: null,
6329
- bestBackend: null
6330
- };
6635
+ return null;
6331
6636
  }
6332
6637
  }
6333
6638
  /**
6334
6639
  * Resolve the effective pool tuning for the configured backend.
6335
6640
  *
6336
- * Reads `TUNING_DEFAULTS[backend]` and ignores any persisted override
6337
- * for `concurrency / batchMode / windowMs / maxBatchSize / numStreams /
6338
- * intraOpThreads`. Stored values are quietly discarded so an old
6641
+ * Reads the registry's `tuningFor(backend)` and ignores any persisted
6642
+ * override for `concurrency / batchMode / windowMs / maxBatchSize /
6643
+ * numStreams / intraOpThreads`. Stored values are quietly discarded so an old
6339
6644
  * suboptimal user-override (saved when the UI exposed these knobs)
6340
6645
  * cannot resurrect itself after a restart.
6341
6646
  *
@@ -6344,18 +6649,17 @@ var DetectionPipelineAddon = class extends BaseAddon {
6344
6649
  * a reason to disagree.
6345
6650
  */
6346
6651
  resolveBackendTuning() {
6347
- const t = TUNING_DEFAULTS[this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend] ?? TUNING_DEFAULTS["onnx"];
6348
- const tAny = t;
6652
+ const t = tuningFor(toRuntimeId(this.config.engineBackend ?? DEFAULT_CONFIG.engineBackend));
6349
6653
  const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
6350
- const str = (v, dflt) => typeof v === "string" && v !== "" ? v : dflt;
6654
+ const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
6351
6655
  return {
6352
- concurrency: num(t.concurrency, 1),
6353
- batchMode: str(t.batchMode, "window"),
6354
- windowMs: num(t.windowMs, 2),
6355
- maxBatchSize: num(t.maxBatchSize, 8),
6356
- numStreams: num(t.numStreams, 0),
6357
- intraOpThreads: num(t.intraOpThreads, 0),
6358
- numWorkers: num(tAny["numWorkers"], 1)
6656
+ concurrency: num(t["concurrency"], 1),
6657
+ batchMode: batch(t["batchMode"], "window"),
6658
+ windowMs: num(t["windowMs"], 2),
6659
+ maxBatchSize: num(t["maxBatchSize"], 8),
6660
+ numStreams: num(t["numStreams"], 0),
6661
+ intraOpThreads: num(t["intraOpThreads"], 0),
6662
+ numWorkers: num(t["numWorkers"], 1)
6359
6663
  };
6360
6664
  }
6361
6665
  async onInitialize() {
@@ -6365,9 +6669,9 @@ var DetectionPipelineAddon = class extends BaseAddon {
6365
6669
  }).catch(() => "camstack-data/models");
6366
6670
  if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
6367
6671
  this.pythonAddonDir = resolveAddonPythonDir();
6368
- const pythonPath = await this.ctx.deps.ensurePython();
6369
- if (pythonPath) this.pythonPath = pythonPath;
6370
- else this.ctx.logger.warn("Embedded Python unavailable runtime=\"python\" pipelines will fail until the download succeeds.");
6672
+ const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
6673
+ if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
6674
+ else this.ctx.logger.warn("Detection engine boot continues without Python selection will provision on demand", {});
6371
6675
  await this.proactivelyInstallOpenvino();
6372
6676
  const effectiveTuning = this.resolveBackendTuning();
6373
6677
  this.provider = new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
@@ -6474,7 +6778,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
6474
6778
  * Snapshot the pool-bound subset of the EFFECTIVE tuning (post-
6475
6779
  * `resolveBackendTuning`). Stored config values for these fields are
6476
6780
  * ignored, so this snapshot only changes when `engineBackend` flips
6477
- * onto a different `TUNING_DEFAULTS` row.
6781
+ * onto a different `tuningFor` row.
6478
6782
  */
6479
6783
  snapshotPoolConfig() {
6480
6784
  const t = this.resolveBackendTuning();
@@ -6507,6 +6811,9 @@ var DetectionPipelineAddon = class extends BaseAddon {
6507
6811
  * lifecycle (engineFactory rebuild on next runPipeline).
6508
6812
  */
6509
6813
  async onConfigChanged() {
6814
+ if (this.provider) await this.provider.onEngineSelectionChanged().catch((err) => {
6815
+ this.ctx.logger.warn("engine provisioning re-select failed on config change", { meta: { error: err instanceof Error ? err.message : String(err) } });
6816
+ });
6510
6817
  if (!this.poolConfigChanged()) return;
6511
6818
  if (!this.provider) {
6512
6819
  this.lastAppliedPoolConfig = this.snapshotPoolConfig();
@@ -6548,4 +6855,4 @@ var DetectionPipelineAddon = class extends BaseAddon {
6548
6855
  }
6549
6856
  };
6550
6857
  //#endregion
6551
- export { ALL_STEPS, DetectionPipelineProvider, backendToFormat, DetectionPipelineAddon as default, filterDeviceOptionsByHardware, getDefaultModelForFormat, getStepDefinition, shouldInstallOpenvino };
6858
+ export { ALL_STEPS, DetectionPipelineProvider, backendToFormat, DetectionPipelineAddon as default, getDefaultModelForFormat, getStepDefinition, shouldInstallOpenvino };