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