@camstack/addon-pipeline 1.1.25 → 1.1.26
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 +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/audio-codec-ffmpeg/index.js +1 -1
- package/dist/audio-codec-ffmpeg/index.mjs +1 -1
- package/dist/decoder-ffmpeg/index.js +1 -1
- package/dist/decoder-ffmpeg/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +148 -26
- package/dist/detection-pipeline/index.mjs +148 -26
- package/dist/{dist-BecXbIzJ.mjs → dist-CgEP_0OL.mjs} +730 -14
- package/dist/{dist-wzWBZ26C.js → dist-DAIlCdAx.js} +730 -14
- package/dist/{frame-handle-plane-DGg0Aevs.mjs → frame-handle-plane-Dq20KtKL.mjs} +1 -1
- package/dist/{frame-handle-plane-BAKiW6t4.js → frame-handle-plane-DtTRX_0n.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +38 -9
- package/dist/pipeline-runner/index.mjs +38 -9
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWPU9tbs.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5tQlh9h4.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkOPfV8r.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-BJK0-svt.mjs +26 -0
- package/dist/stream-broker/{hostInit-B7Fx3R_6.mjs → hostInit-DyLqyJaS.mjs} +2 -2
- package/dist/stream-broker/index.js +2 -2
- package/dist/stream-broker/index.mjs +2 -2
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CPkQfKnu.js → MaskShapeCanvas-DI4BY7W2-BDLNwJ_F.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DlLMQBag.js → MotionZonesSettings-NcxxQN8r-CoLjNiUN.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-kNcauIAN.js → PrivacyMaskSettings-APgPLF7p-DJE3OU-q.js} +1 -1
- package/embed-dist/assets/index-C-pL8ETk.js +81 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +522 -27
- package/python/test_inference_pool_backpressure.py +121 -0
- package/python/test_inference_pool_coreml_cache.py +416 -0
- package/python/test_inference_pool_device_selection.py +256 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-wfWYFiTT.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-lpFwIH9e.mjs +0 -26
- package/embed-dist/assets/index-xpPLFfsT.js +0 -80
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-DAIlCdAx.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, Y as hydrateSchema, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-
|
|
2
|
+
import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, Y as hydrateSchema, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-CgEP_0OL.mjs";
|
|
3
3
|
import { r as downloadFile } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DAIlCdAx.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
//#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-
|
|
1
|
+
import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-CgEP_0OL.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
//#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DAIlCdAx.js");
|
|
6
6
|
const require_frame_dropper = require("../frame-dropper-7RTo_YyG.js");
|
|
7
7
|
let _camstack_shm_ring = require("@camstack/shm-ring");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, p as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-
|
|
1
|
+
import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, p as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-CgEP_0OL.mjs";
|
|
2
2
|
import { r as logBannerArgs, t as FrameDropper } from "../frame-dropper-CwkBTPGV.mjs";
|
|
3
3
|
import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-DAIlCdAx.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -28,8 +28,12 @@ sharp = require_model_download_service_C_IHWnXx.__toESM(sharp);
|
|
|
28
28
|
*
|
|
29
29
|
* These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
|
|
30
30
|
* Everything else in the store stays shared. A legacy un-scoped value (written
|
|
31
|
-
* before this change, or by an older build) is read as a migration fallback
|
|
32
|
-
*
|
|
31
|
+
* before this change, or by an older build) is read as a migration fallback
|
|
32
|
+
* for the HUB ONLY — the pre-scoping store was only ever written by a
|
|
33
|
+
* hub-only deployment, so serving the bare value to any other node leaks the
|
|
34
|
+
* hub's hardware pick (e.g. `openvino/auto`) onto a node that cannot run it
|
|
35
|
+
* (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). A non-hub node with
|
|
36
|
+
* no scoped key reads `undefined`, which drives its own auto-pick.
|
|
33
37
|
*/
|
|
34
38
|
var ENGINE_CASCADE_KEYS = [
|
|
35
39
|
"engineBackend",
|
|
@@ -54,11 +58,27 @@ function nodeEngineKey(base, nodeId) {
|
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
60
|
* Read an engine cascade value for a node: the node-scoped key if present,
|
|
57
|
-
* otherwise the legacy un-scoped value (migration),
|
|
61
|
+
* otherwise — for the HUB only — the legacy un-scoped value (migration),
|
|
62
|
+
* otherwise undefined. The bare fallback is hub-restricted because only the
|
|
63
|
+
* hub ever wrote the pre-scoping store; serving it to other nodes leaked the
|
|
64
|
+
* hub's engine selection cluster-wide (CB3). A non-hub node with no scoped
|
|
65
|
+
* key gets `undefined` so its own platform-driven auto-pick runs and persists
|
|
66
|
+
* a real per-node selection.
|
|
58
67
|
*/
|
|
59
68
|
function readNodeEngineValue(store, base, nodeId) {
|
|
60
69
|
const scoped = store[nodeEngineKey(base, nodeId)];
|
|
61
|
-
|
|
70
|
+
if (scoped !== void 0) return scoped;
|
|
71
|
+
return normalizeEngineNodeId(nodeId) === "hub" ? store[base] : void 0;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Re-map a UI/settings patch so engine cascade fields persist under the
|
|
75
|
+
* node-scoped key; all other keys pass through unchanged. Used on the write
|
|
76
|
+
* path so a UI save on one node never clobbers another node's engine.
|
|
77
|
+
*/
|
|
78
|
+
function scopeEnginePatch(patch, nodeId) {
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const [key, value] of Object.entries(patch)) out[isEngineCascadeKey(key) ? nodeEngineKey(key, nodeId) : key] = value;
|
|
81
|
+
return out;
|
|
62
82
|
}
|
|
63
83
|
/**
|
|
64
84
|
* Project a raw store onto the plain engine cascade keys for THIS node, so the
|
|
@@ -257,6 +277,19 @@ function pickBestRuntime(env, bestBackendHint) {
|
|
|
257
277
|
}
|
|
258
278
|
//#endregion
|
|
259
279
|
//#region src/detection-pipeline/engine/shared-inference-pool.ts
|
|
280
|
+
/**
|
|
281
|
+
* SharedInferencePool — TypeScript wrapper for inference_pool.py.
|
|
282
|
+
*
|
|
283
|
+
* Multi-process orchestrator. Owns N `PoolWorker` instances, each
|
|
284
|
+
* holding its own Python subprocess + MLModel copy, and round-robins
|
|
285
|
+
* inference dispatches across them. Model management (load/unload/
|
|
286
|
+
* replace) propagates to all workers so every subprocess holds the
|
|
287
|
+
* same model set at the same indices.
|
|
288
|
+
*
|
|
289
|
+
* Wire format (per worker):
|
|
290
|
+
* Request: [4B total_len][4B req_id][1B msg_type][payload]
|
|
291
|
+
* Response: [4B total_len][4B req_id][JSON payload]
|
|
292
|
+
*/
|
|
260
293
|
var MSG_COMMAND = 0;
|
|
261
294
|
var MSG_INFER_JPEG = 1;
|
|
262
295
|
var MSG_INFER_RAW = 2;
|
|
@@ -568,6 +601,13 @@ var SharedInferencePool = class {
|
|
|
568
601
|
nextDispatchIdx = 0;
|
|
569
602
|
nextFreeIndex = 0;
|
|
570
603
|
nextFrameId = 1;
|
|
604
|
+
/**
|
|
605
|
+
* Cumulative count of frames the Python pool SHED under overload
|
|
606
|
+
* (`"dropped": true` responses from the per-model in-flight bound in
|
|
607
|
+
* inference_pool.py). Without this the shed response is
|
|
608
|
+
* indistinguishable from a genuine "no detections" result.
|
|
609
|
+
*/
|
|
610
|
+
droppedResponseCount = 0;
|
|
571
611
|
log;
|
|
572
612
|
concurrency;
|
|
573
613
|
tuning;
|
|
@@ -624,17 +664,28 @@ var SharedInferencePool = class {
|
|
|
624
664
|
};
|
|
625
665
|
}
|
|
626
666
|
async infer(modelIndex, jpeg) {
|
|
627
|
-
|
|
667
|
+
const w = this.pickWorker();
|
|
668
|
+
return this.trackDroppedResponse(await w.infer(this.encodeModelByte(modelIndex), jpeg), modelIndex);
|
|
628
669
|
}
|
|
629
670
|
async inferRaw(modelIndex, raw, width, height, format) {
|
|
630
|
-
|
|
671
|
+
const w = this.pickWorker();
|
|
672
|
+
return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
|
|
631
673
|
}
|
|
632
674
|
async inferBatch(modelIndex, items) {
|
|
633
675
|
if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
|
|
634
676
|
return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
|
|
635
677
|
}
|
|
636
678
|
async inferCached(modelIndex, frameId) {
|
|
637
|
-
|
|
679
|
+
const w = this.pickWorker();
|
|
680
|
+
return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Total `"dropped": true` responses seen from the Python pool — frames
|
|
684
|
+
* shed by the per-model in-flight bound under overload. Diagnostic
|
|
685
|
+
* counter (monotonic for the pool's lifetime).
|
|
686
|
+
*/
|
|
687
|
+
getDroppedResponseCount() {
|
|
688
|
+
return this.droppedResponseCount;
|
|
638
689
|
}
|
|
639
690
|
getHandle(modelIndex) {
|
|
640
691
|
return new PoolHandle(this, modelIndex);
|
|
@@ -695,6 +746,22 @@ var SharedInferencePool = class {
|
|
|
695
746
|
this.workers.length = 0;
|
|
696
747
|
this.log.info("Pool process(es) terminated");
|
|
697
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
751
|
+
* single-frame inference paths. Previously indistinguishable from a
|
|
752
|
+
* genuine empty detection result — now counted + debug-logged so
|
|
753
|
+
* overload is measurable. The response passes through unchanged.
|
|
754
|
+
*/
|
|
755
|
+
trackDroppedResponse(result, modelIndex) {
|
|
756
|
+
if (result["dropped"] === true) {
|
|
757
|
+
this.droppedResponseCount++;
|
|
758
|
+
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
759
|
+
modelIndex,
|
|
760
|
+
droppedTotal: this.droppedResponseCount
|
|
761
|
+
} });
|
|
762
|
+
}
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
698
765
|
pickWorker() {
|
|
699
766
|
if (this.workers.length === 0) throw new Error("SharedInferencePool: not initialized");
|
|
700
767
|
const idx = this.nextDispatchIdx;
|
|
@@ -5433,6 +5500,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5433
5500
|
const priorFactory = this.engineFactory;
|
|
5434
5501
|
const priorExecutor = this.executor;
|
|
5435
5502
|
if (!override) return () => {};
|
|
5503
|
+
if (!backendPossibleOnPlatform(override.backend)) {
|
|
5504
|
+
this.log.warn("Engine override impossible on this platform — running on own engine", { meta: {
|
|
5505
|
+
override: `${override.runtime}/${override.backend}/${override.device ?? "default"}`,
|
|
5506
|
+
platform: process.platform,
|
|
5507
|
+
arch: process.arch,
|
|
5508
|
+
engine: `${priorEngine.runtime}/${priorEngine.backend}`
|
|
5509
|
+
} });
|
|
5510
|
+
return () => {};
|
|
5511
|
+
}
|
|
5436
5512
|
const runEngine = {
|
|
5437
5513
|
runtime: override.runtime,
|
|
5438
5514
|
backend: override.backend,
|
|
@@ -6128,6 +6204,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6128
6204
|
stored: migratedBackend,
|
|
6129
6205
|
fallback: platformDefault.backend
|
|
6130
6206
|
} });
|
|
6207
|
+
this.writeStore({
|
|
6208
|
+
[nodeEngineKey("engineBackend", node)]: platformDefault.backend,
|
|
6209
|
+
...platformDefault.device !== void 0 ? { [nodeEngineKey("engineDevice", node)]: platformDefault.device } : {}
|
|
6210
|
+
}).catch((err) => {
|
|
6211
|
+
this.log.warn("Failed to persist platform-default engine after impossible stored pick", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6212
|
+
});
|
|
6131
6213
|
return platformDefault;
|
|
6132
6214
|
}
|
|
6133
6215
|
const device = storedDevice || floor.device;
|
|
@@ -6779,7 +6861,9 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
6779
6861
|
provider = null;
|
|
6780
6862
|
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
6781
6863
|
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
6782
|
-
* node because the hub addon probes other nodes when serving their config.
|
|
6864
|
+
* node because the hub addon probes other nodes when serving their config.
|
|
6865
|
+
* Carries the probed node's platform/arch too, so a transient null probe
|
|
6866
|
+
* doesn't flip a remote node's env back to the serving hub's platform. */
|
|
6783
6867
|
lastGoodHardwareByNode = /* @__PURE__ */ new Map();
|
|
6784
6868
|
/** This node's effective engine selection, cached from the node-scoped store
|
|
6785
6869
|
* so the synchronous `resolveBackendTuning` and the reprobe gate don't read
|
|
@@ -7086,16 +7170,19 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
7086
7170
|
const hardware = await this.resolveProbeHardware(node);
|
|
7087
7171
|
if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
|
|
7088
7172
|
const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
|
|
7173
|
+
const useProbedPlatform = node !== this.localNodeId();
|
|
7089
7174
|
return {
|
|
7090
|
-
platform: process.platform,
|
|
7091
|
-
arch: process.arch,
|
|
7175
|
+
platform: useProbedPlatform && effective?.platform !== void 0 ? effective.platform : process.platform,
|
|
7176
|
+
arch: useProbedPlatform && effective?.arch !== void 0 ? effective.arch : process.arch,
|
|
7092
7177
|
hardware: effective
|
|
7093
7178
|
};
|
|
7094
7179
|
}
|
|
7095
7180
|
/**
|
|
7096
7181
|
* Fetch the probed hardware from the platform-probe cap for `nodeId` (default
|
|
7097
7182
|
* = self). Returns null when the cap is not reachable (caller falls back to
|
|
7098
|
-
* the registry's safe minimum).
|
|
7183
|
+
* the registry's safe minimum). Carries the probed node's `platform`/`arch`
|
|
7184
|
+
* (from `PlatformCapabilities.hardware`) so `probeHardwareEnv` can build the
|
|
7185
|
+
* env for the TARGET node rather than the serving one.
|
|
7099
7186
|
*/
|
|
7100
7187
|
async resolveProbeHardware(nodeId) {
|
|
7101
7188
|
try {
|
|
@@ -7106,7 +7193,9 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
7106
7193
|
if (!hw) return null;
|
|
7107
7194
|
return {
|
|
7108
7195
|
npu: hw.npu ? { type: hw.npu.type } : null,
|
|
7109
|
-
gpu: hw.gpu ? { type: hw.gpu.type } : null
|
|
7196
|
+
gpu: hw.gpu ? { type: hw.gpu.type } : null,
|
|
7197
|
+
platform: hw.platform,
|
|
7198
|
+
arch: hw.arch
|
|
7110
7199
|
};
|
|
7111
7200
|
} catch {
|
|
7112
7201
|
return null;
|
|
@@ -7125,6 +7214,34 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
7125
7214
|
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
7126
7215
|
}
|
|
7127
7216
|
/**
|
|
7217
|
+
* One-time store migration (hub only, F1/CB3): the pre-scoping store
|
|
7218
|
+
* persisted the engine cascade under BARE keys (`engineBackend`, …) that
|
|
7219
|
+
* only the hub ever wrote. `readNodeEngineValue` used to serve those bare
|
|
7220
|
+
* values as a fallback to EVERY node, leaking the hub's engine (e.g.
|
|
7221
|
+
* `openvino/auto`) onto agents with no scoped key. The fallback is now
|
|
7222
|
+
* hub-only (engine-store-keys.ts) and this migration re-homes the bare
|
|
7223
|
+
* values to their `<key>@hub` scoped form and deletes the bare keys so the
|
|
7224
|
+
* legacy values cannot resurface. Guarded: runs only while a bare key is
|
|
7225
|
+
* still present, so post-migration boots are a no-op. Best-effort — an
|
|
7226
|
+
* unreadable store skips the migration (retried on the next boot).
|
|
7227
|
+
*/
|
|
7228
|
+
async migrateLegacyBareEngineKeys() {
|
|
7229
|
+
if (this.localNodeId() !== "hub") return;
|
|
7230
|
+
const store = await this.resolveUiSettingsStore();
|
|
7231
|
+
if (store === null) return;
|
|
7232
|
+
const patch = {};
|
|
7233
|
+
for (const base of ENGINE_CASCADE_KEYS) {
|
|
7234
|
+
const bare = store[base];
|
|
7235
|
+
if (bare === void 0 || bare === null) continue;
|
|
7236
|
+
const scopedKey = nodeEngineKey(base, "hub");
|
|
7237
|
+
if (store[scopedKey] === void 0) patch[scopedKey] = bare;
|
|
7238
|
+
patch[base] = void 0;
|
|
7239
|
+
}
|
|
7240
|
+
if (Object.keys(patch).length === 0) return;
|
|
7241
|
+
await this.ctxIfReady?.settings?.writeAddonStore(patch);
|
|
7242
|
+
this.ctx.logger.info("Migrated legacy bare engine keys to their @hub scoped form", { meta: { keys: Object.keys(patch) } });
|
|
7243
|
+
}
|
|
7244
|
+
/**
|
|
7128
7245
|
* Refresh this node's cached engine selection from the node-scoped store.
|
|
7129
7246
|
* `resolveBackendTuning` is synchronous and the reprobe gate runs before the
|
|
7130
7247
|
* provider exists, so both read these cached fields instead of the
|
|
@@ -7146,26 +7263,30 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
7146
7263
|
* cluster-central store. The hub addon serves writes for every node, so it
|
|
7147
7264
|
* scopes by the requested `nodeId` (default self).
|
|
7148
7265
|
*
|
|
7149
|
-
*
|
|
7150
|
-
*
|
|
7151
|
-
*
|
|
7152
|
-
*
|
|
7153
|
-
* the
|
|
7154
|
-
*
|
|
7266
|
+
* The engine cascade keys are NEVER written bare (F1, CB3): the bare keys are
|
|
7267
|
+
* the legacy pre-scoping store whose values used to leak to every node with
|
|
7268
|
+
* no scoped key. When the target IS this node, `super.updateGlobalSettings`
|
|
7269
|
+
* drives the normal apply path (`resolveConfig` / `onConfigChanged` /
|
|
7270
|
+
* `requiresRestart` restart) with the engine keys STRIPPED from its patch —
|
|
7271
|
+
* they were already persisted node-scoped, and `onConfigChanged` re-reads
|
|
7272
|
+
* them from the scoped keys. When the target is a SIBLING node, one scoped
|
|
7273
|
+
* write persists the engine keys under `<key>@<target>` + the non-engine
|
|
7274
|
+
* keys bare, and this node's restart/reprovision is NOT run — the owning
|
|
7275
|
+
* node applies its own engine selection on its next (re)start.
|
|
7155
7276
|
*/
|
|
7156
7277
|
async updateGlobalSettings(patch, nodeId) {
|
|
7157
7278
|
const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
|
|
7158
7279
|
const patchRecord = patch;
|
|
7280
|
+
if (targetNode !== this.localNodeId()) {
|
|
7281
|
+
const scoped = scopeEnginePatch(patchRecord, targetNode);
|
|
7282
|
+
if (Object.keys(scoped).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scoped);
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
7159
7285
|
const scopedEngine = {};
|
|
7160
7286
|
for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
|
|
7161
7287
|
if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
return;
|
|
7165
|
-
}
|
|
7166
|
-
const sharedPatch = {};
|
|
7167
|
-
for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
|
|
7168
|
-
if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
|
|
7288
|
+
const { engineBackend: _engineBackend, engineDevice: _engineDevice, probedBestEngine: _probedBestEngine, ...sharedPatch } = patch;
|
|
7289
|
+
await super.updateGlobalSettings(sharedPatch, nodeId);
|
|
7169
7290
|
}
|
|
7170
7291
|
/**
|
|
7171
7292
|
* Resolve the effective pool tuning for the configured backend.
|
|
@@ -7197,6 +7318,7 @@ var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
|
7197
7318
|
async onInitialize() {
|
|
7198
7319
|
const modelsDir = await this.resolveModelsDir();
|
|
7199
7320
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
7321
|
+
await this.migrateLegacyBareEngineKeys();
|
|
7200
7322
|
await this.refreshNodeEngineFromStore();
|
|
7201
7323
|
this.pythonAddonDir = resolveAddonPythonDir();
|
|
7202
7324
|
const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, J as createEvent, M as pipelineExecutorCapability, Q as parseJsonUnknown, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, Y as hydrateSchema, a as COCO_TO_MACRO, i as COCO_80_LABELS, j as nodePin, m as YAMNET_TO_MACRO, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, tt as sleep, w as detectionPipelineCapability } from "../dist-
|
|
2
|
+
import { B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, J as createEvent, M as pipelineExecutorCapability, Q as parseJsonUnknown, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, Y as hydrateSchema, a as COCO_TO_MACRO, i as COCO_80_LABELS, j as nodePin, m as YAMNET_TO_MACRO, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, tt as sleep, w as detectionPipelineCapability } from "../dist-CgEP_0OL.mjs";
|
|
3
3
|
import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
@@ -21,8 +21,12 @@ import sharp from "sharp";
|
|
|
21
21
|
*
|
|
22
22
|
* These three keys are therefore persisted node-scoped as `<key>@<nodeId>`.
|
|
23
23
|
* Everything else in the store stays shared. A legacy un-scoped value (written
|
|
24
|
-
* before this change, or by an older build) is read as a migration fallback
|
|
25
|
-
*
|
|
24
|
+
* before this change, or by an older build) is read as a migration fallback
|
|
25
|
+
* for the HUB ONLY — the pre-scoping store was only ever written by a
|
|
26
|
+
* hub-only deployment, so serving the bare value to any other node leaks the
|
|
27
|
+
* hub's hardware pick (e.g. `openvino/auto`) onto a node that cannot run it
|
|
28
|
+
* (the CB3 "Mac runs OpenVINO" / N100 AUTO-device bug). A non-hub node with
|
|
29
|
+
* no scoped key reads `undefined`, which drives its own auto-pick.
|
|
26
30
|
*/
|
|
27
31
|
var ENGINE_CASCADE_KEYS = [
|
|
28
32
|
"engineBackend",
|
|
@@ -47,11 +51,27 @@ function nodeEngineKey(base, nodeId) {
|
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* Read an engine cascade value for a node: the node-scoped key if present,
|
|
50
|
-
* otherwise the legacy un-scoped value (migration),
|
|
54
|
+
* otherwise — for the HUB only — the legacy un-scoped value (migration),
|
|
55
|
+
* otherwise undefined. The bare fallback is hub-restricted because only the
|
|
56
|
+
* hub ever wrote the pre-scoping store; serving it to other nodes leaked the
|
|
57
|
+
* hub's engine selection cluster-wide (CB3). A non-hub node with no scoped
|
|
58
|
+
* key gets `undefined` so its own platform-driven auto-pick runs and persists
|
|
59
|
+
* a real per-node selection.
|
|
51
60
|
*/
|
|
52
61
|
function readNodeEngineValue(store, base, nodeId) {
|
|
53
62
|
const scoped = store[nodeEngineKey(base, nodeId)];
|
|
54
|
-
|
|
63
|
+
if (scoped !== void 0) return scoped;
|
|
64
|
+
return normalizeEngineNodeId(nodeId) === "hub" ? store[base] : void 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Re-map a UI/settings patch so engine cascade fields persist under the
|
|
68
|
+
* node-scoped key; all other keys pass through unchanged. Used on the write
|
|
69
|
+
* path so a UI save on one node never clobbers another node's engine.
|
|
70
|
+
*/
|
|
71
|
+
function scopeEnginePatch(patch, nodeId) {
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [key, value] of Object.entries(patch)) out[isEngineCascadeKey(key) ? nodeEngineKey(key, nodeId) : key] = value;
|
|
74
|
+
return out;
|
|
55
75
|
}
|
|
56
76
|
/**
|
|
57
77
|
* Project a raw store onto the plain engine cascade keys for THIS node, so the
|
|
@@ -250,6 +270,19 @@ function pickBestRuntime(env, bestBackendHint) {
|
|
|
250
270
|
}
|
|
251
271
|
//#endregion
|
|
252
272
|
//#region src/detection-pipeline/engine/shared-inference-pool.ts
|
|
273
|
+
/**
|
|
274
|
+
* SharedInferencePool — TypeScript wrapper for inference_pool.py.
|
|
275
|
+
*
|
|
276
|
+
* Multi-process orchestrator. Owns N `PoolWorker` instances, each
|
|
277
|
+
* holding its own Python subprocess + MLModel copy, and round-robins
|
|
278
|
+
* inference dispatches across them. Model management (load/unload/
|
|
279
|
+
* replace) propagates to all workers so every subprocess holds the
|
|
280
|
+
* same model set at the same indices.
|
|
281
|
+
*
|
|
282
|
+
* Wire format (per worker):
|
|
283
|
+
* Request: [4B total_len][4B req_id][1B msg_type][payload]
|
|
284
|
+
* Response: [4B total_len][4B req_id][JSON payload]
|
|
285
|
+
*/
|
|
253
286
|
var MSG_COMMAND = 0;
|
|
254
287
|
var MSG_INFER_JPEG = 1;
|
|
255
288
|
var MSG_INFER_RAW = 2;
|
|
@@ -561,6 +594,13 @@ var SharedInferencePool = class {
|
|
|
561
594
|
nextDispatchIdx = 0;
|
|
562
595
|
nextFreeIndex = 0;
|
|
563
596
|
nextFrameId = 1;
|
|
597
|
+
/**
|
|
598
|
+
* Cumulative count of frames the Python pool SHED under overload
|
|
599
|
+
* (`"dropped": true` responses from the per-model in-flight bound in
|
|
600
|
+
* inference_pool.py). Without this the shed response is
|
|
601
|
+
* indistinguishable from a genuine "no detections" result.
|
|
602
|
+
*/
|
|
603
|
+
droppedResponseCount = 0;
|
|
564
604
|
log;
|
|
565
605
|
concurrency;
|
|
566
606
|
tuning;
|
|
@@ -617,17 +657,28 @@ var SharedInferencePool = class {
|
|
|
617
657
|
};
|
|
618
658
|
}
|
|
619
659
|
async infer(modelIndex, jpeg) {
|
|
620
|
-
|
|
660
|
+
const w = this.pickWorker();
|
|
661
|
+
return this.trackDroppedResponse(await w.infer(this.encodeModelByte(modelIndex), jpeg), modelIndex);
|
|
621
662
|
}
|
|
622
663
|
async inferRaw(modelIndex, raw, width, height, format) {
|
|
623
|
-
|
|
664
|
+
const w = this.pickWorker();
|
|
665
|
+
return this.trackDroppedResponse(await w.inferRaw(this.encodeModelByte(modelIndex), raw, width, height, format), modelIndex);
|
|
624
666
|
}
|
|
625
667
|
async inferBatch(modelIndex, items) {
|
|
626
668
|
if (items.length > 255) throw new Error(`SharedInferencePool.inferBatch: max 255 items per call, got ${items.length}`);
|
|
627
669
|
return this.pickWorker().inferBatch(this.encodeModelByte(modelIndex), items);
|
|
628
670
|
}
|
|
629
671
|
async inferCached(modelIndex, frameId) {
|
|
630
|
-
|
|
672
|
+
const w = this.pickWorker();
|
|
673
|
+
return this.trackDroppedResponse(await w.inferCached(this.encodeModelByte(modelIndex), frameId), modelIndex);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Total `"dropped": true` responses seen from the Python pool — frames
|
|
677
|
+
* shed by the per-model in-flight bound under overload. Diagnostic
|
|
678
|
+
* counter (monotonic for the pool's lifetime).
|
|
679
|
+
*/
|
|
680
|
+
getDroppedResponseCount() {
|
|
681
|
+
return this.droppedResponseCount;
|
|
631
682
|
}
|
|
632
683
|
getHandle(modelIndex) {
|
|
633
684
|
return new PoolHandle(this, modelIndex);
|
|
@@ -688,6 +739,22 @@ var SharedInferencePool = class {
|
|
|
688
739
|
this.workers.length = 0;
|
|
689
740
|
this.log.info("Pool process(es) terminated");
|
|
690
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
744
|
+
* single-frame inference paths. Previously indistinguishable from a
|
|
745
|
+
* genuine empty detection result — now counted + debug-logged so
|
|
746
|
+
* overload is measurable. The response passes through unchanged.
|
|
747
|
+
*/
|
|
748
|
+
trackDroppedResponse(result, modelIndex) {
|
|
749
|
+
if (result["dropped"] === true) {
|
|
750
|
+
this.droppedResponseCount++;
|
|
751
|
+
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
752
|
+
modelIndex,
|
|
753
|
+
droppedTotal: this.droppedResponseCount
|
|
754
|
+
} });
|
|
755
|
+
}
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
691
758
|
pickWorker() {
|
|
692
759
|
if (this.workers.length === 0) throw new Error("SharedInferencePool: not initialized");
|
|
693
760
|
const idx = this.nextDispatchIdx;
|
|
@@ -5426,6 +5493,15 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5426
5493
|
const priorFactory = this.engineFactory;
|
|
5427
5494
|
const priorExecutor = this.executor;
|
|
5428
5495
|
if (!override) return () => {};
|
|
5496
|
+
if (!backendPossibleOnPlatform(override.backend)) {
|
|
5497
|
+
this.log.warn("Engine override impossible on this platform — running on own engine", { meta: {
|
|
5498
|
+
override: `${override.runtime}/${override.backend}/${override.device ?? "default"}`,
|
|
5499
|
+
platform: process.platform,
|
|
5500
|
+
arch: process.arch,
|
|
5501
|
+
engine: `${priorEngine.runtime}/${priorEngine.backend}`
|
|
5502
|
+
} });
|
|
5503
|
+
return () => {};
|
|
5504
|
+
}
|
|
5429
5505
|
const runEngine = {
|
|
5430
5506
|
runtime: override.runtime,
|
|
5431
5507
|
backend: override.backend,
|
|
@@ -6121,6 +6197,12 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6121
6197
|
stored: migratedBackend,
|
|
6122
6198
|
fallback: platformDefault.backend
|
|
6123
6199
|
} });
|
|
6200
|
+
this.writeStore({
|
|
6201
|
+
[nodeEngineKey("engineBackend", node)]: platformDefault.backend,
|
|
6202
|
+
...platformDefault.device !== void 0 ? { [nodeEngineKey("engineDevice", node)]: platformDefault.device } : {}
|
|
6203
|
+
}).catch((err) => {
|
|
6204
|
+
this.log.warn("Failed to persist platform-default engine after impossible stored pick", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
6205
|
+
});
|
|
6124
6206
|
return platformDefault;
|
|
6125
6207
|
}
|
|
6126
6208
|
const device = storedDevice || floor.device;
|
|
@@ -6772,7 +6854,9 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
6772
6854
|
provider = null;
|
|
6773
6855
|
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
6774
6856
|
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
6775
|
-
* node because the hub addon probes other nodes when serving their config.
|
|
6857
|
+
* node because the hub addon probes other nodes when serving their config.
|
|
6858
|
+
* Carries the probed node's platform/arch too, so a transient null probe
|
|
6859
|
+
* doesn't flip a remote node's env back to the serving hub's platform. */
|
|
6776
6860
|
lastGoodHardwareByNode = /* @__PURE__ */ new Map();
|
|
6777
6861
|
/** This node's effective engine selection, cached from the node-scoped store
|
|
6778
6862
|
* so the synchronous `resolveBackendTuning` and the reprobe gate don't read
|
|
@@ -7079,16 +7163,19 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
7079
7163
|
const hardware = await this.resolveProbeHardware(node);
|
|
7080
7164
|
if (hardware) this.lastGoodHardwareByNode.set(node, hardware);
|
|
7081
7165
|
const effective = hardware ?? this.lastGoodHardwareByNode.get(node) ?? null;
|
|
7166
|
+
const useProbedPlatform = node !== this.localNodeId();
|
|
7082
7167
|
return {
|
|
7083
|
-
platform: process.platform,
|
|
7084
|
-
arch: process.arch,
|
|
7168
|
+
platform: useProbedPlatform && effective?.platform !== void 0 ? effective.platform : process.platform,
|
|
7169
|
+
arch: useProbedPlatform && effective?.arch !== void 0 ? effective.arch : process.arch,
|
|
7085
7170
|
hardware: effective
|
|
7086
7171
|
};
|
|
7087
7172
|
}
|
|
7088
7173
|
/**
|
|
7089
7174
|
* Fetch the probed hardware from the platform-probe cap for `nodeId` (default
|
|
7090
7175
|
* = self). Returns null when the cap is not reachable (caller falls back to
|
|
7091
|
-
* the registry's safe minimum).
|
|
7176
|
+
* the registry's safe minimum). Carries the probed node's `platform`/`arch`
|
|
7177
|
+
* (from `PlatformCapabilities.hardware`) so `probeHardwareEnv` can build the
|
|
7178
|
+
* env for the TARGET node rather than the serving one.
|
|
7092
7179
|
*/
|
|
7093
7180
|
async resolveProbeHardware(nodeId) {
|
|
7094
7181
|
try {
|
|
@@ -7099,7 +7186,9 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
7099
7186
|
if (!hw) return null;
|
|
7100
7187
|
return {
|
|
7101
7188
|
npu: hw.npu ? { type: hw.npu.type } : null,
|
|
7102
|
-
gpu: hw.gpu ? { type: hw.gpu.type } : null
|
|
7189
|
+
gpu: hw.gpu ? { type: hw.gpu.type } : null,
|
|
7190
|
+
platform: hw.platform,
|
|
7191
|
+
arch: hw.arch
|
|
7103
7192
|
};
|
|
7104
7193
|
} catch {
|
|
7105
7194
|
return null;
|
|
@@ -7118,6 +7207,34 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
7118
7207
|
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
7119
7208
|
}
|
|
7120
7209
|
/**
|
|
7210
|
+
* One-time store migration (hub only, F1/CB3): the pre-scoping store
|
|
7211
|
+
* persisted the engine cascade under BARE keys (`engineBackend`, …) that
|
|
7212
|
+
* only the hub ever wrote. `readNodeEngineValue` used to serve those bare
|
|
7213
|
+
* values as a fallback to EVERY node, leaking the hub's engine (e.g.
|
|
7214
|
+
* `openvino/auto`) onto agents with no scoped key. The fallback is now
|
|
7215
|
+
* hub-only (engine-store-keys.ts) and this migration re-homes the bare
|
|
7216
|
+
* values to their `<key>@hub` scoped form and deletes the bare keys so the
|
|
7217
|
+
* legacy values cannot resurface. Guarded: runs only while a bare key is
|
|
7218
|
+
* still present, so post-migration boots are a no-op. Best-effort — an
|
|
7219
|
+
* unreadable store skips the migration (retried on the next boot).
|
|
7220
|
+
*/
|
|
7221
|
+
async migrateLegacyBareEngineKeys() {
|
|
7222
|
+
if (this.localNodeId() !== "hub") return;
|
|
7223
|
+
const store = await this.resolveUiSettingsStore();
|
|
7224
|
+
if (store === null) return;
|
|
7225
|
+
const patch = {};
|
|
7226
|
+
for (const base of ENGINE_CASCADE_KEYS) {
|
|
7227
|
+
const bare = store[base];
|
|
7228
|
+
if (bare === void 0 || bare === null) continue;
|
|
7229
|
+
const scopedKey = nodeEngineKey(base, "hub");
|
|
7230
|
+
if (store[scopedKey] === void 0) patch[scopedKey] = bare;
|
|
7231
|
+
patch[base] = void 0;
|
|
7232
|
+
}
|
|
7233
|
+
if (Object.keys(patch).length === 0) return;
|
|
7234
|
+
await this.ctxIfReady?.settings?.writeAddonStore(patch);
|
|
7235
|
+
this.ctx.logger.info("Migrated legacy bare engine keys to their @hub scoped form", { meta: { keys: Object.keys(patch) } });
|
|
7236
|
+
}
|
|
7237
|
+
/**
|
|
7121
7238
|
* Refresh this node's cached engine selection from the node-scoped store.
|
|
7122
7239
|
* `resolveBackendTuning` is synchronous and the reprobe gate runs before the
|
|
7123
7240
|
* provider exists, so both read these cached fields instead of the
|
|
@@ -7139,26 +7256,30 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
7139
7256
|
* cluster-central store. The hub addon serves writes for every node, so it
|
|
7140
7257
|
* scopes by the requested `nodeId` (default self).
|
|
7141
7258
|
*
|
|
7142
|
-
*
|
|
7143
|
-
*
|
|
7144
|
-
*
|
|
7145
|
-
*
|
|
7146
|
-
* the
|
|
7147
|
-
*
|
|
7259
|
+
* The engine cascade keys are NEVER written bare (F1, CB3): the bare keys are
|
|
7260
|
+
* the legacy pre-scoping store whose values used to leak to every node with
|
|
7261
|
+
* no scoped key. When the target IS this node, `super.updateGlobalSettings`
|
|
7262
|
+
* drives the normal apply path (`resolveConfig` / `onConfigChanged` /
|
|
7263
|
+
* `requiresRestart` restart) with the engine keys STRIPPED from its patch —
|
|
7264
|
+
* they were already persisted node-scoped, and `onConfigChanged` re-reads
|
|
7265
|
+
* them from the scoped keys. When the target is a SIBLING node, one scoped
|
|
7266
|
+
* write persists the engine keys under `<key>@<target>` + the non-engine
|
|
7267
|
+
* keys bare, and this node's restart/reprovision is NOT run — the owning
|
|
7268
|
+
* node applies its own engine selection on its next (re)start.
|
|
7148
7269
|
*/
|
|
7149
7270
|
async updateGlobalSettings(patch, nodeId) {
|
|
7150
7271
|
const targetNode = nodeId ? normalizeEngineNodeId(nodeId) : this.localNodeId();
|
|
7151
7272
|
const patchRecord = patch;
|
|
7273
|
+
if (targetNode !== this.localNodeId()) {
|
|
7274
|
+
const scoped = scopeEnginePatch(patchRecord, targetNode);
|
|
7275
|
+
if (Object.keys(scoped).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scoped);
|
|
7276
|
+
return;
|
|
7277
|
+
}
|
|
7152
7278
|
const scopedEngine = {};
|
|
7153
7279
|
for (const key of ENGINE_CASCADE_KEYS) if (key in patchRecord) scopedEngine[nodeEngineKey(key, targetNode)] = patchRecord[key];
|
|
7154
7280
|
if (Object.keys(scopedEngine).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(scopedEngine);
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
return;
|
|
7158
|
-
}
|
|
7159
|
-
const sharedPatch = {};
|
|
7160
|
-
for (const [k, v] of Object.entries(patchRecord)) if (!ENGINE_CASCADE_KEYS.includes(k)) sharedPatch[k] = v;
|
|
7161
|
-
if (Object.keys(sharedPatch).length > 0) await this.ctxIfReady?.settings?.writeAddonStore(sharedPatch);
|
|
7281
|
+
const { engineBackend: _engineBackend, engineDevice: _engineDevice, probedBestEngine: _probedBestEngine, ...sharedPatch } = patch;
|
|
7282
|
+
await super.updateGlobalSettings(sharedPatch, nodeId);
|
|
7162
7283
|
}
|
|
7163
7284
|
/**
|
|
7164
7285
|
* Resolve the effective pool tuning for the configured backend.
|
|
@@ -7190,6 +7311,7 @@ var DetectionPipelineAddon = class extends BaseAddon {
|
|
|
7190
7311
|
async onInitialize() {
|
|
7191
7312
|
const modelsDir = await this.resolveModelsDir();
|
|
7192
7313
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
7314
|
+
await this.migrateLegacyBareEngineKeys();
|
|
7193
7315
|
await this.refreshNodeEngineFromStore();
|
|
7194
7316
|
this.pythonAddonDir = resolveAddonPythonDir();
|
|
7195
7317
|
const py = await ensurePythonReady(this.ctx.deps, this.ctx.logger);
|