@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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { B as errMsg, E as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, T as enumerateInferenceDevices, V as BaseAddon, X as nodePin, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as pipelineExecutorCapability, nt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as detectionPipelineCapability, x as defaultDeviceFor$1 } from "../dist-
|
|
3
|
-
import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-
|
|
2
|
+
import { B as errMsg, E as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, T as enumerateInferenceDevices, V as BaseAddon, X as nodePin, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as pipelineExecutorCapability, nt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as detectionPipelineCapability, x as defaultDeviceFor$1 } from "../dist-Du7_SNiR.mjs";
|
|
3
|
+
import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-BkY9_IM6.mjs";
|
|
4
4
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
5
5
|
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
|
|
6
|
+
import * as os from "node:os";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
8
|
import sharp from "sharp";
|
|
8
9
|
import * as fs from "node:fs";
|
|
9
10
|
import * as path$1 from "node:path";
|
|
10
|
-
import * as os from "node:os";
|
|
11
11
|
//#region src/detection-pipeline/runtimes.ts
|
|
12
12
|
var KNOWN_PLATFORMS = [
|
|
13
13
|
"darwin",
|
|
@@ -73,14 +73,28 @@ function probedToHardwareInfo(hw) {
|
|
|
73
73
|
coral: coralInfoFrom(hw)
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* THE per-runtime pool provisioning registry — the single source for every
|
|
78
|
+
* pool's concurrency / worker count / batch tuning, keyed by the runtime THAT
|
|
79
|
+
* POOL runs. `engine-factory` used to carry a second, parallel copy of these
|
|
80
|
+
* defaults; it no longer does.
|
|
81
|
+
*
|
|
82
|
+
* `windowMs` / `maxBatchSize` are inert unless `batchMode !== 'none'`, and
|
|
83
|
+
* `numStreams` / `intraOpThreads` are inert at 0 (the runtime picks) — they are
|
|
84
|
+
* still spelled out per row so a reader never has to guess which default a
|
|
85
|
+
* missing key would have hit.
|
|
86
|
+
*/
|
|
76
87
|
var RUNTIME_DETAIL = {
|
|
77
88
|
onnx: {
|
|
78
89
|
label: "ONNX Runtime",
|
|
79
90
|
pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
|
|
80
91
|
tuning: {
|
|
81
92
|
concurrency: 4,
|
|
93
|
+
numWorkers: 1,
|
|
82
94
|
batchMode: "list",
|
|
95
|
+
windowMs: 2,
|
|
83
96
|
maxBatchSize: 8,
|
|
97
|
+
numStreams: 0,
|
|
84
98
|
intraOpThreads: 0
|
|
85
99
|
}
|
|
86
100
|
},
|
|
@@ -89,8 +103,12 @@ var RUNTIME_DETAIL = {
|
|
|
89
103
|
pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
|
|
90
104
|
tuning: {
|
|
91
105
|
concurrency: 1,
|
|
106
|
+
numWorkers: 1,
|
|
92
107
|
batchMode: "none",
|
|
93
|
-
|
|
108
|
+
windowMs: 2,
|
|
109
|
+
maxBatchSize: 8,
|
|
110
|
+
numStreams: 0,
|
|
111
|
+
intraOpThreads: 0
|
|
94
112
|
}
|
|
95
113
|
},
|
|
96
114
|
coreml: {
|
|
@@ -98,10 +116,12 @@ var RUNTIME_DETAIL = {
|
|
|
98
116
|
pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
|
|
99
117
|
tuning: {
|
|
100
118
|
concurrency: 1,
|
|
119
|
+
numWorkers: 1,
|
|
101
120
|
batchMode: "none",
|
|
102
121
|
windowMs: 8,
|
|
103
122
|
maxBatchSize: 8,
|
|
104
|
-
|
|
123
|
+
numStreams: 0,
|
|
124
|
+
intraOpThreads: 0
|
|
105
125
|
}
|
|
106
126
|
},
|
|
107
127
|
edgetpu: {
|
|
@@ -109,8 +129,12 @@ var RUNTIME_DETAIL = {
|
|
|
109
129
|
pythonRequirements: ["requirements.txt", "requirements-edgetpu.txt"],
|
|
110
130
|
tuning: {
|
|
111
131
|
concurrency: 1,
|
|
132
|
+
numWorkers: 1,
|
|
112
133
|
batchMode: "none",
|
|
113
|
-
|
|
134
|
+
windowMs: 2,
|
|
135
|
+
maxBatchSize: 8,
|
|
136
|
+
numStreams: 0,
|
|
137
|
+
intraOpThreads: 0
|
|
114
138
|
}
|
|
115
139
|
}
|
|
116
140
|
};
|
|
@@ -153,6 +177,12 @@ function modelFormatFor(id) {
|
|
|
153
177
|
function pythonRequirementsFor(id) {
|
|
154
178
|
return RUNTIME_DETAIL[id].pythonRequirements;
|
|
155
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* The pool provisioning for ONE runtime. Callers must pass the runtime of the
|
|
182
|
+
* pool they are provisioning — never the node's default runtime. See
|
|
183
|
+
* `engine/pool-provisioning.ts` for the resolver that layers an operator
|
|
184
|
+
* override on top of this.
|
|
185
|
+
*/
|
|
156
186
|
function tuningFor(id) {
|
|
157
187
|
return RUNTIME_DETAIL[id].tuning;
|
|
158
188
|
}
|
|
@@ -371,6 +401,62 @@ var RAW_FMT_CODE = {
|
|
|
371
401
|
* large frame) never trips; override via env for constrained hardware.
|
|
372
402
|
*/
|
|
373
403
|
var POOL_INFER_TIMEOUT_MS = Math.max(1e3, Number(process.env["CAMSTACK_POOL_INFER_TIMEOUT_MS"]) || 6e4);
|
|
404
|
+
/**
|
|
405
|
+
* Max inference requests outstanding to ONE worker before new ones are SHED.
|
|
406
|
+
*
|
|
407
|
+
* Without this the camera dies permanently the first time the worker gets
|
|
408
|
+
* slow, and it did: device 615 produced nothing for 52 minutes on 2026-07-31.
|
|
409
|
+
* The worker was neither dead nor deadlocked — it was replying at full rate,
|
|
410
|
+
* about a minute behind, and every reply landed after {@link
|
|
411
|
+
* POOL_INFER_TIMEOUT_MS} had already abandoned its request. The proof is in the
|
|
412
|
+
* logs: a steady drip of `Response for unknown request id`, request ids
|
|
413
|
+
* climbing ~13/s, for as long as the outage lasted.
|
|
414
|
+
*
|
|
415
|
+
* That state is self-sustaining and cannot drain, because frames keep arriving
|
|
416
|
+
* while every one of them is written straight to the pipe. `dispatch` had NO
|
|
417
|
+
* bound: a transient slowdown (inference went 40ms → 2500ms across several
|
|
418
|
+
* cameras at 16:19 that day) buys a backlog that is never paid off, and once
|
|
419
|
+
* the backlog exceeds the deadline the loss is total.
|
|
420
|
+
*
|
|
421
|
+
* The queue is therefore capped in TIME, not just in count: at the normal ~40ms
|
|
422
|
+
* per inference this is ~0.3s of work, and even at a 20× slowdown it stays well
|
|
423
|
+
* inside the 60s deadline — so a worker that falls behind sheds, catches up,
|
|
424
|
+
* and recovers on its own. Python already sheds under its own per-model bound
|
|
425
|
+
* and answers `{dropped: true}`; this is the same contract applied one layer
|
|
426
|
+
* earlier, where the unbounded queue actually is.
|
|
427
|
+
*/
|
|
428
|
+
var POOL_MAX_IN_FLIGHT = Math.max(1, Number(process.env["CAMSTACK_POOL_MAX_IN_FLIGHT"]) || 8);
|
|
429
|
+
/**
|
|
430
|
+
* Reply deadline for a LIVE inference request (ms), as opposed to the generous
|
|
431
|
+
* {@link POOL_INFER_TIMEOUT_MS} that model loads and commands keep.
|
|
432
|
+
*
|
|
433
|
+
* 60 seconds was never a deadline for live detection — it is three orders of
|
|
434
|
+
* magnitude past the point where the answer is worth having. Frames arrive at
|
|
435
|
+
* ~13/s; a detection that lands 60s late describes a scene that no longer
|
|
436
|
+
* exists, and the only thing the wait accomplished was holding a queue slot
|
|
437
|
+
* while the backlog grew. Operator directive 2026-07-31: "a few seconds are
|
|
438
|
+
* already too many for this scenario".
|
|
439
|
+
*
|
|
440
|
+
* Three seconds is deliberately still loose — normal inference on this fleet is
|
|
441
|
+
* 35-47ms, so this trips only at a ~70× slowdown — but it bounds the damage at
|
|
442
|
+
* seconds instead of a minute, and combined with {@link POOL_MAX_IN_FLIGHT} it
|
|
443
|
+
* makes the backlog mathematically unable to outrun the deadline: the queue can
|
|
444
|
+
* never hold more than `cap × deadline` of work.
|
|
445
|
+
*
|
|
446
|
+
* Commands and model loads keep the long timeout on purpose: a cold model load
|
|
447
|
+
* legitimately takes tens of seconds, and shedding or timing one out
|
|
448
|
+
* desynchronizes the worker's model slots.
|
|
449
|
+
*/
|
|
450
|
+
var POOL_LIVE_INFER_TIMEOUT_MS = Math.max(250, Number(process.env["CAMSTACK_POOL_LIVE_INFER_TIMEOUT_MS"]) || 3e3);
|
|
451
|
+
/** Opcodes the in-flight cap may shed. Commands and frame-cache writes are
|
|
452
|
+
* NEVER shed: a dropped command silently desynchronizes model state, and a
|
|
453
|
+
* dropped `cacheFrame` strands the `inferCached` that follows it. */
|
|
454
|
+
var SHEDDABLE_MSG_TYPES = new Set([
|
|
455
|
+
MSG_INFER_JPEG,
|
|
456
|
+
MSG_INFER_RAW,
|
|
457
|
+
MSG_INFER_BATCH,
|
|
458
|
+
MSG_INFER_CACHED
|
|
459
|
+
]);
|
|
374
460
|
var PoolHandle = class {
|
|
375
461
|
pool;
|
|
376
462
|
modelIndex;
|
|
@@ -450,6 +536,13 @@ var PoolWorker = class {
|
|
|
450
536
|
process = null;
|
|
451
537
|
receiveBuffer = Buffer.alloc(0);
|
|
452
538
|
pending = /* @__PURE__ */ new Map();
|
|
539
|
+
/** True while the in-flight cap is shedding — so the warning fires on the
|
|
540
|
+
* EDGE rather than once per dropped frame. */
|
|
541
|
+
shedding = false;
|
|
542
|
+
/** Inference requests shed by the in-flight cap, for this worker's lifetime.
|
|
543
|
+
* Surfaced so "the camera is quiet" and "the camera is saturated" can be
|
|
544
|
+
* told apart without reading the logs. */
|
|
545
|
+
shedCount = 0;
|
|
453
546
|
nextRequestId = 1;
|
|
454
547
|
ready = false;
|
|
455
548
|
log;
|
|
@@ -461,6 +554,15 @@ var PoolWorker = class {
|
|
|
461
554
|
getPid() {
|
|
462
555
|
return this.process?.pid ?? null;
|
|
463
556
|
}
|
|
557
|
+
/** Requests written and not yet answered. This is the BACKLOG — the number
|
|
558
|
+
* nothing measured while a camera sat at zero frames for 52 minutes. */
|
|
559
|
+
getInFlight() {
|
|
560
|
+
return this.pending.size;
|
|
561
|
+
}
|
|
562
|
+
/** Inference shed by the in-flight cap over this worker's lifetime. */
|
|
563
|
+
getShedCount() {
|
|
564
|
+
return this.shedCount;
|
|
565
|
+
}
|
|
464
566
|
isReady() {
|
|
465
567
|
return this.ready;
|
|
466
568
|
}
|
|
@@ -609,10 +711,51 @@ var PoolWorker = class {
|
|
|
609
711
|
this.ready = false;
|
|
610
712
|
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
611
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Shed an inference request when the worker already has {@link
|
|
716
|
+
* POOL_MAX_IN_FLIGHT} outstanding — the backlog is what turns a slow worker
|
|
717
|
+
* into a dead camera. Returns the shed response, or `null` to proceed.
|
|
718
|
+
*
|
|
719
|
+
* A shed is LOUD the first time and then rate-limited: at frame rate this
|
|
720
|
+
* fires ~13×/s, so logging every one would bury the signal it is meant to
|
|
721
|
+
* raise — but logging none is how a camera went dark for 52 minutes looking
|
|
722
|
+
* like a quiet afternoon.
|
|
723
|
+
*/
|
|
724
|
+
shedIfSaturated(msgType) {
|
|
725
|
+
if (!SHEDDABLE_MSG_TYPES.has(msgType)) return null;
|
|
726
|
+
if (this.pending.size < POOL_MAX_IN_FLIGHT) {
|
|
727
|
+
this.shedding = false;
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
if (!this.shedding) {
|
|
731
|
+
this.shedding = true;
|
|
732
|
+
this.log.warn("worker saturated — shedding inference until it catches up", { meta: {
|
|
733
|
+
worker: this.opts.workerLabel,
|
|
734
|
+
pid: this.getPid(),
|
|
735
|
+
runtime: this.opts.poolRuntime,
|
|
736
|
+
device: this.opts.device ?? "default",
|
|
737
|
+
inFlight: this.pending.size,
|
|
738
|
+
cap: POOL_MAX_IN_FLIGHT,
|
|
739
|
+
deadlineMs: POOL_LIVE_INFER_TIMEOUT_MS
|
|
740
|
+
} });
|
|
741
|
+
}
|
|
742
|
+
this.shedCount++;
|
|
743
|
+
return {
|
|
744
|
+
dropped: true,
|
|
745
|
+
shedReason: "worker-in-flight-cap"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/** Live inference gets seconds; commands and model loads keep the long
|
|
749
|
+
* timeout — see {@link POOL_LIVE_INFER_TIMEOUT_MS}. */
|
|
750
|
+
deadlineFor(msgType) {
|
|
751
|
+
return SHEDDABLE_MSG_TYPES.has(msgType) ? POOL_LIVE_INFER_TIMEOUT_MS : POOL_INFER_TIMEOUT_MS;
|
|
752
|
+
}
|
|
612
753
|
dispatch(msgType, payload) {
|
|
754
|
+
const shed = this.shedIfSaturated(msgType);
|
|
755
|
+
if (shed) return Promise.resolve(shed);
|
|
613
756
|
const reqId = this.allocRequestId();
|
|
614
757
|
return new Promise((resolve, reject) => {
|
|
615
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
758
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
616
759
|
this.pending.set(reqId, {
|
|
617
760
|
resolve,
|
|
618
761
|
reject,
|
|
@@ -632,24 +775,30 @@ var PoolWorker = class {
|
|
|
632
775
|
* `POOL_INFER_TIMEOUT_MS`. `unref` so it never keeps the event loop alive.
|
|
633
776
|
* The normal response + `rejectAll` paths clear it via `PendingRequest.timer`.
|
|
634
777
|
*/
|
|
635
|
-
armRequestTimeout(reqId, reject) {
|
|
778
|
+
armRequestTimeout(reqId, reject, timeoutMs) {
|
|
636
779
|
const timer = setTimeout(() => {
|
|
637
780
|
if (this.pending.delete(reqId)) {
|
|
638
781
|
this.log.error("inference request timed out", { meta: {
|
|
639
782
|
worker: this.opts.workerLabel,
|
|
783
|
+
pid: this.getPid(),
|
|
784
|
+
runtime: this.opts.poolRuntime,
|
|
785
|
+
device: this.opts.device ?? "default",
|
|
786
|
+
inFlight: this.pending.size,
|
|
640
787
|
reqId,
|
|
641
788
|
timeoutMs: POOL_INFER_TIMEOUT_MS
|
|
642
789
|
} });
|
|
643
|
-
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${
|
|
790
|
+
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${timeoutMs}ms (worker alive, no reply)`));
|
|
644
791
|
}
|
|
645
|
-
},
|
|
792
|
+
}, timeoutMs);
|
|
646
793
|
timer.unref?.();
|
|
647
794
|
return timer;
|
|
648
795
|
}
|
|
649
796
|
dispatchMulti(msgType, payloadLen, parts) {
|
|
797
|
+
const shed = this.shedIfSaturated(msgType);
|
|
798
|
+
if (shed) return Promise.resolve(shed);
|
|
650
799
|
const reqId = this.allocRequestId();
|
|
651
800
|
return new Promise((resolve, reject) => {
|
|
652
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
801
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
653
802
|
this.pending.set(reqId, {
|
|
654
803
|
resolve,
|
|
655
804
|
reject,
|
|
@@ -755,6 +904,22 @@ var SharedInferencePool = class {
|
|
|
755
904
|
}
|
|
756
905
|
device;
|
|
757
906
|
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
907
|
+
/** Summed backlog and shed count across the pool's workers. A rising
|
|
908
|
+
* `inFlight` with a rising `shed` is a worker falling behind; a rising
|
|
909
|
+
* `inFlight` with zero `shed` and no results is the state that used to be
|
|
910
|
+
* invisible. */
|
|
911
|
+
getBacklog() {
|
|
912
|
+
let inFlight = 0;
|
|
913
|
+
let shed = 0;
|
|
914
|
+
for (const w of this.workers) {
|
|
915
|
+
inFlight += w.getInFlight();
|
|
916
|
+
shed += w.getShedCount();
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
inFlight,
|
|
920
|
+
shed
|
|
921
|
+
};
|
|
922
|
+
}
|
|
758
923
|
getPid() {
|
|
759
924
|
return this.workers[0]?.getPid() ?? null;
|
|
760
925
|
}
|
|
@@ -788,6 +953,7 @@ var SharedInferencePool = class {
|
|
|
788
953
|
loadedCount,
|
|
789
954
|
startupMs,
|
|
790
955
|
runtime: this.poolRuntime,
|
|
956
|
+
device: this.device ?? "default",
|
|
791
957
|
workers: this.numWorkers,
|
|
792
958
|
pythonWorkers: innerWorkers,
|
|
793
959
|
pids: this.getPids()
|
|
@@ -879,7 +1045,10 @@ var SharedInferencePool = class {
|
|
|
879
1045
|
async dispose() {
|
|
880
1046
|
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
881
1047
|
this.workers.length = 0;
|
|
882
|
-
this.log.info("Pool process(es) terminated"
|
|
1048
|
+
this.log.info("Pool process(es) terminated", { meta: {
|
|
1049
|
+
runtime: this.poolRuntime,
|
|
1050
|
+
device: this.device ?? "default"
|
|
1051
|
+
} });
|
|
883
1052
|
}
|
|
884
1053
|
/**
|
|
885
1054
|
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
@@ -892,7 +1061,9 @@ var SharedInferencePool = class {
|
|
|
892
1061
|
this.droppedResponseCount++;
|
|
893
1062
|
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
894
1063
|
modelIndex,
|
|
895
|
-
droppedTotal: this.droppedResponseCount
|
|
1064
|
+
droppedTotal: this.droppedResponseCount,
|
|
1065
|
+
runtime: this.poolRuntime,
|
|
1066
|
+
device: this.device ?? "default"
|
|
896
1067
|
} });
|
|
897
1068
|
}
|
|
898
1069
|
return result;
|
|
@@ -1228,6 +1399,126 @@ var PipelineModelManager = class {
|
|
|
1228
1399
|
}
|
|
1229
1400
|
};
|
|
1230
1401
|
//#endregion
|
|
1402
|
+
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1403
|
+
/**
|
|
1404
|
+
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1405
|
+
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1406
|
+
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1407
|
+
* than silently building a wrong pool. Format comes from the shared
|
|
1408
|
+
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1409
|
+
*/
|
|
1410
|
+
function resolveDeviceEngine(deviceKey) {
|
|
1411
|
+
const colon = deviceKey.indexOf(":");
|
|
1412
|
+
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1413
|
+
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1414
|
+
const format = DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1415
|
+
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1416
|
+
return {
|
|
1417
|
+
runtime: "python",
|
|
1418
|
+
backend,
|
|
1419
|
+
format,
|
|
1420
|
+
device
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* The inverse of {@link resolveDeviceEngine}: the `<backend>:<device>` key that
|
|
1425
|
+
* names the pool an engine belongs to. Every pool-level log line carries this —
|
|
1426
|
+
* without it a default pool logs anonymously and six hours of logs cannot tell
|
|
1427
|
+
* "the NPU is unused" from "the NPU IS the anonymous default pool".
|
|
1428
|
+
* Falls back to the bare backend when the engine carries no device.
|
|
1429
|
+
*/
|
|
1430
|
+
function deviceKeyOf(engine) {
|
|
1431
|
+
return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
|
|
1432
|
+
}
|
|
1433
|
+
//#endregion
|
|
1434
|
+
//#region src/detection-pipeline/engine/pool-provisioning.ts
|
|
1435
|
+
/** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
|
|
1436
|
+
var OVERRIDABLE_KEYS = [
|
|
1437
|
+
"concurrency",
|
|
1438
|
+
"numWorkers",
|
|
1439
|
+
"batchMode",
|
|
1440
|
+
"windowMs",
|
|
1441
|
+
"maxBatchSize",
|
|
1442
|
+
"numStreams",
|
|
1443
|
+
"intraOpThreads"
|
|
1444
|
+
];
|
|
1445
|
+
/** Backends that are not a runtime of their own and run on onnxruntime. */
|
|
1446
|
+
var BACKEND_TO_RUNTIME = {
|
|
1447
|
+
onnx: "onnx",
|
|
1448
|
+
cpu: "onnx",
|
|
1449
|
+
cuda: "onnx",
|
|
1450
|
+
openvino: "openvino",
|
|
1451
|
+
coreml: "coreml",
|
|
1452
|
+
edgetpu: "edgetpu"
|
|
1453
|
+
};
|
|
1454
|
+
/**
|
|
1455
|
+
* The registry runtime a device-pool backend provisions from. Unknown backends
|
|
1456
|
+
* floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
|
|
1457
|
+
* format, so a pool never comes up unprovisioned.
|
|
1458
|
+
*/
|
|
1459
|
+
function runtimeIdForBackend(backend) {
|
|
1460
|
+
return BACKEND_TO_RUNTIME[backend] ?? "onnx";
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Resolve the provisioning for a pool running `engine`, applying the precedence
|
|
1464
|
+
* documented at the top of this module. `engine.device` is not consulted today
|
|
1465
|
+
* (see the module doc) but is part of the signature so a bench-backed
|
|
1466
|
+
* per-device row lands here and nowhere else.
|
|
1467
|
+
*/
|
|
1468
|
+
function resolvePoolProvisioning(engine, override = {}) {
|
|
1469
|
+
const runtime = runtimeIdForBackend(engine.backend);
|
|
1470
|
+
const profile = tuningFor(runtime);
|
|
1471
|
+
return {
|
|
1472
|
+
runtime,
|
|
1473
|
+
overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
|
|
1474
|
+
concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
|
|
1475
|
+
numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
|
|
1476
|
+
batchMode: override.batchMode ?? profile.batchMode,
|
|
1477
|
+
windowMs: override.windowMs ?? profile.windowMs,
|
|
1478
|
+
maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
|
|
1479
|
+
numStreams: override.numStreams ?? profile.numStreams,
|
|
1480
|
+
intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function positiveNumber(value) {
|
|
1484
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1485
|
+
}
|
|
1486
|
+
function batchMode(value) {
|
|
1487
|
+
return value === "none" || value === "list" || value === "window" ? value : void 0;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Read the operator override out of the detection addon's settings. Only a
|
|
1491
|
+
* NON-AUTO value counts: the sliders persist `0` and the select persists `''`
|
|
1492
|
+
* for "Auto (backend default)", and those must resolve to the runtime's
|
|
1493
|
+
* registry value, not to a literal 0 worker threads.
|
|
1494
|
+
*
|
|
1495
|
+
* The result is deliberately sparse — an empty object means "nothing
|
|
1496
|
+
* overridden", which is what lets a per-device pool provision from its own
|
|
1497
|
+
* runtime instead of inheriting the node's.
|
|
1498
|
+
*/
|
|
1499
|
+
function readOperatorOverride(settings) {
|
|
1500
|
+
const override = {};
|
|
1501
|
+
const concurrency = positiveNumber(settings.concurrency);
|
|
1502
|
+
if (concurrency !== void 0) override.concurrency = concurrency;
|
|
1503
|
+
const numWorkers = positiveNumber(settings.numWorkers);
|
|
1504
|
+
if (numWorkers !== void 0) override.numWorkers = numWorkers;
|
|
1505
|
+
const mode = batchMode(settings.batchMode);
|
|
1506
|
+
if (mode !== void 0) override.batchMode = mode;
|
|
1507
|
+
const windowMs = positiveNumber(settings.windowMs);
|
|
1508
|
+
if (windowMs !== void 0) override.windowMs = windowMs;
|
|
1509
|
+
const maxBatchSize = positiveNumber(settings.maxBatchSize);
|
|
1510
|
+
if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
|
|
1511
|
+
const numStreams = positiveNumber(settings.numStreams);
|
|
1512
|
+
if (numStreams !== void 0) override.numStreams = numStreams;
|
|
1513
|
+
const intraOpThreads = positiveNumber(settings.intraOpThreads);
|
|
1514
|
+
if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
|
|
1515
|
+
return override;
|
|
1516
|
+
}
|
|
1517
|
+
/** Two overrides carry the same fields with the same values. */
|
|
1518
|
+
function overridesEqual(a, b) {
|
|
1519
|
+
return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
|
|
1520
|
+
}
|
|
1521
|
+
//#endregion
|
|
1231
1522
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
1232
1523
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
1233
1524
|
coreml: "coreml",
|
|
@@ -1297,9 +1588,31 @@ var EngineFactory = class {
|
|
|
1297
1588
|
poolManager = null;
|
|
1298
1589
|
log;
|
|
1299
1590
|
opts;
|
|
1591
|
+
/**
|
|
1592
|
+
* `<backend>:<device>` — on EVERY log line this factory writes. A pool that
|
|
1593
|
+
* logs anonymously cannot be told apart from another pool of the same runtime
|
|
1594
|
+
* on a different accelerator.
|
|
1595
|
+
*/
|
|
1596
|
+
deviceKey;
|
|
1597
|
+
/**
|
|
1598
|
+
* Resolved at CONSTRUCTION, not at pool spawn, so it is inspectable without
|
|
1599
|
+
* starting Python and so a pool that fails to spawn still logs what it was
|
|
1600
|
+
* trying to be.
|
|
1601
|
+
*/
|
|
1602
|
+
resolvedProvisioning;
|
|
1300
1603
|
constructor(opts) {
|
|
1301
1604
|
this.opts = opts;
|
|
1302
1605
|
this.log = opts.logger;
|
|
1606
|
+
this.deviceKey = deviceKeyOf(opts.engine);
|
|
1607
|
+
this.resolvedProvisioning = resolvePoolProvisioning(opts.engine, opts.provisioning);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* The provisioning this factory's pool runs with — resolved from the pool's
|
|
1611
|
+
* OWN runtime plus any operator override. Read by the effective-tuning
|
|
1612
|
+
* readout and by the tests that pin the per-runtime behaviour.
|
|
1613
|
+
*/
|
|
1614
|
+
get provisioning() {
|
|
1615
|
+
return this.resolvedProvisioning;
|
|
1303
1616
|
}
|
|
1304
1617
|
/** Detection always uses the Python pool. */
|
|
1305
1618
|
get usesPythonPool() {
|
|
@@ -1401,40 +1714,51 @@ var EngineFactory = class {
|
|
|
1401
1714
|
this.poolManager = null;
|
|
1402
1715
|
}
|
|
1403
1716
|
}
|
|
1717
|
+
/**
|
|
1718
|
+
* The one pool-init log line. `deviceKey` / `device` are the reason it
|
|
1719
|
+
* exists: the default pool used to log neither, so an operator reading a
|
|
1720
|
+
* day of logs could not tell whether the NPU was idle or was simply the
|
|
1721
|
+
* anonymous default pool.
|
|
1722
|
+
*
|
|
1723
|
+
* `overriddenFields` replaces the old `source` field. `source` claimed
|
|
1724
|
+
* `'auto-default' | 'user-override'` about the CONCURRENCY TUNING but read as
|
|
1725
|
+
* the provenance of the device/engine choice, and was `'user-override'` on
|
|
1726
|
+
* every pool because the addon always passed explicit tuning. This lists the
|
|
1727
|
+
* fields the operator actually overrode — empty means pure registry.
|
|
1728
|
+
*/
|
|
1729
|
+
logProvisioning() {
|
|
1730
|
+
const p = this.resolvedProvisioning;
|
|
1731
|
+
this.log.info("Python inference pool", { meta: {
|
|
1732
|
+
deviceKey: this.deviceKey,
|
|
1733
|
+
backend: this.opts.engine.backend,
|
|
1734
|
+
device: this.opts.engine.device ?? null,
|
|
1735
|
+
runtime: p.runtime,
|
|
1736
|
+
concurrency: p.concurrency,
|
|
1737
|
+
numWorkers: p.numWorkers,
|
|
1738
|
+
overriddenFields: p.overriddenFields,
|
|
1739
|
+
tuning: {
|
|
1740
|
+
batchMode: p.batchMode,
|
|
1741
|
+
windowMs: p.windowMs,
|
|
1742
|
+
maxBatchSize: p.maxBatchSize,
|
|
1743
|
+
numStreams: p.numStreams,
|
|
1744
|
+
intraOpThreads: p.intraOpThreads
|
|
1745
|
+
}
|
|
1746
|
+
} });
|
|
1747
|
+
}
|
|
1404
1748
|
async initPythonPool(steps) {
|
|
1749
|
+
this.logProvisioning();
|
|
1405
1750
|
const pythonPath = this.opts.pythonPath;
|
|
1406
1751
|
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.");
|
|
1407
1752
|
const poolRuntime = BACKEND_TO_POOL_RUNTIME[this.opts.engine.backend];
|
|
1408
1753
|
if (!poolRuntime) throw new Error(`No pool runtime mapping for backend "${this.opts.engine.backend}"`);
|
|
1409
|
-
const concurrency = this.
|
|
1410
|
-
coreml: 1,
|
|
1411
|
-
openvino: 1,
|
|
1412
|
-
onnxruntime: 4,
|
|
1413
|
-
edgetpu: 1
|
|
1414
|
-
}[poolRuntime];
|
|
1415
|
-
const isAutoDefault = this.opts.concurrency === void 0;
|
|
1416
|
-
const defaultBatchMode = {
|
|
1417
|
-
coreml: "window",
|
|
1418
|
-
openvino: "none",
|
|
1419
|
-
onnxruntime: "list",
|
|
1420
|
-
edgetpu: "none"
|
|
1421
|
-
};
|
|
1422
|
-
const tuning = this.opts.tuning ?? {};
|
|
1754
|
+
const { concurrency, numWorkers } = this.resolvedProvisioning;
|
|
1423
1755
|
const resolvedTuning = {
|
|
1424
|
-
batchMode:
|
|
1425
|
-
windowMs:
|
|
1426
|
-
maxBatchSize:
|
|
1427
|
-
numStreams:
|
|
1428
|
-
intraOpThreads:
|
|
1756
|
+
batchMode: this.resolvedProvisioning.batchMode,
|
|
1757
|
+
windowMs: this.resolvedProvisioning.windowMs,
|
|
1758
|
+
maxBatchSize: this.resolvedProvisioning.maxBatchSize,
|
|
1759
|
+
numStreams: this.resolvedProvisioning.numStreams,
|
|
1760
|
+
intraOpThreads: this.resolvedProvisioning.intraOpThreads
|
|
1429
1761
|
};
|
|
1430
|
-
const numWorkers = Math.max(1, this.opts.numWorkers ?? 1);
|
|
1431
|
-
this.log.info("Python inference pool", { meta: {
|
|
1432
|
-
runtime: poolRuntime,
|
|
1433
|
-
concurrency,
|
|
1434
|
-
numWorkers,
|
|
1435
|
-
source: isAutoDefault ? "auto-default" : "user-override",
|
|
1436
|
-
tuning: resolvedTuning
|
|
1437
|
-
} });
|
|
1438
1762
|
this.pool = new SharedInferencePool(pythonPath, poolRuntime, this.log.child("pool"), {
|
|
1439
1763
|
concurrency,
|
|
1440
1764
|
tuning: resolvedTuning,
|
|
@@ -1490,28 +1814,6 @@ var EngineFactory = class {
|
|
|
1490
1814
|
}
|
|
1491
1815
|
};
|
|
1492
1816
|
//#endregion
|
|
1493
|
-
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1494
|
-
/**
|
|
1495
|
-
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1496
|
-
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1497
|
-
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1498
|
-
* than silently building a wrong pool. Format comes from the shared
|
|
1499
|
-
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1500
|
-
*/
|
|
1501
|
-
function resolveDeviceEngine(deviceKey) {
|
|
1502
|
-
const colon = deviceKey.indexOf(":");
|
|
1503
|
-
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1504
|
-
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1505
|
-
const format = DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1506
|
-
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1507
|
-
return {
|
|
1508
|
-
runtime: "python",
|
|
1509
|
-
backend,
|
|
1510
|
-
format,
|
|
1511
|
-
device
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
//#endregion
|
|
1515
1817
|
//#region src/detection-pipeline/engine/idle-pool-reaper.ts
|
|
1516
1818
|
var IdlePoolReaper = class {
|
|
1517
1819
|
lastUsed = /* @__PURE__ */ new Map();
|
|
@@ -2696,19 +2998,25 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
|
|
|
2696
2998
|
* DROPS detections outright, so it sits between that and a hard cut).
|
|
2697
2999
|
*/
|
|
2698
3000
|
var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
3001
|
+
/** A cut only makes sense in (0, 1); anything else is treated as OFF rather
|
|
3002
|
+
* than dropping every detection on a typo. */
|
|
3003
|
+
function isUsableHardAreaCut(ratio) {
|
|
3004
|
+
return Number.isFinite(ratio) && ratio > 0 && ratio < 1;
|
|
3005
|
+
}
|
|
2699
3006
|
/**
|
|
2700
3007
|
* True when `bbox` is a near-full-frame LOW-confidence phantom that should be
|
|
2701
3008
|
* DROPPED from the frame's detections (overlay / tracker / events / motion-wake).
|
|
2702
3009
|
*
|
|
2703
3010
|
* Rule (drop ⇔ returns true):
|
|
2704
|
-
*
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2707
|
-
*
|
|
3011
|
+
* areaRatio ≥ `hardAreaRatio` ← score-independent, OFF by default
|
|
3012
|
+
* OR ( (areaRatio ≥ {@link FULL_FRAME_AREA_RATIO}
|
|
3013
|
+
* OR width ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameWidth
|
|
3014
|
+
* OR height ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameHeight)
|
|
3015
|
+
* AND score < `maxConfidence` )
|
|
2708
3016
|
*
|
|
2709
|
-
*
|
|
2710
|
-
* subject and
|
|
2711
|
-
* to reject on → not a phantom.
|
|
3017
|
+
* Below the hard cut a high-confidence detection (`score ≥ maxConfidence`) is
|
|
3018
|
+
* trusted as a real subject and survives. Degenerate frame dimensions (≤0)
|
|
3019
|
+
* carry no info to reject on → not a phantom.
|
|
2712
3020
|
*
|
|
2713
3021
|
* @param bbox absolute-pixel `[x1, y1, x2, y2]` box in source-frame coords.
|
|
2714
3022
|
* @param frameWidth source frame width in px.
|
|
@@ -2717,10 +3025,26 @@ var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
|
2717
3025
|
* @param maxConfidence confidence bar below which a full-frame box is a phantom
|
|
2718
3026
|
* (defaults to {@link FULL_FRAME_MAX_CONFIDENCE}; overridable by the caller so
|
|
2719
3027
|
* the bar can be threaded from detection settings without new cap methods).
|
|
3028
|
+
* @param hardAreaRatio area fraction at/above which the box is dropped NO
|
|
3029
|
+
* MATTER the score. Defaults to {@link HARD_AREA_CUT_DISABLED} (off) — it is
|
|
3030
|
+
* armed per camera, because a doorbell close-up legitimately fills the frame
|
|
3031
|
+
* while an overhead courtyard box never does. Threaded from step settings as
|
|
3032
|
+
* `fullFrameGuardHardAreaRatio`.
|
|
3033
|
+
*/
|
|
3034
|
+
/**
|
|
3035
|
+
* True when a box trips the guard's GEOMETRY test (near-full-frame, or
|
|
3036
|
+
* edge-to-edge in one axis) but SURVIVES because its score cleared the
|
|
3037
|
+
* confidence bar.
|
|
3038
|
+
*
|
|
3039
|
+
* These are the ones worth counting: on 2026-07-30 device 615 produced six
|
|
3040
|
+
* such boxes in a day, at 0.710-0.787, and they were only found by an offline
|
|
3041
|
+
* sweep of stored events — nothing in production reported them. Arming the
|
|
3042
|
+
* hard-area cut on a camera should be a decision made from this count, not
|
|
3043
|
+
* from one incident.
|
|
2720
3044
|
*/
|
|
2721
|
-
function
|
|
3045
|
+
function isFullFrameSurvivor(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE) {
|
|
2722
3046
|
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
2723
|
-
if (score
|
|
3047
|
+
if (score < maxConfidence) return false;
|
|
2724
3048
|
const [x1, y1, x2, y2] = bbox;
|
|
2725
3049
|
const w = x2 - x1;
|
|
2726
3050
|
const h = y2 - y1;
|
|
@@ -2729,6 +3053,19 @@ function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxCo
|
|
|
2729
3053
|
if (h >= frameHeight * .98) return true;
|
|
2730
3054
|
return false;
|
|
2731
3055
|
}
|
|
3056
|
+
function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE, hardAreaRatio = 1) {
|
|
3057
|
+
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
3058
|
+
const [x1, y1, x2, y2] = bbox;
|
|
3059
|
+
const w = x2 - x1;
|
|
3060
|
+
const h = y2 - y1;
|
|
3061
|
+
const areaRatio = w * h / (frameWidth * frameHeight);
|
|
3062
|
+
if (isUsableHardAreaCut(hardAreaRatio) && areaRatio >= hardAreaRatio) return true;
|
|
3063
|
+
if (score >= maxConfidence) return false;
|
|
3064
|
+
if (areaRatio >= .9) return true;
|
|
3065
|
+
if (w >= frameWidth * .98) return true;
|
|
3066
|
+
if (h >= frameHeight * .98) return true;
|
|
3067
|
+
return false;
|
|
3068
|
+
}
|
|
2732
3069
|
//#endregion
|
|
2733
3070
|
//#region src/detection-pipeline/pipeline/plate-deskew.ts
|
|
2734
3071
|
/**
|
|
@@ -3576,10 +3913,34 @@ var PipelineExecutor = class {
|
|
|
3576
3913
|
continue;
|
|
3577
3914
|
}
|
|
3578
3915
|
const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
|
|
3579
|
-
|
|
3580
|
-
|
|
3916
|
+
const fullFrameHardArea = rootStep.settings?.["fullFrameGuardHardAreaRatio"];
|
|
3917
|
+
if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0, typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0)) {
|
|
3918
|
+
this.opts.logger?.info("full-frame phantom guard: detection dropped", {
|
|
3919
|
+
tags: { deviceId },
|
|
3920
|
+
meta: {
|
|
3921
|
+
macroClass: mutable.macroClass,
|
|
3922
|
+
score: mutable.score,
|
|
3923
|
+
bbox: mutable.bbox,
|
|
3924
|
+
frameWidth: imageWidth,
|
|
3925
|
+
frameHeight: imageHeight,
|
|
3926
|
+
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,
|
|
3927
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3928
|
+
hardAreaRatio: typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3581
3931
|
continue;
|
|
3582
3932
|
}
|
|
3933
|
+
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", {
|
|
3934
|
+
tags: { deviceId },
|
|
3935
|
+
meta: {
|
|
3936
|
+
macroClass: mutable.macroClass,
|
|
3937
|
+
score: mutable.score,
|
|
3938
|
+
bbox: mutable.bbox,
|
|
3939
|
+
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,
|
|
3940
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3941
|
+
hint: "arm fullFrameGuardHardAreaRatio on this camera if these are phantoms"
|
|
3942
|
+
}
|
|
3943
|
+
});
|
|
3583
3944
|
try {
|
|
3584
3945
|
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3585
3946
|
} catch (err) {
|
|
@@ -4894,8 +5255,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4894
5255
|
modelsDir: this.modelsDir,
|
|
4895
5256
|
logger: this.log.child("engine-verify"),
|
|
4896
5257
|
pythonPath: this.executorOptions.pythonPath,
|
|
4897
|
-
|
|
4898
|
-
|
|
5258
|
+
provisioning: {
|
|
5259
|
+
concurrency: 1,
|
|
5260
|
+
numWorkers: 1
|
|
5261
|
+
}
|
|
4899
5262
|
});
|
|
4900
5263
|
try {
|
|
4901
5264
|
await probe.initialize([]);
|
|
@@ -5651,9 +6014,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5651
6014
|
modelsDir: this.modelsDir,
|
|
5652
6015
|
logger: this.log.child("engine-override"),
|
|
5653
6016
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5654
|
-
|
|
5655
|
-
tuning: this.executorOptions.tuning,
|
|
5656
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6017
|
+
provisioning: this.executorOptions.provisioning
|
|
5657
6018
|
});
|
|
5658
6019
|
const initPromise = newFactory.initialize([]);
|
|
5659
6020
|
this.overrideCache = {
|
|
@@ -5833,9 +6194,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5833
6194
|
modelsDir: this.modelsDir,
|
|
5834
6195
|
logger: this.log.child("engine-override"),
|
|
5835
6196
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5836
|
-
|
|
5837
|
-
tuning: this.executorOptions.tuning,
|
|
5838
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6197
|
+
provisioning: this.executorOptions.provisioning
|
|
5839
6198
|
});
|
|
5840
6199
|
const initPromise = newFactory.initialize([]);
|
|
5841
6200
|
this.overrideCache = {
|
|
@@ -6367,13 +6726,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6367
6726
|
if (!this.engineFactory) return;
|
|
6368
6727
|
await this.engineFactory.uncacheFrame(input.frameId);
|
|
6369
6728
|
}
|
|
6729
|
+
/**
|
|
6730
|
+
* The tuning of the node's DEFAULT pool. Per-device pools each resolve their
|
|
6731
|
+
* own runtime's row, so this is not "the node's tuning" — the per-pool truth
|
|
6732
|
+
* is on each pool's `Python inference pool` log line and in `listLoadedEngines`.
|
|
6733
|
+
*/
|
|
6370
6734
|
async getEffectiveTuning() {
|
|
6371
|
-
const
|
|
6735
|
+
const p = resolvePoolProvisioning(this.currentEngine, this.executorOptions.provisioning);
|
|
6372
6736
|
return {
|
|
6373
|
-
batchMode:
|
|
6374
|
-
windowMs:
|
|
6375
|
-
maxBatchSize:
|
|
6376
|
-
concurrency:
|
|
6737
|
+
batchMode: p.batchMode,
|
|
6738
|
+
windowMs: p.windowMs,
|
|
6739
|
+
maxBatchSize: p.maxBatchSize,
|
|
6740
|
+
concurrency: p.concurrency
|
|
6377
6741
|
};
|
|
6378
6742
|
}
|
|
6379
6743
|
/**
|
|
@@ -6391,9 +6755,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6391
6755
|
modelsDir: this.modelsDir,
|
|
6392
6756
|
logger: this.log.child("engine"),
|
|
6393
6757
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6394
|
-
|
|
6395
|
-
tuning: this.executorOptions.tuning,
|
|
6396
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6758
|
+
provisioning: this.executorOptions.provisioning
|
|
6397
6759
|
});
|
|
6398
6760
|
await this.engineFactory.initialize([]);
|
|
6399
6761
|
}
|
|
@@ -6432,13 +6794,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6432
6794
|
modelsDir: this.modelsDir,
|
|
6433
6795
|
logger: this.log.child(`engine:${deviceKey}`),
|
|
6434
6796
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6435
|
-
|
|
6436
|
-
tuning: this.executorOptions.tuning,
|
|
6437
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6797
|
+
provisioning: this.executorOptions.provisioning
|
|
6438
6798
|
});
|
|
6439
6799
|
try {
|
|
6440
6800
|
await factory.initialize([]);
|
|
6441
6801
|
} catch (err) {
|
|
6802
|
+
this.log.error("Device pool failed to initialize", { meta: {
|
|
6803
|
+
deviceKey,
|
|
6804
|
+
backend: engine.backend,
|
|
6805
|
+
device: engine.device ?? null,
|
|
6806
|
+
provisioning: factory.provisioning,
|
|
6807
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6808
|
+
} });
|
|
6442
6809
|
await factory.dispose().catch(() => void 0);
|
|
6443
6810
|
throw err;
|
|
6444
6811
|
}
|
|
@@ -7049,21 +7416,22 @@ function toNodePlatform(value) {
|
|
|
7049
7416
|
return value === "darwin" || value === "linux" || value === "win32" ? value : void 0;
|
|
7050
7417
|
}
|
|
7051
7418
|
/**
|
|
7052
|
-
*
|
|
7053
|
-
*
|
|
7054
|
-
*
|
|
7055
|
-
* these and rebuilds the provider when they flip.
|
|
7419
|
+
* Slider keys on the "Inference executor" section that map 1:1 onto a
|
|
7420
|
+
* {@link RuntimePoolProfile} field. Used to inject the node-default runtime's
|
|
7421
|
+
* registry value as the field's suggested default in `getGlobalSettings`.
|
|
7056
7422
|
*/
|
|
7057
|
-
var
|
|
7423
|
+
var PROFILE_SLIDER_KEYS = [
|
|
7058
7424
|
"concurrency",
|
|
7059
|
-
"
|
|
7425
|
+
"numWorkers",
|
|
7060
7426
|
"windowMs",
|
|
7061
7427
|
"maxBatchSize",
|
|
7062
7428
|
"numStreams",
|
|
7063
|
-
"intraOpThreads"
|
|
7064
|
-
"numWorkers"
|
|
7429
|
+
"intraOpThreads"
|
|
7065
7430
|
];
|
|
7066
|
-
|
|
7431
|
+
function isProfileSliderKey(key) {
|
|
7432
|
+
return PROFILE_SLIDER_KEYS.some((k) => k === key);
|
|
7433
|
+
}
|
|
7434
|
+
var DetectionPipelineAddon = class extends BaseAddon {
|
|
7067
7435
|
provider = null;
|
|
7068
7436
|
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
7069
7437
|
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
@@ -7078,14 +7446,17 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7078
7446
|
* ENGINE_METRICS_HEARTBEAT_MS for liveness. */
|
|
7079
7447
|
lastEmittedEngineSnapshot = null;
|
|
7080
7448
|
/**
|
|
7081
|
-
* Last-applied
|
|
7082
|
-
*
|
|
7083
|
-
*
|
|
7084
|
-
*
|
|
7085
|
-
*
|
|
7086
|
-
*
|
|
7449
|
+
* Last-applied OPERATOR OVERRIDE, taken at the end of `onInitialize` and
|
|
7450
|
+
* refreshed at the end of `onConfigChanged`. Lets the change handler diff the
|
|
7451
|
+
* current config against the snapshot and decide whether a pool respawn is
|
|
7452
|
+
* necessary — most config edits (engine cascade, audio settings) don't touch
|
|
7453
|
+
* the tuning fields and don't deserve a multi-second model reload.
|
|
7454
|
+
*
|
|
7455
|
+
* The per-runtime registry values are compile-time constants resolved inside
|
|
7456
|
+
* each pool, so they can never be the reason for a respawn; only an operator
|
|
7457
|
+
* edit can.
|
|
7087
7458
|
*/
|
|
7088
|
-
|
|
7459
|
+
lastAppliedOverride = {};
|
|
7089
7460
|
/**
|
|
7090
7461
|
* Embedded Python path resolved once at boot via
|
|
7091
7462
|
* `ctx.deps.ensurePython()`. Empty string means the download failed
|
|
@@ -7253,17 +7624,16 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7253
7624
|
...section,
|
|
7254
7625
|
fields: section.fields.map((field) => {
|
|
7255
7626
|
if (field.type === "select") {
|
|
7256
|
-
if (field.key === "batchMode"
|
|
7627
|
+
if (field.key === "batchMode") return {
|
|
7257
7628
|
...field,
|
|
7258
|
-
default: tuning
|
|
7629
|
+
default: tuning.batchMode
|
|
7259
7630
|
};
|
|
7260
7631
|
}
|
|
7261
7632
|
if (field.type === "slider" && "key" in field) {
|
|
7262
|
-
const tuned = tuning[field.key];
|
|
7263
7633
|
const sliderField = field;
|
|
7264
|
-
let patchedField =
|
|
7634
|
+
let patchedField = isProfileSliderKey(sliderField.key) ? {
|
|
7265
7635
|
...sliderField,
|
|
7266
|
-
default:
|
|
7636
|
+
default: tuning[sliderField.key]
|
|
7267
7637
|
} : sliderField;
|
|
7268
7638
|
if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
|
|
7269
7639
|
...patchedField,
|
|
@@ -7372,60 +7742,39 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7372
7742
|
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
7373
7743
|
}
|
|
7374
7744
|
/**
|
|
7375
|
-
*
|
|
7745
|
+
* The operator's explicit deviations from the per-runtime registry — and
|
|
7746
|
+
* NOTHING else.
|
|
7376
7747
|
*
|
|
7377
|
-
*
|
|
7378
|
-
*
|
|
7379
|
-
*
|
|
7380
|
-
*
|
|
7381
|
-
*
|
|
7748
|
+
* This used to be `resolveBackendTuning()`: it read `tuningFor(<the node's
|
|
7749
|
+
* default backend>)` and handed the whole resolved row to the provider, which
|
|
7750
|
+
* passed it verbatim to every pool it built. That is how an `openvino:npu`
|
|
7751
|
+
* pool on an onnx-default node came up with onnx's `concurrency: 4`. The
|
|
7752
|
+
* per-runtime values now resolve inside each pool
|
|
7753
|
+
* (`engine/pool-provisioning.ts`), keyed by that pool's own runtime, so
|
|
7754
|
+
* nothing backend-shaped may be computed here.
|
|
7382
7755
|
*
|
|
7383
|
-
*
|
|
7384
|
-
*
|
|
7385
|
-
*
|
|
7756
|
+
* Precedence, in full: an operator override below wins over the pool's
|
|
7757
|
+
* runtime registry row; an "Auto" field (slider `0`, select `''`) is not an
|
|
7758
|
+
* override. An override is node-wide by design — the operator set it against
|
|
7759
|
+
* a node, not against a runtime.
|
|
7386
7760
|
*/
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
|
|
7390
|
-
const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
|
|
7391
|
-
return {
|
|
7392
|
-
concurrency: num(t["concurrency"], 1),
|
|
7393
|
-
batchMode: batch(t["batchMode"], "window"),
|
|
7394
|
-
windowMs: num(t["windowMs"], 2),
|
|
7395
|
-
maxBatchSize: num(t["maxBatchSize"], 8),
|
|
7396
|
-
numStreams: num(t["numStreams"], 0),
|
|
7397
|
-
intraOpThreads: num(t["intraOpThreads"], 0),
|
|
7398
|
-
numWorkers: num(t["numWorkers"], 1)
|
|
7399
|
-
};
|
|
7761
|
+
resolveOperatorOverride() {
|
|
7762
|
+
return readOperatorOverride(this.config);
|
|
7400
7763
|
}
|
|
7401
7764
|
/**
|
|
7402
|
-
* Construct a fresh provider
|
|
7403
|
-
* truth for the provider wiring — shared by `onInitialize
|
|
7404
|
-
*
|
|
7765
|
+
* Construct a fresh provider carrying the operator override. Single source of
|
|
7766
|
+
* truth for the provider wiring — shared by `onInitialize` and the
|
|
7767
|
+
* `onConfigChanged` pool respawn. NOTHING backend-derived is passed: each
|
|
7768
|
+
* pool resolves its own runtime's registry row.
|
|
7405
7769
|
*/
|
|
7406
|
-
buildProvider(modelsDir,
|
|
7770
|
+
buildProvider(modelsDir, override) {
|
|
7407
7771
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
7408
7772
|
return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
|
|
7409
|
-
|
|
7410
|
-
tuning: {
|
|
7411
|
-
batchMode: tuning.batchMode,
|
|
7412
|
-
windowMs: tuning.windowMs,
|
|
7413
|
-
maxBatchSize: tuning.maxBatchSize,
|
|
7414
|
-
numStreams: tuning.numStreams,
|
|
7415
|
-
intraOpThreads: tuning.intraOpThreads
|
|
7416
|
-
},
|
|
7417
|
-
numWorkers: tuning.numWorkers,
|
|
7773
|
+
provisioning: override,
|
|
7418
7774
|
pythonPath: this.pythonPath,
|
|
7419
7775
|
pythonAddonDir: this.pythonAddonDir
|
|
7420
7776
|
});
|
|
7421
7777
|
}
|
|
7422
|
-
/**
|
|
7423
|
-
* Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
|
|
7424
|
-
* post-resolve / config-change respawn is actually needed).
|
|
7425
|
-
*/
|
|
7426
|
-
static poolTuningEqual(a, b) {
|
|
7427
|
-
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;
|
|
7428
|
-
}
|
|
7429
7778
|
async onInitialize() {
|
|
7430
7779
|
const modelsDir = await this.resolveModelsDir();
|
|
7431
7780
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
@@ -7434,23 +7783,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7434
7783
|
if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
|
|
7435
7784
|
else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
|
|
7436
7785
|
await this.proactivelyInstallOpenvino();
|
|
7437
|
-
|
|
7438
|
-
this.provider = this.buildProvider(modelsDir, initialTuning);
|
|
7786
|
+
this.provider = this.buildProvider(modelsDir, this.resolveOperatorOverride());
|
|
7439
7787
|
await this.provider.init();
|
|
7440
7788
|
await this.provider.setApi(this.ctx);
|
|
7441
|
-
const resolvedTuning = this.resolveBackendTuning();
|
|
7442
|
-
if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
|
|
7443
|
-
this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
|
|
7444
|
-
from: initialTuning,
|
|
7445
|
-
to: resolvedTuning
|
|
7446
|
-
} });
|
|
7447
|
-
this.provider = this.buildProvider(modelsDir, resolvedTuning);
|
|
7448
|
-
await this.provider.init();
|
|
7449
|
-
await this.provider.setApi(this.ctx);
|
|
7450
|
-
}
|
|
7451
7789
|
await this.provider.warmPool();
|
|
7452
7790
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
7453
|
-
this.
|
|
7791
|
+
this.lastAppliedOverride = this.resolveOperatorOverride();
|
|
7454
7792
|
return [{
|
|
7455
7793
|
capability: pipelineExecutorCapability,
|
|
7456
7794
|
provider: this.provider
|
|
@@ -7536,36 +7874,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7536
7874
|
}
|
|
7537
7875
|
}
|
|
7538
7876
|
/**
|
|
7539
|
-
* Snapshot the pool-bound subset of the EFFECTIVE tuning (post-
|
|
7540
|
-
* `resolveBackendTuning`). Stored config values for these fields are
|
|
7541
|
-
* ignored, so this snapshot only changes when `engineBackend` flips
|
|
7542
|
-
* onto a different `tuningFor` row.
|
|
7543
|
-
*/
|
|
7544
|
-
snapshotPoolConfig() {
|
|
7545
|
-
const t = this.resolveBackendTuning();
|
|
7546
|
-
return {
|
|
7547
|
-
concurrency: t.concurrency ?? 0,
|
|
7548
|
-
batchMode: t.batchMode,
|
|
7549
|
-
windowMs: t.windowMs,
|
|
7550
|
-
maxBatchSize: t.maxBatchSize,
|
|
7551
|
-
numStreams: t.numStreams,
|
|
7552
|
-
intraOpThreads: t.intraOpThreads,
|
|
7553
|
-
numWorkers: t.numWorkers
|
|
7554
|
-
};
|
|
7555
|
-
}
|
|
7556
|
-
poolConfigChanged() {
|
|
7557
|
-
const next = this.snapshotPoolConfig();
|
|
7558
|
-
const prev = this.lastAppliedPoolConfig;
|
|
7559
|
-
for (const k of POOL_BOUND_KEYS) if (next[k] !== prev[k]) return true;
|
|
7560
|
-
return false;
|
|
7561
|
-
}
|
|
7562
|
-
/**
|
|
7563
7877
|
* BaseAddon calls `onConfigChanged` after every settings write. This is the
|
|
7564
7878
|
* SOLE apply path for tuning changes — none of those fields declare
|
|
7565
7879
|
* `requiresRestart` anymore (an addon restart re-probes hardware + reloads
|
|
7566
|
-
* every model on every set, which is exactly what we want to avoid):
|
|
7567
|
-
*
|
|
7568
|
-
*
|
|
7880
|
+
* every model on every set, which is exactly what we want to avoid): a changed
|
|
7881
|
+
* OPERATOR OVERRIDE drives an in-place pool respawn. The per-runtime registry
|
|
7882
|
+
* values cannot change at runtime, so they can never trigger one. The engine
|
|
7569
7883
|
* SELECTION is no longer operator-facing — the node-default backend is derived
|
|
7570
7884
|
* once at boot (`setApi` → `resolveAutoEngine`) from static hardware, so there
|
|
7571
7885
|
* is no per-write engine re-select. The respawn applies optimistically — the
|
|
@@ -7573,14 +7887,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7573
7887
|
* crashed), no addon bounce.
|
|
7574
7888
|
*/
|
|
7575
7889
|
async onConfigChanged() {
|
|
7576
|
-
|
|
7890
|
+
const next = this.resolveOperatorOverride();
|
|
7891
|
+
if (overridesEqual(next, this.lastAppliedOverride)) return;
|
|
7577
7892
|
if (!this.provider) {
|
|
7578
|
-
this.
|
|
7893
|
+
this.lastAppliedOverride = next;
|
|
7579
7894
|
return;
|
|
7580
7895
|
}
|
|
7581
|
-
this.ctx.logger.info("detection-pipeline tuning changed — respawning pool", { meta: {
|
|
7582
|
-
prev: this.
|
|
7583
|
-
next
|
|
7896
|
+
this.ctx.logger.info("detection-pipeline tuning override changed — respawning pool", { meta: {
|
|
7897
|
+
prev: this.lastAppliedOverride,
|
|
7898
|
+
next
|
|
7584
7899
|
} });
|
|
7585
7900
|
try {
|
|
7586
7901
|
await this.provider.shutdown();
|
|
@@ -7588,11 +7903,11 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7588
7903
|
this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7589
7904
|
}
|
|
7590
7905
|
const modelsDir = await this.resolveModelsDir();
|
|
7591
|
-
this.provider = this.buildProvider(modelsDir,
|
|
7906
|
+
this.provider = this.buildProvider(modelsDir, next);
|
|
7592
7907
|
await this.provider.init();
|
|
7593
7908
|
await this.provider.setApi(this.ctx);
|
|
7594
7909
|
await this.provider.warmPool();
|
|
7595
|
-
this.
|
|
7910
|
+
this.lastAppliedOverride = next;
|
|
7596
7911
|
this.ctx.logger.info("detection-pipeline pool respawn complete");
|
|
7597
7912
|
}
|
|
7598
7913
|
};
|