@camstack/addon-pipeline 1.2.28 → 1.2.31
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/detection-pipeline/index.js +518 -203
- package/dist/detection-pipeline/index.mjs +517 -202
- package/dist/{dist-DUHlZcWd.js → dist-BvFLZyhW.js} +105 -3
- package/dist/{dist-CsKaXmP8.mjs → dist-Du7_SNiR.mjs} +105 -3
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +149 -8
- package/dist/pipeline-runner/index.mjs +148 -8
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/{step-definitions-C8A78dBi.mjs → step-definitions-BkY9_IM6.mjs} +13 -2
- package/dist/{step-definitions-Bo1hJQQw.js → step-definitions-gC-Ovglc.js} +13 -2
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-tHahN-lK.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BUXeX-vb.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ByHi7ZAe.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-CscpvIRR.mjs +26 -0
- package/dist/stream-broker/{hostInit-CIwilJUZ.mjs → hostInit-DJ3h-b1X.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B7VdNJlO.js → MaskShapeCanvas-DI4BY7W2-B0DyP46h.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-YyB4uLdH.js → MotionZonesSettings-NcxxQN8r-CzGSy_EO.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DfqixEad.js → PrivacyMaskSettings-APgPLF7p-CEgMcOOW.js} +1 -1
- package/embed-dist/assets/{index-zVLLVdIS.js → index-CHXOWKHF.js} +14 -14
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1zJmtUe.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-CSWUWZ8c.mjs +0 -26
|
@@ -3,10 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_step_definitions = require("../step-definitions-
|
|
6
|
+
const require_dist = require("../dist-BvFLZyhW.js");
|
|
7
|
+
const require_step_definitions = require("../step-definitions-gC-Ovglc.js");
|
|
8
8
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
9
9
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
|
|
10
|
+
let node_os = require("node:os");
|
|
11
|
+
node_os = require_chunk.__toESM(node_os);
|
|
10
12
|
let node_child_process = require("node:child_process");
|
|
11
13
|
let sharp = require("sharp");
|
|
12
14
|
sharp = require_chunk.__toESM(sharp);
|
|
@@ -14,8 +16,6 @@ let node_fs = require("node:fs");
|
|
|
14
16
|
node_fs = require_chunk.__toESM(node_fs);
|
|
15
17
|
let node_path = require("node:path");
|
|
16
18
|
node_path = require_chunk.__toESM(node_path);
|
|
17
|
-
let node_os = require("node:os");
|
|
18
|
-
node_os = require_chunk.__toESM(node_os);
|
|
19
19
|
//#region src/detection-pipeline/runtimes.ts
|
|
20
20
|
var KNOWN_PLATFORMS = [
|
|
21
21
|
"darwin",
|
|
@@ -81,14 +81,28 @@ function probedToHardwareInfo(hw) {
|
|
|
81
81
|
coral: coralInfoFrom(hw)
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* THE per-runtime pool provisioning registry — the single source for every
|
|
86
|
+
* pool's concurrency / worker count / batch tuning, keyed by the runtime THAT
|
|
87
|
+
* POOL runs. `engine-factory` used to carry a second, parallel copy of these
|
|
88
|
+
* defaults; it no longer does.
|
|
89
|
+
*
|
|
90
|
+
* `windowMs` / `maxBatchSize` are inert unless `batchMode !== 'none'`, and
|
|
91
|
+
* `numStreams` / `intraOpThreads` are inert at 0 (the runtime picks) — they are
|
|
92
|
+
* still spelled out per row so a reader never has to guess which default a
|
|
93
|
+
* missing key would have hit.
|
|
94
|
+
*/
|
|
84
95
|
var RUNTIME_DETAIL = {
|
|
85
96
|
onnx: {
|
|
86
97
|
label: "ONNX Runtime",
|
|
87
98
|
pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
|
|
88
99
|
tuning: {
|
|
89
100
|
concurrency: 4,
|
|
101
|
+
numWorkers: 1,
|
|
90
102
|
batchMode: "list",
|
|
103
|
+
windowMs: 2,
|
|
91
104
|
maxBatchSize: 8,
|
|
105
|
+
numStreams: 0,
|
|
92
106
|
intraOpThreads: 0
|
|
93
107
|
}
|
|
94
108
|
},
|
|
@@ -97,8 +111,12 @@ var RUNTIME_DETAIL = {
|
|
|
97
111
|
pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
|
|
98
112
|
tuning: {
|
|
99
113
|
concurrency: 1,
|
|
114
|
+
numWorkers: 1,
|
|
100
115
|
batchMode: "none",
|
|
101
|
-
|
|
116
|
+
windowMs: 2,
|
|
117
|
+
maxBatchSize: 8,
|
|
118
|
+
numStreams: 0,
|
|
119
|
+
intraOpThreads: 0
|
|
102
120
|
}
|
|
103
121
|
},
|
|
104
122
|
coreml: {
|
|
@@ -106,10 +124,12 @@ var RUNTIME_DETAIL = {
|
|
|
106
124
|
pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
|
|
107
125
|
tuning: {
|
|
108
126
|
concurrency: 1,
|
|
127
|
+
numWorkers: 1,
|
|
109
128
|
batchMode: "none",
|
|
110
129
|
windowMs: 8,
|
|
111
130
|
maxBatchSize: 8,
|
|
112
|
-
|
|
131
|
+
numStreams: 0,
|
|
132
|
+
intraOpThreads: 0
|
|
113
133
|
}
|
|
114
134
|
},
|
|
115
135
|
edgetpu: {
|
|
@@ -117,8 +137,12 @@ var RUNTIME_DETAIL = {
|
|
|
117
137
|
pythonRequirements: ["requirements.txt", "requirements-edgetpu.txt"],
|
|
118
138
|
tuning: {
|
|
119
139
|
concurrency: 1,
|
|
140
|
+
numWorkers: 1,
|
|
120
141
|
batchMode: "none",
|
|
121
|
-
|
|
142
|
+
windowMs: 2,
|
|
143
|
+
maxBatchSize: 8,
|
|
144
|
+
numStreams: 0,
|
|
145
|
+
intraOpThreads: 0
|
|
122
146
|
}
|
|
123
147
|
}
|
|
124
148
|
};
|
|
@@ -161,6 +185,12 @@ function modelFormatFor(id) {
|
|
|
161
185
|
function pythonRequirementsFor(id) {
|
|
162
186
|
return RUNTIME_DETAIL[id].pythonRequirements;
|
|
163
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The pool provisioning for ONE runtime. Callers must pass the runtime of the
|
|
190
|
+
* pool they are provisioning — never the node's default runtime. See
|
|
191
|
+
* `engine/pool-provisioning.ts` for the resolver that layers an operator
|
|
192
|
+
* override on top of this.
|
|
193
|
+
*/
|
|
164
194
|
function tuningFor(id) {
|
|
165
195
|
return RUNTIME_DETAIL[id].tuning;
|
|
166
196
|
}
|
|
@@ -379,6 +409,62 @@ var RAW_FMT_CODE = {
|
|
|
379
409
|
* large frame) never trips; override via env for constrained hardware.
|
|
380
410
|
*/
|
|
381
411
|
var POOL_INFER_TIMEOUT_MS = Math.max(1e3, Number(process.env["CAMSTACK_POOL_INFER_TIMEOUT_MS"]) || 6e4);
|
|
412
|
+
/**
|
|
413
|
+
* Max inference requests outstanding to ONE worker before new ones are SHED.
|
|
414
|
+
*
|
|
415
|
+
* Without this the camera dies permanently the first time the worker gets
|
|
416
|
+
* slow, and it did: device 615 produced nothing for 52 minutes on 2026-07-31.
|
|
417
|
+
* The worker was neither dead nor deadlocked — it was replying at full rate,
|
|
418
|
+
* about a minute behind, and every reply landed after {@link
|
|
419
|
+
* POOL_INFER_TIMEOUT_MS} had already abandoned its request. The proof is in the
|
|
420
|
+
* logs: a steady drip of `Response for unknown request id`, request ids
|
|
421
|
+
* climbing ~13/s, for as long as the outage lasted.
|
|
422
|
+
*
|
|
423
|
+
* That state is self-sustaining and cannot drain, because frames keep arriving
|
|
424
|
+
* while every one of them is written straight to the pipe. `dispatch` had NO
|
|
425
|
+
* bound: a transient slowdown (inference went 40ms → 2500ms across several
|
|
426
|
+
* cameras at 16:19 that day) buys a backlog that is never paid off, and once
|
|
427
|
+
* the backlog exceeds the deadline the loss is total.
|
|
428
|
+
*
|
|
429
|
+
* The queue is therefore capped in TIME, not just in count: at the normal ~40ms
|
|
430
|
+
* per inference this is ~0.3s of work, and even at a 20× slowdown it stays well
|
|
431
|
+
* inside the 60s deadline — so a worker that falls behind sheds, catches up,
|
|
432
|
+
* and recovers on its own. Python already sheds under its own per-model bound
|
|
433
|
+
* and answers `{dropped: true}`; this is the same contract applied one layer
|
|
434
|
+
* earlier, where the unbounded queue actually is.
|
|
435
|
+
*/
|
|
436
|
+
var POOL_MAX_IN_FLIGHT = Math.max(1, Number(process.env["CAMSTACK_POOL_MAX_IN_FLIGHT"]) || 8);
|
|
437
|
+
/**
|
|
438
|
+
* Reply deadline for a LIVE inference request (ms), as opposed to the generous
|
|
439
|
+
* {@link POOL_INFER_TIMEOUT_MS} that model loads and commands keep.
|
|
440
|
+
*
|
|
441
|
+
* 60 seconds was never a deadline for live detection — it is three orders of
|
|
442
|
+
* magnitude past the point where the answer is worth having. Frames arrive at
|
|
443
|
+
* ~13/s; a detection that lands 60s late describes a scene that no longer
|
|
444
|
+
* exists, and the only thing the wait accomplished was holding a queue slot
|
|
445
|
+
* while the backlog grew. Operator directive 2026-07-31: "a few seconds are
|
|
446
|
+
* already too many for this scenario".
|
|
447
|
+
*
|
|
448
|
+
* Three seconds is deliberately still loose — normal inference on this fleet is
|
|
449
|
+
* 35-47ms, so this trips only at a ~70× slowdown — but it bounds the damage at
|
|
450
|
+
* seconds instead of a minute, and combined with {@link POOL_MAX_IN_FLIGHT} it
|
|
451
|
+
* makes the backlog mathematically unable to outrun the deadline: the queue can
|
|
452
|
+
* never hold more than `cap × deadline` of work.
|
|
453
|
+
*
|
|
454
|
+
* Commands and model loads keep the long timeout on purpose: a cold model load
|
|
455
|
+
* legitimately takes tens of seconds, and shedding or timing one out
|
|
456
|
+
* desynchronizes the worker's model slots.
|
|
457
|
+
*/
|
|
458
|
+
var POOL_LIVE_INFER_TIMEOUT_MS = Math.max(250, Number(process.env["CAMSTACK_POOL_LIVE_INFER_TIMEOUT_MS"]) || 3e3);
|
|
459
|
+
/** Opcodes the in-flight cap may shed. Commands and frame-cache writes are
|
|
460
|
+
* NEVER shed: a dropped command silently desynchronizes model state, and a
|
|
461
|
+
* dropped `cacheFrame` strands the `inferCached` that follows it. */
|
|
462
|
+
var SHEDDABLE_MSG_TYPES = new Set([
|
|
463
|
+
MSG_INFER_JPEG,
|
|
464
|
+
MSG_INFER_RAW,
|
|
465
|
+
MSG_INFER_BATCH,
|
|
466
|
+
MSG_INFER_CACHED
|
|
467
|
+
]);
|
|
382
468
|
var PoolHandle = class {
|
|
383
469
|
pool;
|
|
384
470
|
modelIndex;
|
|
@@ -458,6 +544,13 @@ var PoolWorker = class {
|
|
|
458
544
|
process = null;
|
|
459
545
|
receiveBuffer = Buffer.alloc(0);
|
|
460
546
|
pending = /* @__PURE__ */ new Map();
|
|
547
|
+
/** True while the in-flight cap is shedding — so the warning fires on the
|
|
548
|
+
* EDGE rather than once per dropped frame. */
|
|
549
|
+
shedding = false;
|
|
550
|
+
/** Inference requests shed by the in-flight cap, for this worker's lifetime.
|
|
551
|
+
* Surfaced so "the camera is quiet" and "the camera is saturated" can be
|
|
552
|
+
* told apart without reading the logs. */
|
|
553
|
+
shedCount = 0;
|
|
461
554
|
nextRequestId = 1;
|
|
462
555
|
ready = false;
|
|
463
556
|
log;
|
|
@@ -469,6 +562,15 @@ var PoolWorker = class {
|
|
|
469
562
|
getPid() {
|
|
470
563
|
return this.process?.pid ?? null;
|
|
471
564
|
}
|
|
565
|
+
/** Requests written and not yet answered. This is the BACKLOG — the number
|
|
566
|
+
* nothing measured while a camera sat at zero frames for 52 minutes. */
|
|
567
|
+
getInFlight() {
|
|
568
|
+
return this.pending.size;
|
|
569
|
+
}
|
|
570
|
+
/** Inference shed by the in-flight cap over this worker's lifetime. */
|
|
571
|
+
getShedCount() {
|
|
572
|
+
return this.shedCount;
|
|
573
|
+
}
|
|
472
574
|
isReady() {
|
|
473
575
|
return this.ready;
|
|
474
576
|
}
|
|
@@ -617,10 +719,51 @@ var PoolWorker = class {
|
|
|
617
719
|
this.ready = false;
|
|
618
720
|
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
619
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Shed an inference request when the worker already has {@link
|
|
724
|
+
* POOL_MAX_IN_FLIGHT} outstanding — the backlog is what turns a slow worker
|
|
725
|
+
* into a dead camera. Returns the shed response, or `null` to proceed.
|
|
726
|
+
*
|
|
727
|
+
* A shed is LOUD the first time and then rate-limited: at frame rate this
|
|
728
|
+
* fires ~13×/s, so logging every one would bury the signal it is meant to
|
|
729
|
+
* raise — but logging none is how a camera went dark for 52 minutes looking
|
|
730
|
+
* like a quiet afternoon.
|
|
731
|
+
*/
|
|
732
|
+
shedIfSaturated(msgType) {
|
|
733
|
+
if (!SHEDDABLE_MSG_TYPES.has(msgType)) return null;
|
|
734
|
+
if (this.pending.size < POOL_MAX_IN_FLIGHT) {
|
|
735
|
+
this.shedding = false;
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
if (!this.shedding) {
|
|
739
|
+
this.shedding = true;
|
|
740
|
+
this.log.warn("worker saturated — shedding inference until it catches up", { meta: {
|
|
741
|
+
worker: this.opts.workerLabel,
|
|
742
|
+
pid: this.getPid(),
|
|
743
|
+
runtime: this.opts.poolRuntime,
|
|
744
|
+
device: this.opts.device ?? "default",
|
|
745
|
+
inFlight: this.pending.size,
|
|
746
|
+
cap: POOL_MAX_IN_FLIGHT,
|
|
747
|
+
deadlineMs: POOL_LIVE_INFER_TIMEOUT_MS
|
|
748
|
+
} });
|
|
749
|
+
}
|
|
750
|
+
this.shedCount++;
|
|
751
|
+
return {
|
|
752
|
+
dropped: true,
|
|
753
|
+
shedReason: "worker-in-flight-cap"
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/** Live inference gets seconds; commands and model loads keep the long
|
|
757
|
+
* timeout — see {@link POOL_LIVE_INFER_TIMEOUT_MS}. */
|
|
758
|
+
deadlineFor(msgType) {
|
|
759
|
+
return SHEDDABLE_MSG_TYPES.has(msgType) ? POOL_LIVE_INFER_TIMEOUT_MS : POOL_INFER_TIMEOUT_MS;
|
|
760
|
+
}
|
|
620
761
|
dispatch(msgType, payload) {
|
|
762
|
+
const shed = this.shedIfSaturated(msgType);
|
|
763
|
+
if (shed) return Promise.resolve(shed);
|
|
621
764
|
const reqId = this.allocRequestId();
|
|
622
765
|
return new Promise((resolve, reject) => {
|
|
623
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
766
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
624
767
|
this.pending.set(reqId, {
|
|
625
768
|
resolve,
|
|
626
769
|
reject,
|
|
@@ -640,24 +783,30 @@ var PoolWorker = class {
|
|
|
640
783
|
* `POOL_INFER_TIMEOUT_MS`. `unref` so it never keeps the event loop alive.
|
|
641
784
|
* The normal response + `rejectAll` paths clear it via `PendingRequest.timer`.
|
|
642
785
|
*/
|
|
643
|
-
armRequestTimeout(reqId, reject) {
|
|
786
|
+
armRequestTimeout(reqId, reject, timeoutMs) {
|
|
644
787
|
const timer = setTimeout(() => {
|
|
645
788
|
if (this.pending.delete(reqId)) {
|
|
646
789
|
this.log.error("inference request timed out", { meta: {
|
|
647
790
|
worker: this.opts.workerLabel,
|
|
791
|
+
pid: this.getPid(),
|
|
792
|
+
runtime: this.opts.poolRuntime,
|
|
793
|
+
device: this.opts.device ?? "default",
|
|
794
|
+
inFlight: this.pending.size,
|
|
648
795
|
reqId,
|
|
649
796
|
timeoutMs: POOL_INFER_TIMEOUT_MS
|
|
650
797
|
} });
|
|
651
|
-
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${
|
|
798
|
+
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${timeoutMs}ms (worker alive, no reply)`));
|
|
652
799
|
}
|
|
653
|
-
},
|
|
800
|
+
}, timeoutMs);
|
|
654
801
|
timer.unref?.();
|
|
655
802
|
return timer;
|
|
656
803
|
}
|
|
657
804
|
dispatchMulti(msgType, payloadLen, parts) {
|
|
805
|
+
const shed = this.shedIfSaturated(msgType);
|
|
806
|
+
if (shed) return Promise.resolve(shed);
|
|
658
807
|
const reqId = this.allocRequestId();
|
|
659
808
|
return new Promise((resolve, reject) => {
|
|
660
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
809
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
661
810
|
this.pending.set(reqId, {
|
|
662
811
|
resolve,
|
|
663
812
|
reject,
|
|
@@ -763,6 +912,22 @@ var SharedInferencePool = class {
|
|
|
763
912
|
}
|
|
764
913
|
device;
|
|
765
914
|
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
915
|
+
/** Summed backlog and shed count across the pool's workers. A rising
|
|
916
|
+
* `inFlight` with a rising `shed` is a worker falling behind; a rising
|
|
917
|
+
* `inFlight` with zero `shed` and no results is the state that used to be
|
|
918
|
+
* invisible. */
|
|
919
|
+
getBacklog() {
|
|
920
|
+
let inFlight = 0;
|
|
921
|
+
let shed = 0;
|
|
922
|
+
for (const w of this.workers) {
|
|
923
|
+
inFlight += w.getInFlight();
|
|
924
|
+
shed += w.getShedCount();
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
inFlight,
|
|
928
|
+
shed
|
|
929
|
+
};
|
|
930
|
+
}
|
|
766
931
|
getPid() {
|
|
767
932
|
return this.workers[0]?.getPid() ?? null;
|
|
768
933
|
}
|
|
@@ -796,6 +961,7 @@ var SharedInferencePool = class {
|
|
|
796
961
|
loadedCount,
|
|
797
962
|
startupMs,
|
|
798
963
|
runtime: this.poolRuntime,
|
|
964
|
+
device: this.device ?? "default",
|
|
799
965
|
workers: this.numWorkers,
|
|
800
966
|
pythonWorkers: innerWorkers,
|
|
801
967
|
pids: this.getPids()
|
|
@@ -887,7 +1053,10 @@ var SharedInferencePool = class {
|
|
|
887
1053
|
async dispose() {
|
|
888
1054
|
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
889
1055
|
this.workers.length = 0;
|
|
890
|
-
this.log.info("Pool process(es) terminated"
|
|
1056
|
+
this.log.info("Pool process(es) terminated", { meta: {
|
|
1057
|
+
runtime: this.poolRuntime,
|
|
1058
|
+
device: this.device ?? "default"
|
|
1059
|
+
} });
|
|
891
1060
|
}
|
|
892
1061
|
/**
|
|
893
1062
|
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
@@ -900,7 +1069,9 @@ var SharedInferencePool = class {
|
|
|
900
1069
|
this.droppedResponseCount++;
|
|
901
1070
|
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
902
1071
|
modelIndex,
|
|
903
|
-
droppedTotal: this.droppedResponseCount
|
|
1072
|
+
droppedTotal: this.droppedResponseCount,
|
|
1073
|
+
runtime: this.poolRuntime,
|
|
1074
|
+
device: this.device ?? "default"
|
|
904
1075
|
} });
|
|
905
1076
|
}
|
|
906
1077
|
return result;
|
|
@@ -1236,6 +1407,126 @@ var PipelineModelManager = class {
|
|
|
1236
1407
|
}
|
|
1237
1408
|
};
|
|
1238
1409
|
//#endregion
|
|
1410
|
+
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1411
|
+
/**
|
|
1412
|
+
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1413
|
+
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1414
|
+
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1415
|
+
* than silently building a wrong pool. Format comes from the shared
|
|
1416
|
+
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1417
|
+
*/
|
|
1418
|
+
function resolveDeviceEngine(deviceKey) {
|
|
1419
|
+
const colon = deviceKey.indexOf(":");
|
|
1420
|
+
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1421
|
+
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1422
|
+
const format = require_dist.DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1423
|
+
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1424
|
+
return {
|
|
1425
|
+
runtime: "python",
|
|
1426
|
+
backend,
|
|
1427
|
+
format,
|
|
1428
|
+
device
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* The inverse of {@link resolveDeviceEngine}: the `<backend>:<device>` key that
|
|
1433
|
+
* names the pool an engine belongs to. Every pool-level log line carries this —
|
|
1434
|
+
* without it a default pool logs anonymously and six hours of logs cannot tell
|
|
1435
|
+
* "the NPU is unused" from "the NPU IS the anonymous default pool".
|
|
1436
|
+
* Falls back to the bare backend when the engine carries no device.
|
|
1437
|
+
*/
|
|
1438
|
+
function deviceKeyOf(engine) {
|
|
1439
|
+
return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
|
|
1440
|
+
}
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/detection-pipeline/engine/pool-provisioning.ts
|
|
1443
|
+
/** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
|
|
1444
|
+
var OVERRIDABLE_KEYS = [
|
|
1445
|
+
"concurrency",
|
|
1446
|
+
"numWorkers",
|
|
1447
|
+
"batchMode",
|
|
1448
|
+
"windowMs",
|
|
1449
|
+
"maxBatchSize",
|
|
1450
|
+
"numStreams",
|
|
1451
|
+
"intraOpThreads"
|
|
1452
|
+
];
|
|
1453
|
+
/** Backends that are not a runtime of their own and run on onnxruntime. */
|
|
1454
|
+
var BACKEND_TO_RUNTIME = {
|
|
1455
|
+
onnx: "onnx",
|
|
1456
|
+
cpu: "onnx",
|
|
1457
|
+
cuda: "onnx",
|
|
1458
|
+
openvino: "openvino",
|
|
1459
|
+
coreml: "coreml",
|
|
1460
|
+
edgetpu: "edgetpu"
|
|
1461
|
+
};
|
|
1462
|
+
/**
|
|
1463
|
+
* The registry runtime a device-pool backend provisions from. Unknown backends
|
|
1464
|
+
* floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
|
|
1465
|
+
* format, so a pool never comes up unprovisioned.
|
|
1466
|
+
*/
|
|
1467
|
+
function runtimeIdForBackend(backend) {
|
|
1468
|
+
return BACKEND_TO_RUNTIME[backend] ?? "onnx";
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Resolve the provisioning for a pool running `engine`, applying the precedence
|
|
1472
|
+
* documented at the top of this module. `engine.device` is not consulted today
|
|
1473
|
+
* (see the module doc) but is part of the signature so a bench-backed
|
|
1474
|
+
* per-device row lands here and nowhere else.
|
|
1475
|
+
*/
|
|
1476
|
+
function resolvePoolProvisioning(engine, override = {}) {
|
|
1477
|
+
const runtime = runtimeIdForBackend(engine.backend);
|
|
1478
|
+
const profile = tuningFor(runtime);
|
|
1479
|
+
return {
|
|
1480
|
+
runtime,
|
|
1481
|
+
overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
|
|
1482
|
+
concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
|
|
1483
|
+
numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
|
|
1484
|
+
batchMode: override.batchMode ?? profile.batchMode,
|
|
1485
|
+
windowMs: override.windowMs ?? profile.windowMs,
|
|
1486
|
+
maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
|
|
1487
|
+
numStreams: override.numStreams ?? profile.numStreams,
|
|
1488
|
+
intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function positiveNumber(value) {
|
|
1492
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1493
|
+
}
|
|
1494
|
+
function batchMode(value) {
|
|
1495
|
+
return value === "none" || value === "list" || value === "window" ? value : void 0;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Read the operator override out of the detection addon's settings. Only a
|
|
1499
|
+
* NON-AUTO value counts: the sliders persist `0` and the select persists `''`
|
|
1500
|
+
* for "Auto (backend default)", and those must resolve to the runtime's
|
|
1501
|
+
* registry value, not to a literal 0 worker threads.
|
|
1502
|
+
*
|
|
1503
|
+
* The result is deliberately sparse — an empty object means "nothing
|
|
1504
|
+
* overridden", which is what lets a per-device pool provision from its own
|
|
1505
|
+
* runtime instead of inheriting the node's.
|
|
1506
|
+
*/
|
|
1507
|
+
function readOperatorOverride(settings) {
|
|
1508
|
+
const override = {};
|
|
1509
|
+
const concurrency = positiveNumber(settings.concurrency);
|
|
1510
|
+
if (concurrency !== void 0) override.concurrency = concurrency;
|
|
1511
|
+
const numWorkers = positiveNumber(settings.numWorkers);
|
|
1512
|
+
if (numWorkers !== void 0) override.numWorkers = numWorkers;
|
|
1513
|
+
const mode = batchMode(settings.batchMode);
|
|
1514
|
+
if (mode !== void 0) override.batchMode = mode;
|
|
1515
|
+
const windowMs = positiveNumber(settings.windowMs);
|
|
1516
|
+
if (windowMs !== void 0) override.windowMs = windowMs;
|
|
1517
|
+
const maxBatchSize = positiveNumber(settings.maxBatchSize);
|
|
1518
|
+
if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
|
|
1519
|
+
const numStreams = positiveNumber(settings.numStreams);
|
|
1520
|
+
if (numStreams !== void 0) override.numStreams = numStreams;
|
|
1521
|
+
const intraOpThreads = positiveNumber(settings.intraOpThreads);
|
|
1522
|
+
if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
|
|
1523
|
+
return override;
|
|
1524
|
+
}
|
|
1525
|
+
/** Two overrides carry the same fields with the same values. */
|
|
1526
|
+
function overridesEqual(a, b) {
|
|
1527
|
+
return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
|
|
1528
|
+
}
|
|
1529
|
+
//#endregion
|
|
1239
1530
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
1240
1531
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
1241
1532
|
coreml: "coreml",
|
|
@@ -1305,9 +1596,31 @@ var EngineFactory = class {
|
|
|
1305
1596
|
poolManager = null;
|
|
1306
1597
|
log;
|
|
1307
1598
|
opts;
|
|
1599
|
+
/**
|
|
1600
|
+
* `<backend>:<device>` — on EVERY log line this factory writes. A pool that
|
|
1601
|
+
* logs anonymously cannot be told apart from another pool of the same runtime
|
|
1602
|
+
* on a different accelerator.
|
|
1603
|
+
*/
|
|
1604
|
+
deviceKey;
|
|
1605
|
+
/**
|
|
1606
|
+
* Resolved at CONSTRUCTION, not at pool spawn, so it is inspectable without
|
|
1607
|
+
* starting Python and so a pool that fails to spawn still logs what it was
|
|
1608
|
+
* trying to be.
|
|
1609
|
+
*/
|
|
1610
|
+
resolvedProvisioning;
|
|
1308
1611
|
constructor(opts) {
|
|
1309
1612
|
this.opts = opts;
|
|
1310
1613
|
this.log = opts.logger;
|
|
1614
|
+
this.deviceKey = deviceKeyOf(opts.engine);
|
|
1615
|
+
this.resolvedProvisioning = resolvePoolProvisioning(opts.engine, opts.provisioning);
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* The provisioning this factory's pool runs with — resolved from the pool's
|
|
1619
|
+
* OWN runtime plus any operator override. Read by the effective-tuning
|
|
1620
|
+
* readout and by the tests that pin the per-runtime behaviour.
|
|
1621
|
+
*/
|
|
1622
|
+
get provisioning() {
|
|
1623
|
+
return this.resolvedProvisioning;
|
|
1311
1624
|
}
|
|
1312
1625
|
/** Detection always uses the Python pool. */
|
|
1313
1626
|
get usesPythonPool() {
|
|
@@ -1409,40 +1722,51 @@ var EngineFactory = class {
|
|
|
1409
1722
|
this.poolManager = null;
|
|
1410
1723
|
}
|
|
1411
1724
|
}
|
|
1725
|
+
/**
|
|
1726
|
+
* The one pool-init log line. `deviceKey` / `device` are the reason it
|
|
1727
|
+
* exists: the default pool used to log neither, so an operator reading a
|
|
1728
|
+
* day of logs could not tell whether the NPU was idle or was simply the
|
|
1729
|
+
* anonymous default pool.
|
|
1730
|
+
*
|
|
1731
|
+
* `overriddenFields` replaces the old `source` field. `source` claimed
|
|
1732
|
+
* `'auto-default' | 'user-override'` about the CONCURRENCY TUNING but read as
|
|
1733
|
+
* the provenance of the device/engine choice, and was `'user-override'` on
|
|
1734
|
+
* every pool because the addon always passed explicit tuning. This lists the
|
|
1735
|
+
* fields the operator actually overrode — empty means pure registry.
|
|
1736
|
+
*/
|
|
1737
|
+
logProvisioning() {
|
|
1738
|
+
const p = this.resolvedProvisioning;
|
|
1739
|
+
this.log.info("Python inference pool", { meta: {
|
|
1740
|
+
deviceKey: this.deviceKey,
|
|
1741
|
+
backend: this.opts.engine.backend,
|
|
1742
|
+
device: this.opts.engine.device ?? null,
|
|
1743
|
+
runtime: p.runtime,
|
|
1744
|
+
concurrency: p.concurrency,
|
|
1745
|
+
numWorkers: p.numWorkers,
|
|
1746
|
+
overriddenFields: p.overriddenFields,
|
|
1747
|
+
tuning: {
|
|
1748
|
+
batchMode: p.batchMode,
|
|
1749
|
+
windowMs: p.windowMs,
|
|
1750
|
+
maxBatchSize: p.maxBatchSize,
|
|
1751
|
+
numStreams: p.numStreams,
|
|
1752
|
+
intraOpThreads: p.intraOpThreads
|
|
1753
|
+
}
|
|
1754
|
+
} });
|
|
1755
|
+
}
|
|
1412
1756
|
async initPythonPool(steps) {
|
|
1757
|
+
this.logProvisioning();
|
|
1413
1758
|
const pythonPath = this.opts.pythonPath;
|
|
1414
1759
|
if (!pythonPath) throw new Error("EngineFactory: pythonPath is required — the addon must call ctx.deps.ensurePython() and pass the result. The embedded portable Python download likely failed; check the addon logs for the download error.");
|
|
1415
1760
|
const poolRuntime = BACKEND_TO_POOL_RUNTIME[this.opts.engine.backend];
|
|
1416
1761
|
if (!poolRuntime) throw new Error(`No pool runtime mapping for backend "${this.opts.engine.backend}"`);
|
|
1417
|
-
const concurrency = this.
|
|
1418
|
-
coreml: 1,
|
|
1419
|
-
openvino: 1,
|
|
1420
|
-
onnxruntime: 4,
|
|
1421
|
-
edgetpu: 1
|
|
1422
|
-
}[poolRuntime];
|
|
1423
|
-
const isAutoDefault = this.opts.concurrency === void 0;
|
|
1424
|
-
const defaultBatchMode = {
|
|
1425
|
-
coreml: "window",
|
|
1426
|
-
openvino: "none",
|
|
1427
|
-
onnxruntime: "list",
|
|
1428
|
-
edgetpu: "none"
|
|
1429
|
-
};
|
|
1430
|
-
const tuning = this.opts.tuning ?? {};
|
|
1762
|
+
const { concurrency, numWorkers } = this.resolvedProvisioning;
|
|
1431
1763
|
const resolvedTuning = {
|
|
1432
|
-
batchMode:
|
|
1433
|
-
windowMs:
|
|
1434
|
-
maxBatchSize:
|
|
1435
|
-
numStreams:
|
|
1436
|
-
intraOpThreads:
|
|
1764
|
+
batchMode: this.resolvedProvisioning.batchMode,
|
|
1765
|
+
windowMs: this.resolvedProvisioning.windowMs,
|
|
1766
|
+
maxBatchSize: this.resolvedProvisioning.maxBatchSize,
|
|
1767
|
+
numStreams: this.resolvedProvisioning.numStreams,
|
|
1768
|
+
intraOpThreads: this.resolvedProvisioning.intraOpThreads
|
|
1437
1769
|
};
|
|
1438
|
-
const numWorkers = Math.max(1, this.opts.numWorkers ?? 1);
|
|
1439
|
-
this.log.info("Python inference pool", { meta: {
|
|
1440
|
-
runtime: poolRuntime,
|
|
1441
|
-
concurrency,
|
|
1442
|
-
numWorkers,
|
|
1443
|
-
source: isAutoDefault ? "auto-default" : "user-override",
|
|
1444
|
-
tuning: resolvedTuning
|
|
1445
|
-
} });
|
|
1446
1770
|
this.pool = new SharedInferencePool(pythonPath, poolRuntime, this.log.child("pool"), {
|
|
1447
1771
|
concurrency,
|
|
1448
1772
|
tuning: resolvedTuning,
|
|
@@ -1498,28 +1822,6 @@ var EngineFactory = class {
|
|
|
1498
1822
|
}
|
|
1499
1823
|
};
|
|
1500
1824
|
//#endregion
|
|
1501
|
-
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1502
|
-
/**
|
|
1503
|
-
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1504
|
-
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1505
|
-
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1506
|
-
* than silently building a wrong pool. Format comes from the shared
|
|
1507
|
-
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1508
|
-
*/
|
|
1509
|
-
function resolveDeviceEngine(deviceKey) {
|
|
1510
|
-
const colon = deviceKey.indexOf(":");
|
|
1511
|
-
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1512
|
-
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1513
|
-
const format = require_dist.DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1514
|
-
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1515
|
-
return {
|
|
1516
|
-
runtime: "python",
|
|
1517
|
-
backend,
|
|
1518
|
-
format,
|
|
1519
|
-
device
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
//#endregion
|
|
1523
1825
|
//#region src/detection-pipeline/engine/idle-pool-reaper.ts
|
|
1524
1826
|
var IdlePoolReaper = class {
|
|
1525
1827
|
lastUsed = /* @__PURE__ */ new Map();
|
|
@@ -2704,19 +3006,25 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
|
|
|
2704
3006
|
* DROPS detections outright, so it sits between that and a hard cut).
|
|
2705
3007
|
*/
|
|
2706
3008
|
var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
3009
|
+
/** A cut only makes sense in (0, 1); anything else is treated as OFF rather
|
|
3010
|
+
* than dropping every detection on a typo. */
|
|
3011
|
+
function isUsableHardAreaCut(ratio) {
|
|
3012
|
+
return Number.isFinite(ratio) && ratio > 0 && ratio < 1;
|
|
3013
|
+
}
|
|
2707
3014
|
/**
|
|
2708
3015
|
* True when `bbox` is a near-full-frame LOW-confidence phantom that should be
|
|
2709
3016
|
* DROPPED from the frame's detections (overlay / tracker / events / motion-wake).
|
|
2710
3017
|
*
|
|
2711
3018
|
* Rule (drop ⇔ returns true):
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
3019
|
+
* areaRatio ≥ `hardAreaRatio` ← score-independent, OFF by default
|
|
3020
|
+
* OR ( (areaRatio ≥ {@link FULL_FRAME_AREA_RATIO}
|
|
3021
|
+
* OR width ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameWidth
|
|
3022
|
+
* OR height ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameHeight)
|
|
3023
|
+
* AND score < `maxConfidence` )
|
|
2716
3024
|
*
|
|
2717
|
-
*
|
|
2718
|
-
* subject and
|
|
2719
|
-
* to reject on → not a phantom.
|
|
3025
|
+
* Below the hard cut a high-confidence detection (`score ≥ maxConfidence`) is
|
|
3026
|
+
* trusted as a real subject and survives. Degenerate frame dimensions (≤0)
|
|
3027
|
+
* carry no info to reject on → not a phantom.
|
|
2720
3028
|
*
|
|
2721
3029
|
* @param bbox absolute-pixel `[x1, y1, x2, y2]` box in source-frame coords.
|
|
2722
3030
|
* @param frameWidth source frame width in px.
|
|
@@ -2725,10 +3033,26 @@ var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
|
2725
3033
|
* @param maxConfidence confidence bar below which a full-frame box is a phantom
|
|
2726
3034
|
* (defaults to {@link FULL_FRAME_MAX_CONFIDENCE}; overridable by the caller so
|
|
2727
3035
|
* the bar can be threaded from detection settings without new cap methods).
|
|
3036
|
+
* @param hardAreaRatio area fraction at/above which the box is dropped NO
|
|
3037
|
+
* MATTER the score. Defaults to {@link HARD_AREA_CUT_DISABLED} (off) — it is
|
|
3038
|
+
* armed per camera, because a doorbell close-up legitimately fills the frame
|
|
3039
|
+
* while an overhead courtyard box never does. Threaded from step settings as
|
|
3040
|
+
* `fullFrameGuardHardAreaRatio`.
|
|
3041
|
+
*/
|
|
3042
|
+
/**
|
|
3043
|
+
* True when a box trips the guard's GEOMETRY test (near-full-frame, or
|
|
3044
|
+
* edge-to-edge in one axis) but SURVIVES because its score cleared the
|
|
3045
|
+
* confidence bar.
|
|
3046
|
+
*
|
|
3047
|
+
* These are the ones worth counting: on 2026-07-30 device 615 produced six
|
|
3048
|
+
* such boxes in a day, at 0.710-0.787, and they were only found by an offline
|
|
3049
|
+
* sweep of stored events — nothing in production reported them. Arming the
|
|
3050
|
+
* hard-area cut on a camera should be a decision made from this count, not
|
|
3051
|
+
* from one incident.
|
|
2728
3052
|
*/
|
|
2729
|
-
function
|
|
3053
|
+
function isFullFrameSurvivor(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE) {
|
|
2730
3054
|
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
2731
|
-
if (score
|
|
3055
|
+
if (score < maxConfidence) return false;
|
|
2732
3056
|
const [x1, y1, x2, y2] = bbox;
|
|
2733
3057
|
const w = x2 - x1;
|
|
2734
3058
|
const h = y2 - y1;
|
|
@@ -2737,6 +3061,19 @@ function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxCo
|
|
|
2737
3061
|
if (h >= frameHeight * .98) return true;
|
|
2738
3062
|
return false;
|
|
2739
3063
|
}
|
|
3064
|
+
function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE, hardAreaRatio = 1) {
|
|
3065
|
+
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
3066
|
+
const [x1, y1, x2, y2] = bbox;
|
|
3067
|
+
const w = x2 - x1;
|
|
3068
|
+
const h = y2 - y1;
|
|
3069
|
+
const areaRatio = w * h / (frameWidth * frameHeight);
|
|
3070
|
+
if (isUsableHardAreaCut(hardAreaRatio) && areaRatio >= hardAreaRatio) return true;
|
|
3071
|
+
if (score >= maxConfidence) return false;
|
|
3072
|
+
if (areaRatio >= .9) return true;
|
|
3073
|
+
if (w >= frameWidth * .98) return true;
|
|
3074
|
+
if (h >= frameHeight * .98) return true;
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
2740
3077
|
//#endregion
|
|
2741
3078
|
//#region src/detection-pipeline/pipeline/plate-deskew.ts
|
|
2742
3079
|
/**
|
|
@@ -3584,10 +3921,34 @@ var PipelineExecutor = class {
|
|
|
3584
3921
|
continue;
|
|
3585
3922
|
}
|
|
3586
3923
|
const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
|
|
3587
|
-
|
|
3588
|
-
|
|
3924
|
+
const fullFrameHardArea = rootStep.settings?.["fullFrameGuardHardAreaRatio"];
|
|
3925
|
+
if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0, typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0)) {
|
|
3926
|
+
this.opts.logger?.info("full-frame phantom guard: detection dropped", {
|
|
3927
|
+
tags: { deviceId },
|
|
3928
|
+
meta: {
|
|
3929
|
+
macroClass: mutable.macroClass,
|
|
3930
|
+
score: mutable.score,
|
|
3931
|
+
bbox: mutable.bbox,
|
|
3932
|
+
frameWidth: imageWidth,
|
|
3933
|
+
frameHeight: imageHeight,
|
|
3934
|
+
areaRatio: imageWidth > 0 && imageHeight > 0 ? Math.round((mutable.bbox[2] - mutable.bbox[0]) * (mutable.bbox[3] - mutable.bbox[1]) / (imageWidth * imageHeight) * 1e3) / 1e3 : void 0,
|
|
3935
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3936
|
+
hardAreaRatio: typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3589
3939
|
continue;
|
|
3590
3940
|
}
|
|
3941
|
+
if (isFullFrameSurvivor(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0)) this.opts.logger?.warn("full-frame box SURVIVED the guard on score", {
|
|
3942
|
+
tags: { deviceId },
|
|
3943
|
+
meta: {
|
|
3944
|
+
macroClass: mutable.macroClass,
|
|
3945
|
+
score: mutable.score,
|
|
3946
|
+
bbox: mutable.bbox,
|
|
3947
|
+
areaRatio: imageWidth > 0 && imageHeight > 0 ? Math.round((mutable.bbox[2] - mutable.bbox[0]) * (mutable.bbox[3] - mutable.bbox[1]) / (imageWidth * imageHeight) * 1e3) / 1e3 : void 0,
|
|
3948
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3949
|
+
hint: "arm fullFrameGuardHardAreaRatio on this camera if these are phantoms"
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3591
3952
|
try {
|
|
3592
3953
|
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3593
3954
|
} catch (err) {
|
|
@@ -4902,8 +5263,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4902
5263
|
modelsDir: this.modelsDir,
|
|
4903
5264
|
logger: this.log.child("engine-verify"),
|
|
4904
5265
|
pythonPath: this.executorOptions.pythonPath,
|
|
4905
|
-
|
|
4906
|
-
|
|
5266
|
+
provisioning: {
|
|
5267
|
+
concurrency: 1,
|
|
5268
|
+
numWorkers: 1
|
|
5269
|
+
}
|
|
4907
5270
|
});
|
|
4908
5271
|
try {
|
|
4909
5272
|
await probe.initialize([]);
|
|
@@ -5659,9 +6022,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5659
6022
|
modelsDir: this.modelsDir,
|
|
5660
6023
|
logger: this.log.child("engine-override"),
|
|
5661
6024
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5662
|
-
|
|
5663
|
-
tuning: this.executorOptions.tuning,
|
|
5664
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6025
|
+
provisioning: this.executorOptions.provisioning
|
|
5665
6026
|
});
|
|
5666
6027
|
const initPromise = newFactory.initialize([]);
|
|
5667
6028
|
this.overrideCache = {
|
|
@@ -5841,9 +6202,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5841
6202
|
modelsDir: this.modelsDir,
|
|
5842
6203
|
logger: this.log.child("engine-override"),
|
|
5843
6204
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5844
|
-
|
|
5845
|
-
tuning: this.executorOptions.tuning,
|
|
5846
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6205
|
+
provisioning: this.executorOptions.provisioning
|
|
5847
6206
|
});
|
|
5848
6207
|
const initPromise = newFactory.initialize([]);
|
|
5849
6208
|
this.overrideCache = {
|
|
@@ -6375,13 +6734,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6375
6734
|
if (!this.engineFactory) return;
|
|
6376
6735
|
await this.engineFactory.uncacheFrame(input.frameId);
|
|
6377
6736
|
}
|
|
6737
|
+
/**
|
|
6738
|
+
* The tuning of the node's DEFAULT pool. Per-device pools each resolve their
|
|
6739
|
+
* own runtime's row, so this is not "the node's tuning" — the per-pool truth
|
|
6740
|
+
* is on each pool's `Python inference pool` log line and in `listLoadedEngines`.
|
|
6741
|
+
*/
|
|
6378
6742
|
async getEffectiveTuning() {
|
|
6379
|
-
const
|
|
6743
|
+
const p = resolvePoolProvisioning(this.currentEngine, this.executorOptions.provisioning);
|
|
6380
6744
|
return {
|
|
6381
|
-
batchMode:
|
|
6382
|
-
windowMs:
|
|
6383
|
-
maxBatchSize:
|
|
6384
|
-
concurrency:
|
|
6745
|
+
batchMode: p.batchMode,
|
|
6746
|
+
windowMs: p.windowMs,
|
|
6747
|
+
maxBatchSize: p.maxBatchSize,
|
|
6748
|
+
concurrency: p.concurrency
|
|
6385
6749
|
};
|
|
6386
6750
|
}
|
|
6387
6751
|
/**
|
|
@@ -6399,9 +6763,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6399
6763
|
modelsDir: this.modelsDir,
|
|
6400
6764
|
logger: this.log.child("engine"),
|
|
6401
6765
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6402
|
-
|
|
6403
|
-
tuning: this.executorOptions.tuning,
|
|
6404
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6766
|
+
provisioning: this.executorOptions.provisioning
|
|
6405
6767
|
});
|
|
6406
6768
|
await this.engineFactory.initialize([]);
|
|
6407
6769
|
}
|
|
@@ -6440,13 +6802,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6440
6802
|
modelsDir: this.modelsDir,
|
|
6441
6803
|
logger: this.log.child(`engine:${deviceKey}`),
|
|
6442
6804
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6443
|
-
|
|
6444
|
-
tuning: this.executorOptions.tuning,
|
|
6445
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6805
|
+
provisioning: this.executorOptions.provisioning
|
|
6446
6806
|
});
|
|
6447
6807
|
try {
|
|
6448
6808
|
await factory.initialize([]);
|
|
6449
6809
|
} catch (err) {
|
|
6810
|
+
this.log.error("Device pool failed to initialize", { meta: {
|
|
6811
|
+
deviceKey,
|
|
6812
|
+
backend: engine.backend,
|
|
6813
|
+
device: engine.device ?? null,
|
|
6814
|
+
provisioning: factory.provisioning,
|
|
6815
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6816
|
+
} });
|
|
6450
6817
|
await factory.dispose().catch(() => void 0);
|
|
6451
6818
|
throw err;
|
|
6452
6819
|
}
|
|
@@ -7057,21 +7424,22 @@ function toNodePlatform(value) {
|
|
|
7057
7424
|
return value === "darwin" || value === "linux" || value === "win32" ? value : void 0;
|
|
7058
7425
|
}
|
|
7059
7426
|
/**
|
|
7060
|
-
*
|
|
7061
|
-
*
|
|
7062
|
-
*
|
|
7063
|
-
* these and rebuilds the provider when they flip.
|
|
7427
|
+
* Slider keys on the "Inference executor" section that map 1:1 onto a
|
|
7428
|
+
* {@link RuntimePoolProfile} field. Used to inject the node-default runtime's
|
|
7429
|
+
* registry value as the field's suggested default in `getGlobalSettings`.
|
|
7064
7430
|
*/
|
|
7065
|
-
var
|
|
7431
|
+
var PROFILE_SLIDER_KEYS = [
|
|
7066
7432
|
"concurrency",
|
|
7067
|
-
"
|
|
7433
|
+
"numWorkers",
|
|
7068
7434
|
"windowMs",
|
|
7069
7435
|
"maxBatchSize",
|
|
7070
7436
|
"numStreams",
|
|
7071
|
-
"intraOpThreads"
|
|
7072
|
-
"numWorkers"
|
|
7437
|
+
"intraOpThreads"
|
|
7073
7438
|
];
|
|
7074
|
-
|
|
7439
|
+
function isProfileSliderKey(key) {
|
|
7440
|
+
return PROFILE_SLIDER_KEYS.some((k) => k === key);
|
|
7441
|
+
}
|
|
7442
|
+
var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
7075
7443
|
provider = null;
|
|
7076
7444
|
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
7077
7445
|
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
@@ -7086,14 +7454,17 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7086
7454
|
* ENGINE_METRICS_HEARTBEAT_MS for liveness. */
|
|
7087
7455
|
lastEmittedEngineSnapshot = null;
|
|
7088
7456
|
/**
|
|
7089
|
-
* Last-applied
|
|
7090
|
-
*
|
|
7091
|
-
*
|
|
7092
|
-
*
|
|
7093
|
-
*
|
|
7094
|
-
*
|
|
7457
|
+
* Last-applied OPERATOR OVERRIDE, taken at the end of `onInitialize` and
|
|
7458
|
+
* refreshed at the end of `onConfigChanged`. Lets the change handler diff the
|
|
7459
|
+
* current config against the snapshot and decide whether a pool respawn is
|
|
7460
|
+
* necessary — most config edits (engine cascade, audio settings) don't touch
|
|
7461
|
+
* the tuning fields and don't deserve a multi-second model reload.
|
|
7462
|
+
*
|
|
7463
|
+
* The per-runtime registry values are compile-time constants resolved inside
|
|
7464
|
+
* each pool, so they can never be the reason for a respawn; only an operator
|
|
7465
|
+
* edit can.
|
|
7095
7466
|
*/
|
|
7096
|
-
|
|
7467
|
+
lastAppliedOverride = {};
|
|
7097
7468
|
/**
|
|
7098
7469
|
* Embedded Python path resolved once at boot via
|
|
7099
7470
|
* `ctx.deps.ensurePython()`. Empty string means the download failed
|
|
@@ -7261,17 +7632,16 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7261
7632
|
...section,
|
|
7262
7633
|
fields: section.fields.map((field) => {
|
|
7263
7634
|
if (field.type === "select") {
|
|
7264
|
-
if (field.key === "batchMode"
|
|
7635
|
+
if (field.key === "batchMode") return {
|
|
7265
7636
|
...field,
|
|
7266
|
-
default: tuning
|
|
7637
|
+
default: tuning.batchMode
|
|
7267
7638
|
};
|
|
7268
7639
|
}
|
|
7269
7640
|
if (field.type === "slider" && "key" in field) {
|
|
7270
|
-
const tuned = tuning[field.key];
|
|
7271
7641
|
const sliderField = field;
|
|
7272
|
-
let patchedField =
|
|
7642
|
+
let patchedField = isProfileSliderKey(sliderField.key) ? {
|
|
7273
7643
|
...sliderField,
|
|
7274
|
-
default:
|
|
7644
|
+
default: tuning[sliderField.key]
|
|
7275
7645
|
} : sliderField;
|
|
7276
7646
|
if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
|
|
7277
7647
|
...patchedField,
|
|
@@ -7380,60 +7750,39 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7380
7750
|
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
7381
7751
|
}
|
|
7382
7752
|
/**
|
|
7383
|
-
*
|
|
7753
|
+
* The operator's explicit deviations from the per-runtime registry — and
|
|
7754
|
+
* NOTHING else.
|
|
7384
7755
|
*
|
|
7385
|
-
*
|
|
7386
|
-
*
|
|
7387
|
-
*
|
|
7388
|
-
*
|
|
7389
|
-
*
|
|
7756
|
+
* This used to be `resolveBackendTuning()`: it read `tuningFor(<the node's
|
|
7757
|
+
* default backend>)` and handed the whole resolved row to the provider, which
|
|
7758
|
+
* passed it verbatim to every pool it built. That is how an `openvino:npu`
|
|
7759
|
+
* pool on an onnx-default node came up with onnx's `concurrency: 4`. The
|
|
7760
|
+
* per-runtime values now resolve inside each pool
|
|
7761
|
+
* (`engine/pool-provisioning.ts`), keyed by that pool's own runtime, so
|
|
7762
|
+
* nothing backend-shaped may be computed here.
|
|
7390
7763
|
*
|
|
7391
|
-
*
|
|
7392
|
-
*
|
|
7393
|
-
*
|
|
7764
|
+
* Precedence, in full: an operator override below wins over the pool's
|
|
7765
|
+
* runtime registry row; an "Auto" field (slider `0`, select `''`) is not an
|
|
7766
|
+
* override. An override is node-wide by design — the operator set it against
|
|
7767
|
+
* a node, not against a runtime.
|
|
7394
7768
|
*/
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
|
|
7398
|
-
const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
|
|
7399
|
-
return {
|
|
7400
|
-
concurrency: num(t["concurrency"], 1),
|
|
7401
|
-
batchMode: batch(t["batchMode"], "window"),
|
|
7402
|
-
windowMs: num(t["windowMs"], 2),
|
|
7403
|
-
maxBatchSize: num(t["maxBatchSize"], 8),
|
|
7404
|
-
numStreams: num(t["numStreams"], 0),
|
|
7405
|
-
intraOpThreads: num(t["intraOpThreads"], 0),
|
|
7406
|
-
numWorkers: num(t["numWorkers"], 1)
|
|
7407
|
-
};
|
|
7769
|
+
resolveOperatorOverride() {
|
|
7770
|
+
return readOperatorOverride(this.config);
|
|
7408
7771
|
}
|
|
7409
7772
|
/**
|
|
7410
|
-
* Construct a fresh provider
|
|
7411
|
-
* truth for the provider wiring — shared by `onInitialize
|
|
7412
|
-
*
|
|
7773
|
+
* Construct a fresh provider carrying the operator override. Single source of
|
|
7774
|
+
* truth for the provider wiring — shared by `onInitialize` and the
|
|
7775
|
+
* `onConfigChanged` pool respawn. NOTHING backend-derived is passed: each
|
|
7776
|
+
* pool resolves its own runtime's registry row.
|
|
7413
7777
|
*/
|
|
7414
|
-
buildProvider(modelsDir,
|
|
7778
|
+
buildProvider(modelsDir, override) {
|
|
7415
7779
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
7416
7780
|
return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
|
|
7417
|
-
|
|
7418
|
-
tuning: {
|
|
7419
|
-
batchMode: tuning.batchMode,
|
|
7420
|
-
windowMs: tuning.windowMs,
|
|
7421
|
-
maxBatchSize: tuning.maxBatchSize,
|
|
7422
|
-
numStreams: tuning.numStreams,
|
|
7423
|
-
intraOpThreads: tuning.intraOpThreads
|
|
7424
|
-
},
|
|
7425
|
-
numWorkers: tuning.numWorkers,
|
|
7781
|
+
provisioning: override,
|
|
7426
7782
|
pythonPath: this.pythonPath,
|
|
7427
7783
|
pythonAddonDir: this.pythonAddonDir
|
|
7428
7784
|
});
|
|
7429
7785
|
}
|
|
7430
|
-
/**
|
|
7431
|
-
* Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
|
|
7432
|
-
* post-resolve / config-change respawn is actually needed).
|
|
7433
|
-
*/
|
|
7434
|
-
static poolTuningEqual(a, b) {
|
|
7435
|
-
return a.concurrency === b.concurrency && a.batchMode === b.batchMode && a.windowMs === b.windowMs && a.maxBatchSize === b.maxBatchSize && a.numStreams === b.numStreams && a.intraOpThreads === b.intraOpThreads && a.numWorkers === b.numWorkers;
|
|
7436
|
-
}
|
|
7437
7786
|
async onInitialize() {
|
|
7438
7787
|
const modelsDir = await this.resolveModelsDir();
|
|
7439
7788
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
@@ -7442,23 +7791,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7442
7791
|
if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
|
|
7443
7792
|
else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
|
|
7444
7793
|
await this.proactivelyInstallOpenvino();
|
|
7445
|
-
|
|
7446
|
-
this.provider = this.buildProvider(modelsDir, initialTuning);
|
|
7794
|
+
this.provider = this.buildProvider(modelsDir, this.resolveOperatorOverride());
|
|
7447
7795
|
await this.provider.init();
|
|
7448
7796
|
await this.provider.setApi(this.ctx);
|
|
7449
|
-
const resolvedTuning = this.resolveBackendTuning();
|
|
7450
|
-
if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
|
|
7451
|
-
this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
|
|
7452
|
-
from: initialTuning,
|
|
7453
|
-
to: resolvedTuning
|
|
7454
|
-
} });
|
|
7455
|
-
this.provider = this.buildProvider(modelsDir, resolvedTuning);
|
|
7456
|
-
await this.provider.init();
|
|
7457
|
-
await this.provider.setApi(this.ctx);
|
|
7458
|
-
}
|
|
7459
7797
|
await this.provider.warmPool();
|
|
7460
7798
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
7461
|
-
this.
|
|
7799
|
+
this.lastAppliedOverride = this.resolveOperatorOverride();
|
|
7462
7800
|
return [{
|
|
7463
7801
|
capability: require_dist.pipelineExecutorCapability,
|
|
7464
7802
|
provider: this.provider
|
|
@@ -7544,36 +7882,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7544
7882
|
}
|
|
7545
7883
|
}
|
|
7546
7884
|
/**
|
|
7547
|
-
* Snapshot the pool-bound subset of the EFFECTIVE tuning (post-
|
|
7548
|
-
* `resolveBackendTuning`). Stored config values for these fields are
|
|
7549
|
-
* ignored, so this snapshot only changes when `engineBackend` flips
|
|
7550
|
-
* onto a different `tuningFor` row.
|
|
7551
|
-
*/
|
|
7552
|
-
snapshotPoolConfig() {
|
|
7553
|
-
const t = this.resolveBackendTuning();
|
|
7554
|
-
return {
|
|
7555
|
-
concurrency: t.concurrency ?? 0,
|
|
7556
|
-
batchMode: t.batchMode,
|
|
7557
|
-
windowMs: t.windowMs,
|
|
7558
|
-
maxBatchSize: t.maxBatchSize,
|
|
7559
|
-
numStreams: t.numStreams,
|
|
7560
|
-
intraOpThreads: t.intraOpThreads,
|
|
7561
|
-
numWorkers: t.numWorkers
|
|
7562
|
-
};
|
|
7563
|
-
}
|
|
7564
|
-
poolConfigChanged() {
|
|
7565
|
-
const next = this.snapshotPoolConfig();
|
|
7566
|
-
const prev = this.lastAppliedPoolConfig;
|
|
7567
|
-
for (const k of POOL_BOUND_KEYS) if (next[k] !== prev[k]) return true;
|
|
7568
|
-
return false;
|
|
7569
|
-
}
|
|
7570
|
-
/**
|
|
7571
7885
|
* BaseAddon calls `onConfigChanged` after every settings write. This is the
|
|
7572
7886
|
* SOLE apply path for tuning changes — none of those fields declare
|
|
7573
7887
|
* `requiresRestart` anymore (an addon restart re-probes hardware + reloads
|
|
7574
|
-
* every model on every set, which is exactly what we want to avoid):
|
|
7575
|
-
*
|
|
7576
|
-
*
|
|
7888
|
+
* every model on every set, which is exactly what we want to avoid): a changed
|
|
7889
|
+
* OPERATOR OVERRIDE drives an in-place pool respawn. The per-runtime registry
|
|
7890
|
+
* values cannot change at runtime, so they can never trigger one. The engine
|
|
7577
7891
|
* SELECTION is no longer operator-facing — the node-default backend is derived
|
|
7578
7892
|
* once at boot (`setApi` → `resolveAutoEngine`) from static hardware, so there
|
|
7579
7893
|
* is no per-write engine re-select. The respawn applies optimistically — the
|
|
@@ -7581,14 +7895,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7581
7895
|
* crashed), no addon bounce.
|
|
7582
7896
|
*/
|
|
7583
7897
|
async onConfigChanged() {
|
|
7584
|
-
|
|
7898
|
+
const next = this.resolveOperatorOverride();
|
|
7899
|
+
if (overridesEqual(next, this.lastAppliedOverride)) return;
|
|
7585
7900
|
if (!this.provider) {
|
|
7586
|
-
this.
|
|
7901
|
+
this.lastAppliedOverride = next;
|
|
7587
7902
|
return;
|
|
7588
7903
|
}
|
|
7589
|
-
this.ctx.logger.info("detection-pipeline tuning changed — respawning pool", { meta: {
|
|
7590
|
-
prev: this.
|
|
7591
|
-
next
|
|
7904
|
+
this.ctx.logger.info("detection-pipeline tuning override changed — respawning pool", { meta: {
|
|
7905
|
+
prev: this.lastAppliedOverride,
|
|
7906
|
+
next
|
|
7592
7907
|
} });
|
|
7593
7908
|
try {
|
|
7594
7909
|
await this.provider.shutdown();
|
|
@@ -7596,11 +7911,11 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7596
7911
|
this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7597
7912
|
}
|
|
7598
7913
|
const modelsDir = await this.resolveModelsDir();
|
|
7599
|
-
this.provider = this.buildProvider(modelsDir,
|
|
7914
|
+
this.provider = this.buildProvider(modelsDir, next);
|
|
7600
7915
|
await this.provider.init();
|
|
7601
7916
|
await this.provider.setApi(this.ctx);
|
|
7602
7917
|
await this.provider.warmPool();
|
|
7603
|
-
this.
|
|
7918
|
+
this.lastAppliedOverride = next;
|
|
7604
7919
|
this.ctx.logger.info("detection-pipeline pool respawn complete");
|
|
7605
7920
|
}
|
|
7606
7921
|
};
|