@camstack/addon-pipeline 1.2.192 → 1.2.194
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 +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{local-frame-registry-BDeDpYQk.mjs → default-detection-model-BGBAmtEp.mjs} +84 -2
- package/dist/{local-frame-registry-P7tSfvaY.js → default-detection-model-CJdc8j83.js} +93 -5
- package/dist/detection-pipeline/index.js +34 -116
- package/dist/detection-pipeline/index.mjs +5 -87
- package/dist/{dist-CXO_Fmx6.js → dist-HG5geL9t.js} +129 -1
- package/dist/{dist-B8J7wrtY.mjs → dist-mYDK58Jp.mjs} +118 -2
- package/dist/{lazy-sharp-o-aNNXuz.js → lazy-sharp-DmAwZ0s9.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-DF98pVP7.js → node--my8JTOW.js} +1 -1
- package/dist/{node-BSbB0or0.mjs → node-xRsaPwBa.mjs} +1 -1
- package/dist/pipeline-runner/index.js +661 -236
- package/dist/pipeline-runner/index.mjs +651 -226
- package/dist/{process-memory-BZTgJyE8.mjs → process-memory-BDHR3lxe.mjs} +1 -1
- package/dist/{process-memory-BVMk3gfY.js → process-memory-CoCSce63.js} +1 -1
- package/dist/recorder/index.js +319 -8
- package/dist/recorder/index.mjs +319 -8
- package/dist/{segment-demux-js-CLaSHw0n.js → segment-demux-js-CBynn9Gh.js} +1 -1
- package/dist/{segment-demux-js-0L1EF4Fg.mjs → segment-demux-js-CXvgKfsn.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BmNfrv_U.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C6K2j4VN.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D8AK-_F6.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_NVJXAy.mjs} +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CBxobiZW.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-DQLtjntf.mjs → hostInit-cpmZf09I.mjs} +3 -3
- package/dist/stream-broker/index.js +93 -43
- package/dist/stream-broker/index.mjs +93 -43
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-WiXU2VuW.mjs → worker-protocol-C0R1DDC0.mjs} +1 -1
- package/dist/{worker-protocol-BJzhw3t0.js → worker-protocol-DCsi4YS1.js} +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_ui_mf_2_library__loadShare__.js-CkCh5TvE.mjs +0 -26
|
@@ -2,67 +2,22 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node
|
|
7
|
-
const
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
5
|
+
const require_dist = require("../dist-HG5geL9t.js");
|
|
6
|
+
const require_node = require("../node--my8JTOW.js");
|
|
7
|
+
const require_default_detection_model = require("../default-detection-model-CJdc8j83.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-DCsi4YS1.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
10
|
+
const require_lazy_sharp = require("../lazy-sharp-DmAwZ0s9.js");
|
|
11
11
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
12
12
|
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
13
13
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
14
|
+
let node_os = require("node:os");
|
|
15
|
+
node_os = require_dist.__toESM(node_os);
|
|
14
16
|
let node_child_process = require("node:child_process");
|
|
15
17
|
let sharp = require("sharp");
|
|
16
18
|
sharp = require_dist.__toESM(sharp);
|
|
17
19
|
let node_url = require("node:url");
|
|
18
20
|
let node_perf_hooks = require("node:perf_hooks");
|
|
19
|
-
let node_os = require("node:os");
|
|
20
|
-
node_os = require_dist.__toESM(node_os);
|
|
21
|
-
//#region src/pipeline-runner/first-detection-frame.ts
|
|
22
|
-
/**
|
|
23
|
-
* How long a camera waits, after `attachCamera`, before a decoded frame
|
|
24
|
-
* actually reaches the detection queue.
|
|
25
|
-
*
|
|
26
|
-
* The orchestrator already measures `session motion → attach latency` and it
|
|
27
|
-
* is small — 11, 13 and 18 ms across three consecutive sessions on the same
|
|
28
|
-
* camera. What nothing measured is the NEXT leg: from the attach to the first
|
|
29
|
-
* frame the detector is given. An operator reporting "detection starts late"
|
|
30
|
-
* is describing that leg, and answering them required reading the timestamps
|
|
31
|
-
* of unrelated INFO lines and inferring — which produced a wrong first answer
|
|
32
|
-
* (the absence of a `frame processed` line means the frame produced nothing
|
|
33
|
-
* notable, not that no frame arrived).
|
|
34
|
-
*
|
|
35
|
-
* One number, once per attach, so a slow start is a value rather than an
|
|
36
|
-
* inference.
|
|
37
|
-
*/
|
|
38
|
-
/** Pending attach marks, keyed by device. */
|
|
39
|
-
var FirstDetectionFrameTimer = class {
|
|
40
|
-
attachedAt = /* @__PURE__ */ new Map();
|
|
41
|
-
/** A camera attached. Replaces any pending mark: the newest attach is the
|
|
42
|
-
* one the next frame belongs to. */
|
|
43
|
-
markAttached(deviceId, nowMs) {
|
|
44
|
-
this.attachedAt.set(deviceId, nowMs);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* A decoded frame was admitted to the detection queue. Returns the elapsed
|
|
48
|
-
* ms since the attach the FIRST time it is called after one, and `null`
|
|
49
|
-
* every time after — so the caller logs once per attach, not once per frame.
|
|
50
|
-
*/
|
|
51
|
-
takeLatency(deviceId, nowMs) {
|
|
52
|
-
const at = this.attachedAt.get(deviceId);
|
|
53
|
-
if (at === void 0) return null;
|
|
54
|
-
this.attachedAt.delete(deviceId);
|
|
55
|
-
return Math.max(0, nowMs - at);
|
|
56
|
-
}
|
|
57
|
-
/** A camera detached with no frame ever admitted — drop the mark so the
|
|
58
|
-
* NEXT attach is not measured from this one. Returns whether one was
|
|
59
|
-
* pending, which is itself the interesting fact: a session that ran and
|
|
60
|
-
* fed the detector nothing. */
|
|
61
|
-
forget(deviceId) {
|
|
62
|
-
return this.attachedAt.delete(deviceId);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
//#endregion
|
|
66
21
|
//#region src/session-decode/fork-decode-worker.ts
|
|
67
22
|
/** Absolute path to the built decode-worker child entry (a vite build entry). */
|
|
68
23
|
var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
|
|
@@ -527,6 +482,57 @@ var NativeCropRegistry = class {
|
|
|
527
482
|
}
|
|
528
483
|
};
|
|
529
484
|
//#endregion
|
|
485
|
+
//#region src/session-decode/node-decode-budget.ts
|
|
486
|
+
/**
|
|
487
|
+
* The decode RAM budget and worker cap THIS node runs under.
|
|
488
|
+
*
|
|
489
|
+
* `nodeDecodeBudgetMb` and `maxLiveDecodeWorkers` are cluster settings — one
|
|
490
|
+
* number for a 64 GB hub and a 16 GB Mac alike. Until 2026-09-05 the shipped
|
|
491
|
+
* 8192 MB was the hub's number applied everywhere; with the footprint fixed
|
|
492
|
+
* (D361) that admits 8 workers on the Mac, whose workers measure RSS p90
|
|
493
|
+
* 952 MB on a machine already at 80% — 7.6 GB of decode on 16 GB is a swap
|
|
494
|
+
* away from a stall — while the hub leaves 56 GB idle.
|
|
495
|
+
*
|
|
496
|
+
* Three sources, in order, and the winner is NAMED so a log line can say why:
|
|
497
|
+
* 1. the node's own agent setting (`decodeBudgetMb` / `maxLiveDecodeWorkers`
|
|
498
|
+
* in `AgentPipelineSettings`) — the operator sized this node;
|
|
499
|
+
* 2. the cluster setting when the operator MOVED it off the shipped default
|
|
500
|
+
* (a value equal to the default reads as "nobody chose", the same rule the
|
|
501
|
+
* native-lease knobs use, `readKnob`);
|
|
502
|
+
* 3. otherwise a share of the node's RAM, clamped — the shipped default
|
|
503
|
+
* becomes a per-node derivation instead of the hub's number.
|
|
504
|
+
*/
|
|
505
|
+
var DECODE_BUDGET_RAM_SHARE = .5;
|
|
506
|
+
var DECODE_BUDGET_MIN_MB = 2048;
|
|
507
|
+
var DECODE_BUDGET_MAX_MB = 16384;
|
|
508
|
+
function derivedDecodeBudgetMb(totalMemMb) {
|
|
509
|
+
const share = Math.round(totalMemMb * DECODE_BUDGET_RAM_SHARE);
|
|
510
|
+
return Math.max(DECODE_BUDGET_MIN_MB, Math.min(DECODE_BUDGET_MAX_MB, share));
|
|
511
|
+
}
|
|
512
|
+
function resolveNodeDecodeLimits(input) {
|
|
513
|
+
const budget = input.node.decodeBudgetMb !== void 0 ? {
|
|
514
|
+
budgetMb: input.node.decodeBudgetMb,
|
|
515
|
+
budgetSource: "node"
|
|
516
|
+
} : input.clusterBudgetMb !== input.defaultBudgetMb ? {
|
|
517
|
+
budgetMb: input.clusterBudgetMb,
|
|
518
|
+
budgetSource: "cluster"
|
|
519
|
+
} : {
|
|
520
|
+
budgetMb: derivedDecodeBudgetMb(input.totalMemMb),
|
|
521
|
+
budgetSource: "derived"
|
|
522
|
+
};
|
|
523
|
+
const workers = input.node.maxLiveDecodeWorkers !== void 0 ? {
|
|
524
|
+
maxWorkers: input.node.maxLiveDecodeWorkers,
|
|
525
|
+
maxWorkersSource: "node"
|
|
526
|
+
} : {
|
|
527
|
+
maxWorkers: input.clusterMaxWorkers,
|
|
528
|
+
maxWorkersSource: "cluster"
|
|
529
|
+
};
|
|
530
|
+
return {
|
|
531
|
+
...budget,
|
|
532
|
+
...workers
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
530
536
|
//#region src/session-decode/retained-content-fallback.ts
|
|
531
537
|
/**
|
|
532
538
|
* Honest ≤640 RAM fallback for `getNativeCrop` full-frame misses.
|
|
@@ -928,55 +934,6 @@ function cropRgb(frame, bbox, maxWidth) {
|
|
|
928
934
|
};
|
|
929
935
|
}
|
|
930
936
|
//#endregion
|
|
931
|
-
//#region src/pipeline-runner/frame-cutter-binding.ts
|
|
932
|
-
/** The per-frame binding key — the same identity the retention handle carries. */
|
|
933
|
-
function frameCutterKey(handle) {
|
|
934
|
-
return `${handle.shmId}#${handle.slot}#${handle.seq}`;
|
|
935
|
-
}
|
|
936
|
-
var FrameCutterBinding = class {
|
|
937
|
-
max;
|
|
938
|
-
map = /* @__PURE__ */ new Map();
|
|
939
|
-
constructor(max) {
|
|
940
|
-
this.max = max;
|
|
941
|
-
}
|
|
942
|
-
/** Bind at delivery. Re-binding the same handle replaces without stranding the first. */
|
|
943
|
-
bind(handle, cutter) {
|
|
944
|
-
const key = frameCutterKey(handle);
|
|
945
|
-
this.map.delete(key);
|
|
946
|
-
this.map.set(key, cutter);
|
|
947
|
-
while (this.map.size > this.max) {
|
|
948
|
-
const oldest = this.map.keys().next().value;
|
|
949
|
-
if (oldest === void 0) break;
|
|
950
|
-
const stale = this.map.get(oldest);
|
|
951
|
-
this.map.delete(oldest);
|
|
952
|
-
release(stale);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
/** Consume the binding for a frame whose result arrived. `undefined` when already ended. */
|
|
956
|
-
take(handle) {
|
|
957
|
-
const key = frameCutterKey(handle);
|
|
958
|
-
const cutter = this.map.get(key);
|
|
959
|
-
this.map.delete(key);
|
|
960
|
-
return cutter;
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* End the hold of a frame the runner let go without a result. A no-op for a
|
|
964
|
-
* frame whose result already consumed the binding, so the two paths cannot
|
|
965
|
-
* double-release.
|
|
966
|
-
*/
|
|
967
|
-
drop(handle) {
|
|
968
|
-
release(this.take(handle));
|
|
969
|
-
}
|
|
970
|
-
get size() {
|
|
971
|
-
return this.map.size;
|
|
972
|
-
}
|
|
973
|
-
};
|
|
974
|
-
function release(cutter) {
|
|
975
|
-
try {
|
|
976
|
-
cutter?.release();
|
|
977
|
-
} catch {}
|
|
978
|
-
}
|
|
979
|
-
//#endregion
|
|
980
937
|
//#region src/session-decode/session-decode-coordinator.ts
|
|
981
938
|
/** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
|
|
982
939
|
var SIGKILL_FALLBACK_MS = 1e3;
|
|
@@ -1511,7 +1468,7 @@ function createSessionDecodeCoordinator(deps) {
|
|
|
1511
1468
|
* shm-ring cameras.
|
|
1512
1469
|
*/
|
|
1513
1470
|
async function toDecodedFrame(frame, format, viewSpec) {
|
|
1514
|
-
const geometry = viewSpec ?
|
|
1471
|
+
const geometry = viewSpec ? require_default_detection_model.resolveFrameViewGeometry(frame.image.width, frame.image.height, viewSpec) : void 0;
|
|
1515
1472
|
const bytes = await frame.image.toBuffer(geometry ? {
|
|
1516
1473
|
crop: geometry.sourceCrop,
|
|
1517
1474
|
resize: geometry.output,
|
|
@@ -2919,12 +2876,174 @@ function summarizeDeviceLoad(attachedDeviceKeys) {
|
|
|
2919
2876
|
if (!key) continue;
|
|
2920
2877
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2921
2878
|
}
|
|
2922
|
-
return Array.from(counts.entries()).map(([deviceKey, attachedCameras]) => ({
|
|
2923
|
-
deviceKey,
|
|
2924
|
-
backend: backendOf(deviceKey),
|
|
2925
|
-
attachedCameras,
|
|
2926
|
-
queueDepthTotal: 0
|
|
2927
|
-
})).toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey));
|
|
2879
|
+
return Array.from(counts.entries()).map(([deviceKey, attachedCameras]) => ({
|
|
2880
|
+
deviceKey,
|
|
2881
|
+
backend: backendOf(deviceKey),
|
|
2882
|
+
attachedCameras,
|
|
2883
|
+
queueDepthTotal: 0
|
|
2884
|
+
})).toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey));
|
|
2885
|
+
}
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region src/pipeline-runner/failure-report.ts
|
|
2888
|
+
/**
|
|
2889
|
+
* What `pipeline-runner` reports through `failure-contribution` — the inference
|
|
2890
|
+
* deadline, per camera, with the frames that made it as the denominator.
|
|
2891
|
+
*
|
|
2892
|
+
* ## The number that was never divided
|
|
2893
|
+
*
|
|
2894
|
+
* ~120 `inference timed out — permit released, result discarded` lines in six
|
|
2895
|
+
* hours across seven cameras, on 2026-08-28, never investigated. The line has
|
|
2896
|
+
* carried `tags: { deviceId }` for a while, so the count per camera was always
|
|
2897
|
+
* available; what was not available was the number to divide it by. 120
|
|
2898
|
+
* timeouts out of 120 000 admitted frames and 120 out of 1 200 are opposite
|
|
2899
|
+
* findings and produce identical log volume.
|
|
2900
|
+
*
|
|
2901
|
+
* So the counter takes BOTH outcomes of the same await, from the same place in
|
|
2902
|
+
* `awaitInferenceBounded`. A success counted anywhere else would eventually
|
|
2903
|
+
* drift from the failure and turn the ratio into fiction.
|
|
2904
|
+
*
|
|
2905
|
+
* ## Why there is no `variant` yet
|
|
2906
|
+
*
|
|
2907
|
+
* `failure-contribution` has a `variant` slot exactly so "which camera AND
|
|
2908
|
+
* which model" is one read, and the operator asked for it. The runner cannot
|
|
2909
|
+
* fill it honestly: `awaitInferenceBounded` wraps
|
|
2910
|
+
* `config.processFrame(deviceId, frameInput, executorHandle)`, which runs the
|
|
2911
|
+
* camera's WHOLE pipeline graph behind one deadline. Nothing at this seam knows
|
|
2912
|
+
* which step was still running when the 20 s expired — `CameraRegistration`
|
|
2913
|
+
* carries fps and mode, not models.
|
|
2914
|
+
*
|
|
2915
|
+
* Naming the step means the executor attributing the deadline to the step that
|
|
2916
|
+
* held it, and reporting THAT. Emitting a `variant` the runner guessed (the
|
|
2917
|
+
* camera's configured graph id, say) would be worse than leaving it absent: it
|
|
2918
|
+
* would look like an attribution and be a label. Absent, never invented — the
|
|
2919
|
+
* same rule `load-contribution` states for a measurement nobody made.
|
|
2920
|
+
*/
|
|
2921
|
+
/** One bounded inference deadline per admitted detection frame. */
|
|
2922
|
+
var FAMILY_INFERENCE = "inference";
|
|
2923
|
+
/** The call did not return inside `inferenceTimeoutMs`. */
|
|
2924
|
+
var REASON_INFERENCE_TIMEOUT = "timeout";
|
|
2925
|
+
/**
|
|
2926
|
+
* The runner's `failure-contribution` counters.
|
|
2927
|
+
*
|
|
2928
|
+
* One instance per runner process, read (never drained) by the provider.
|
|
2929
|
+
*/
|
|
2930
|
+
var RunnerFailureReport = class {
|
|
2931
|
+
now;
|
|
2932
|
+
counters;
|
|
2933
|
+
constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
|
|
2934
|
+
this.now = now;
|
|
2935
|
+
this.counters = counters;
|
|
2936
|
+
}
|
|
2937
|
+
/** One bounded inference call. `reason` absent = it returned in time. */
|
|
2938
|
+
noteInference(deviceId, reason) {
|
|
2939
|
+
this.counters.note({
|
|
2940
|
+
deviceId,
|
|
2941
|
+
family: FAMILY_INFERENCE,
|
|
2942
|
+
...reason !== void 0 ? { reason } : {}
|
|
2943
|
+
}, this.now());
|
|
2944
|
+
}
|
|
2945
|
+
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
2946
|
+
list() {
|
|
2947
|
+
return this.counters.snapshot(this.now());
|
|
2948
|
+
}
|
|
2949
|
+
/** Addon disposal. */
|
|
2950
|
+
clear() {
|
|
2951
|
+
this.counters.clear();
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
//#endregion
|
|
2955
|
+
//#region src/pipeline-runner/first-detection-frame.ts
|
|
2956
|
+
/**
|
|
2957
|
+
* How long a camera waits, after `attachCamera`, before a decoded frame
|
|
2958
|
+
* actually reaches the detection queue.
|
|
2959
|
+
*
|
|
2960
|
+
* The orchestrator already measures `session motion → attach latency` and it
|
|
2961
|
+
* is small — 11, 13 and 18 ms across three consecutive sessions on the same
|
|
2962
|
+
* camera. What nothing measured is the NEXT leg: from the attach to the first
|
|
2963
|
+
* frame the detector is given. An operator reporting "detection starts late"
|
|
2964
|
+
* is describing that leg, and answering them required reading the timestamps
|
|
2965
|
+
* of unrelated INFO lines and inferring — which produced a wrong first answer
|
|
2966
|
+
* (the absence of a `frame processed` line means the frame produced nothing
|
|
2967
|
+
* notable, not that no frame arrived).
|
|
2968
|
+
*
|
|
2969
|
+
* One number, once per attach, so a slow start is a value rather than an
|
|
2970
|
+
* inference.
|
|
2971
|
+
*/
|
|
2972
|
+
/** Pending attach marks, keyed by device. */
|
|
2973
|
+
var FirstDetectionFrameTimer = class {
|
|
2974
|
+
attachedAt = /* @__PURE__ */ new Map();
|
|
2975
|
+
/** A camera attached. Replaces any pending mark: the newest attach is the
|
|
2976
|
+
* one the next frame belongs to. */
|
|
2977
|
+
markAttached(deviceId, nowMs) {
|
|
2978
|
+
this.attachedAt.set(deviceId, nowMs);
|
|
2979
|
+
}
|
|
2980
|
+
/**
|
|
2981
|
+
* A decoded frame was admitted to the detection queue. Returns the elapsed
|
|
2982
|
+
* ms since the attach the FIRST time it is called after one, and `null`
|
|
2983
|
+
* every time after — so the caller logs once per attach, not once per frame.
|
|
2984
|
+
*/
|
|
2985
|
+
takeLatency(deviceId, nowMs) {
|
|
2986
|
+
const at = this.attachedAt.get(deviceId);
|
|
2987
|
+
if (at === void 0) return null;
|
|
2988
|
+
this.attachedAt.delete(deviceId);
|
|
2989
|
+
return Math.max(0, nowMs - at);
|
|
2990
|
+
}
|
|
2991
|
+
/** A camera detached with no frame ever admitted — drop the mark so the
|
|
2992
|
+
* NEXT attach is not measured from this one. Returns whether one was
|
|
2993
|
+
* pending, which is itself the interesting fact: a session that ran and
|
|
2994
|
+
* fed the detector nothing. */
|
|
2995
|
+
forget(deviceId) {
|
|
2996
|
+
return this.attachedAt.delete(deviceId);
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
//#endregion
|
|
3000
|
+
//#region src/pipeline-runner/frame-cutter-binding.ts
|
|
3001
|
+
/** The per-frame binding key — the same identity the retention handle carries. */
|
|
3002
|
+
function frameCutterKey(handle) {
|
|
3003
|
+
return `${handle.shmId}#${handle.slot}#${handle.seq}`;
|
|
3004
|
+
}
|
|
3005
|
+
var FrameCutterBinding = class {
|
|
3006
|
+
max;
|
|
3007
|
+
map = /* @__PURE__ */ new Map();
|
|
3008
|
+
constructor(max) {
|
|
3009
|
+
this.max = max;
|
|
3010
|
+
}
|
|
3011
|
+
/** Bind at delivery. Re-binding the same handle replaces without stranding the first. */
|
|
3012
|
+
bind(handle, cutter) {
|
|
3013
|
+
const key = frameCutterKey(handle);
|
|
3014
|
+
this.map.delete(key);
|
|
3015
|
+
this.map.set(key, cutter);
|
|
3016
|
+
while (this.map.size > this.max) {
|
|
3017
|
+
const oldest = this.map.keys().next().value;
|
|
3018
|
+
if (oldest === void 0) break;
|
|
3019
|
+
const stale = this.map.get(oldest);
|
|
3020
|
+
this.map.delete(oldest);
|
|
3021
|
+
release(stale);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
/** Consume the binding for a frame whose result arrived. `undefined` when already ended. */
|
|
3025
|
+
take(handle) {
|
|
3026
|
+
const key = frameCutterKey(handle);
|
|
3027
|
+
const cutter = this.map.get(key);
|
|
3028
|
+
this.map.delete(key);
|
|
3029
|
+
return cutter;
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* End the hold of a frame the runner let go without a result. A no-op for a
|
|
3033
|
+
* frame whose result already consumed the binding, so the two paths cannot
|
|
3034
|
+
* double-release.
|
|
3035
|
+
*/
|
|
3036
|
+
drop(handle) {
|
|
3037
|
+
release(this.take(handle));
|
|
3038
|
+
}
|
|
3039
|
+
get size() {
|
|
3040
|
+
return this.map.size;
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
function release(cutter) {
|
|
3044
|
+
try {
|
|
3045
|
+
cutter?.release();
|
|
3046
|
+
} catch {}
|
|
2928
3047
|
}
|
|
2929
3048
|
//#endregion
|
|
2930
3049
|
//#region src/pipeline-runner/frame-lazy-metrics.ts
|
|
@@ -3499,57 +3618,6 @@ var NodeDecodeLimitsSource = class {
|
|
|
3499
3618
|
}
|
|
3500
3619
|
};
|
|
3501
3620
|
//#endregion
|
|
3502
|
-
//#region src/session-decode/node-decode-budget.ts
|
|
3503
|
-
/**
|
|
3504
|
-
* The decode RAM budget and worker cap THIS node runs under.
|
|
3505
|
-
*
|
|
3506
|
-
* `nodeDecodeBudgetMb` and `maxLiveDecodeWorkers` are cluster settings — one
|
|
3507
|
-
* number for a 64 GB hub and a 16 GB Mac alike. Until 2026-09-05 the shipped
|
|
3508
|
-
* 8192 MB was the hub's number applied everywhere; with the footprint fixed
|
|
3509
|
-
* (D361) that admits 8 workers on the Mac, whose workers measure RSS p90
|
|
3510
|
-
* 952 MB on a machine already at 80% — 7.6 GB of decode on 16 GB is a swap
|
|
3511
|
-
* away from a stall — while the hub leaves 56 GB idle.
|
|
3512
|
-
*
|
|
3513
|
-
* Three sources, in order, and the winner is NAMED so a log line can say why:
|
|
3514
|
-
* 1. the node's own agent setting (`decodeBudgetMb` / `maxLiveDecodeWorkers`
|
|
3515
|
-
* in `AgentPipelineSettings`) — the operator sized this node;
|
|
3516
|
-
* 2. the cluster setting when the operator MOVED it off the shipped default
|
|
3517
|
-
* (a value equal to the default reads as "nobody chose", the same rule the
|
|
3518
|
-
* native-lease knobs use, `readKnob`);
|
|
3519
|
-
* 3. otherwise a share of the node's RAM, clamped — the shipped default
|
|
3520
|
-
* becomes a per-node derivation instead of the hub's number.
|
|
3521
|
-
*/
|
|
3522
|
-
var DECODE_BUDGET_RAM_SHARE = .5;
|
|
3523
|
-
var DECODE_BUDGET_MIN_MB = 2048;
|
|
3524
|
-
var DECODE_BUDGET_MAX_MB = 16384;
|
|
3525
|
-
function derivedDecodeBudgetMb(totalMemMb) {
|
|
3526
|
-
const share = Math.round(totalMemMb * DECODE_BUDGET_RAM_SHARE);
|
|
3527
|
-
return Math.max(DECODE_BUDGET_MIN_MB, Math.min(DECODE_BUDGET_MAX_MB, share));
|
|
3528
|
-
}
|
|
3529
|
-
function resolveNodeDecodeLimits(input) {
|
|
3530
|
-
const budget = input.node.decodeBudgetMb !== void 0 ? {
|
|
3531
|
-
budgetMb: input.node.decodeBudgetMb,
|
|
3532
|
-
budgetSource: "node"
|
|
3533
|
-
} : input.clusterBudgetMb !== input.defaultBudgetMb ? {
|
|
3534
|
-
budgetMb: input.clusterBudgetMb,
|
|
3535
|
-
budgetSource: "cluster"
|
|
3536
|
-
} : {
|
|
3537
|
-
budgetMb: derivedDecodeBudgetMb(input.totalMemMb),
|
|
3538
|
-
budgetSource: "derived"
|
|
3539
|
-
};
|
|
3540
|
-
const workers = input.node.maxLiveDecodeWorkers !== void 0 ? {
|
|
3541
|
-
maxWorkers: input.node.maxLiveDecodeWorkers,
|
|
3542
|
-
maxWorkersSource: "node"
|
|
3543
|
-
} : {
|
|
3544
|
-
maxWorkers: input.clusterMaxWorkers,
|
|
3545
|
-
maxWorkersSource: "cluster"
|
|
3546
|
-
};
|
|
3547
|
-
return {
|
|
3548
|
-
...budget,
|
|
3549
|
-
...workers
|
|
3550
|
-
};
|
|
3551
|
-
}
|
|
3552
|
-
//#endregion
|
|
3553
3621
|
//#region src/session-decode/on-demand-jpeg-fallback.ts
|
|
3554
3622
|
/** Single content-sized JPEG requested after a detection, never a continuous raw. */
|
|
3555
3623
|
var ON_DEMAND_JPEG_VIEW = {
|
|
@@ -3895,74 +3963,6 @@ function packageZoneUnion(polygons) {
|
|
|
3895
3963
|
};
|
|
3896
3964
|
}
|
|
3897
3965
|
//#endregion
|
|
3898
|
-
//#region src/pipeline-runner/failure-report.ts
|
|
3899
|
-
/**
|
|
3900
|
-
* What `pipeline-runner` reports through `failure-contribution` — the inference
|
|
3901
|
-
* deadline, per camera, with the frames that made it as the denominator.
|
|
3902
|
-
*
|
|
3903
|
-
* ## The number that was never divided
|
|
3904
|
-
*
|
|
3905
|
-
* ~120 `inference timed out — permit released, result discarded` lines in six
|
|
3906
|
-
* hours across seven cameras, on 2026-08-28, never investigated. The line has
|
|
3907
|
-
* carried `tags: { deviceId }` for a while, so the count per camera was always
|
|
3908
|
-
* available; what was not available was the number to divide it by. 120
|
|
3909
|
-
* timeouts out of 120 000 admitted frames and 120 out of 1 200 are opposite
|
|
3910
|
-
* findings and produce identical log volume.
|
|
3911
|
-
*
|
|
3912
|
-
* So the counter takes BOTH outcomes of the same await, from the same place in
|
|
3913
|
-
* `awaitInferenceBounded`. A success counted anywhere else would eventually
|
|
3914
|
-
* drift from the failure and turn the ratio into fiction.
|
|
3915
|
-
*
|
|
3916
|
-
* ## Why there is no `variant` yet
|
|
3917
|
-
*
|
|
3918
|
-
* `failure-contribution` has a `variant` slot exactly so "which camera AND
|
|
3919
|
-
* which model" is one read, and the operator asked for it. The runner cannot
|
|
3920
|
-
* fill it honestly: `awaitInferenceBounded` wraps
|
|
3921
|
-
* `config.processFrame(deviceId, frameInput, executorHandle)`, which runs the
|
|
3922
|
-
* camera's WHOLE pipeline graph behind one deadline. Nothing at this seam knows
|
|
3923
|
-
* which step was still running when the 20 s expired — `CameraRegistration`
|
|
3924
|
-
* carries fps and mode, not models.
|
|
3925
|
-
*
|
|
3926
|
-
* Naming the step means the executor attributing the deadline to the step that
|
|
3927
|
-
* held it, and reporting THAT. Emitting a `variant` the runner guessed (the
|
|
3928
|
-
* camera's configured graph id, say) would be worse than leaving it absent: it
|
|
3929
|
-
* would look like an attribution and be a label. Absent, never invented — the
|
|
3930
|
-
* same rule `load-contribution` states for a measurement nobody made.
|
|
3931
|
-
*/
|
|
3932
|
-
/** One bounded inference deadline per admitted detection frame. */
|
|
3933
|
-
var FAMILY_INFERENCE = "inference";
|
|
3934
|
-
/** The call did not return inside `inferenceTimeoutMs`. */
|
|
3935
|
-
var REASON_INFERENCE_TIMEOUT = "timeout";
|
|
3936
|
-
/**
|
|
3937
|
-
* The runner's `failure-contribution` counters.
|
|
3938
|
-
*
|
|
3939
|
-
* One instance per runner process, read (never drained) by the provider.
|
|
3940
|
-
*/
|
|
3941
|
-
var RunnerFailureReport = class {
|
|
3942
|
-
now;
|
|
3943
|
-
counters;
|
|
3944
|
-
constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
|
|
3945
|
-
this.now = now;
|
|
3946
|
-
this.counters = counters;
|
|
3947
|
-
}
|
|
3948
|
-
/** One bounded inference call. `reason` absent = it returned in time. */
|
|
3949
|
-
noteInference(deviceId, reason) {
|
|
3950
|
-
this.counters.note({
|
|
3951
|
-
deviceId,
|
|
3952
|
-
family: FAMILY_INFERENCE,
|
|
3953
|
-
...reason !== void 0 ? { reason } : {}
|
|
3954
|
-
}, this.now());
|
|
3955
|
-
}
|
|
3956
|
-
/** The `failure-contribution` provider's payload. Reads, never resets. */
|
|
3957
|
-
list() {
|
|
3958
|
-
return this.counters.snapshot(this.now());
|
|
3959
|
-
}
|
|
3960
|
-
/** Addon disposal. */
|
|
3961
|
-
clear() {
|
|
3962
|
-
this.counters.clear();
|
|
3963
|
-
}
|
|
3964
|
-
};
|
|
3965
|
-
//#endregion
|
|
3966
3966
|
//#region src/pipeline-runner/frame-governor.ts
|
|
3967
3967
|
var FrameGovernor = class {
|
|
3968
3968
|
config;
|
|
@@ -5156,6 +5156,191 @@ var PipelineRunner = class {
|
|
|
5156
5156
|
return null;
|
|
5157
5157
|
}
|
|
5158
5158
|
};
|
|
5159
|
+
/**
|
|
5160
|
+
* How much of a region must lie OUTSIDE every full-frame detection for the
|
|
5161
|
+
* pass to run on it. A person's own blob (dilated, with shadow) is covered
|
|
5162
|
+
* 70–90% by the person box; a person-and-dog blob is covered ~55% by it.
|
|
5163
|
+
*/
|
|
5164
|
+
var RECOVERY_MIN_UNCOVERED_FRACTION = .35;
|
|
5165
|
+
function area(r) {
|
|
5166
|
+
return Math.max(0, r.w) * Math.max(0, r.h);
|
|
5167
|
+
}
|
|
5168
|
+
function intersection(a, b) {
|
|
5169
|
+
return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
5170
|
+
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Share of `region` not covered by any detection box. The sum of pairwise
|
|
5173
|
+
* intersections over-counts where detections overlap each other, which only
|
|
5174
|
+
* makes the pass MORE conservative; clipped at 1.
|
|
5175
|
+
*/
|
|
5176
|
+
function uncoveredFraction(region, detections) {
|
|
5177
|
+
const regionArea = area(region);
|
|
5178
|
+
if (regionArea <= 0) return 0;
|
|
5179
|
+
let covered = 0;
|
|
5180
|
+
for (const detection of detections) covered += intersection(region, detection);
|
|
5181
|
+
return 1 - Math.min(1, covered / regionArea);
|
|
5182
|
+
}
|
|
5183
|
+
function squareAround(centreX, centreY, side, native) {
|
|
5184
|
+
return {
|
|
5185
|
+
left: Math.round(Math.max(0, Math.min(native.width - side, centreX - side / 2))),
|
|
5186
|
+
top: Math.round(Math.max(0, Math.min(native.height - side, centreY - side / 2))),
|
|
5187
|
+
side
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5190
|
+
function contains(crop, x, y) {
|
|
5191
|
+
return x >= crop.left && x <= crop.left + crop.side && y >= crop.top && y <= crop.top + crop.side;
|
|
5192
|
+
}
|
|
5193
|
+
/**
|
|
5194
|
+
* Squares for one region: one when it fits the largest side, otherwise up to
|
|
5195
|
+
* two tiles along its longer axis (a person-sized blob with something small
|
|
5196
|
+
* at either end). The padded region decides the side, never the frame.
|
|
5197
|
+
*/
|
|
5198
|
+
function cropsForRegion(region, native) {
|
|
5199
|
+
const widthPx = region.w * native.width;
|
|
5200
|
+
const heightPx = region.h * native.height;
|
|
5201
|
+
const longest = Math.max(widthPx, heightPx);
|
|
5202
|
+
const padded = longest * 1.5;
|
|
5203
|
+
const side = Math.round(Math.min(Math.max(padded, 640), 800));
|
|
5204
|
+
const centreX = (region.x + region.w / 2) * native.width;
|
|
5205
|
+
const centreY = (region.y + region.h / 2) * native.height;
|
|
5206
|
+
if (padded <= 800) return [squareAround(centreX, centreY, side, native)];
|
|
5207
|
+
const alongX = widthPx >= heightPx;
|
|
5208
|
+
const quarter = longest / 4;
|
|
5209
|
+
return [squareAround(alongX ? centreX - quarter : centreX, alongX ? centreY : centreY - quarter, side, native), squareAround(alongX ? centreX + quarter : centreX, alongX ? centreY : centreY + quarter, side, native)];
|
|
5210
|
+
}
|
|
5211
|
+
/**
|
|
5212
|
+
* The crops the second pass runs on this frame — possibly none.
|
|
5213
|
+
*
|
|
5214
|
+
* A region qualifies when it is small (`RECOVERY_MAX_REGION_FRAME_FRACTION`)
|
|
5215
|
+
* and mostly uncovered by the full-frame boxes
|
|
5216
|
+
* (`RECOVERY_MIN_UNCOVERED_FRACTION`). Qualifying regions are served largest
|
|
5217
|
+
* uncovered area first under `RECOVERY_MAX_CROPS_PER_FRAME`; a crop whose
|
|
5218
|
+
* centre already lies inside a planned crop is folded into it. A frame that
|
|
5219
|
+
* cannot hold the smallest crop yields nothing: it is already seen at scale.
|
|
5220
|
+
*/
|
|
5221
|
+
function planRecoveryCrops(input) {
|
|
5222
|
+
const { native } = input;
|
|
5223
|
+
if (Math.min(native.width, native.height) < 640) return [];
|
|
5224
|
+
const ranked = input.regions.map((region) => ({
|
|
5225
|
+
region,
|
|
5226
|
+
uncovered: uncoveredFraction(region, input.detections)
|
|
5227
|
+
})).filter(({ region, uncovered }) => {
|
|
5228
|
+
if (area(region) > .25) return false;
|
|
5229
|
+
return uncovered >= RECOVERY_MIN_UNCOVERED_FRACTION;
|
|
5230
|
+
}).toSorted((a, b) => area(b.region) * b.uncovered - area(a.region) * a.uncovered);
|
|
5231
|
+
const planned = [];
|
|
5232
|
+
for (const { region } of ranked) for (const crop of cropsForRegion(region, native)) {
|
|
5233
|
+
if (planned.length >= 2) return planned;
|
|
5234
|
+
const centreX = crop.left + crop.side / 2;
|
|
5235
|
+
const centreY = crop.top + crop.side / 2;
|
|
5236
|
+
if (planned.some((existing) => contains(existing, centreX, centreY))) continue;
|
|
5237
|
+
planned.push(crop);
|
|
5238
|
+
}
|
|
5239
|
+
return planned;
|
|
5240
|
+
}
|
|
5241
|
+
/**
|
|
5242
|
+
* Boxes the executor produced on the crop VIEW → the result's own space.
|
|
5243
|
+
*
|
|
5244
|
+
* Three spaces meet here: the view the model saw (`view`, ≤640 px), the native
|
|
5245
|
+
* square it was cut from (`crop`, native px) and the space the full-frame
|
|
5246
|
+
* result reports in (`result`, native px on the `frameRef` path, the inline
|
|
5247
|
+
* ≤640 view otherwise). A box touching the view edge was clipped by the crop
|
|
5248
|
+
* and is dropped: half a person is not a person, and the tracker would take
|
|
5249
|
+
* the half as the whole.
|
|
5250
|
+
*/
|
|
5251
|
+
function remapCropDetections(detections, crop, view, result, native) {
|
|
5252
|
+
const viewToNative = crop.side / Math.max(1, view.width);
|
|
5253
|
+
const nativeToResultX = result.width / Math.max(1, native.width);
|
|
5254
|
+
const nativeToResultY = result.height / Math.max(1, native.height);
|
|
5255
|
+
const out = [];
|
|
5256
|
+
for (const detection of detections) {
|
|
5257
|
+
const { x, y, width, height } = detection.bbox;
|
|
5258
|
+
if (x <= 2 || y <= 2 || x + width >= view.width - 2 || y + height >= view.height - 2) continue;
|
|
5259
|
+
out.push({
|
|
5260
|
+
...detection,
|
|
5261
|
+
bbox: {
|
|
5262
|
+
x: (crop.left + x * viewToNative) * nativeToResultX,
|
|
5263
|
+
y: (crop.top + y * viewToNative) * nativeToResultY,
|
|
5264
|
+
width: width * viewToNative * nativeToResultX,
|
|
5265
|
+
height: height * viewToNative * nativeToResultY
|
|
5266
|
+
}
|
|
5267
|
+
});
|
|
5268
|
+
}
|
|
5269
|
+
return out;
|
|
5270
|
+
}
|
|
5271
|
+
function boxIntersection(a, b) {
|
|
5272
|
+
return Math.max(0, Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y));
|
|
5273
|
+
}
|
|
5274
|
+
function sameSubject(recovered, existing) {
|
|
5275
|
+
const inter = boxIntersection(recovered, existing);
|
|
5276
|
+
if (inter <= 0) return false;
|
|
5277
|
+
const areaR = recovered.width * recovered.height;
|
|
5278
|
+
const union = areaR + existing.width * existing.height - inter;
|
|
5279
|
+
if (union > 0 && inter / union >= .5) return true;
|
|
5280
|
+
return areaR > 0 && inter / areaR >= .7;
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Class-aware NMS of the recovered boxes against the full-frame ones. Every
|
|
5284
|
+
* full-frame box is kept as it is — the second pass only ADDS. A recovered box
|
|
5285
|
+
* is added when no full-frame box of the same macro class is the same subject
|
|
5286
|
+
* (`RECOVERY_FUSE_IOU`) or contains it (`RECOVERY_FUSE_CONTAINMENT`). Ids are
|
|
5287
|
+
* re-minted so a crop's `d0` cannot collide with the frame's.
|
|
5288
|
+
*/
|
|
5289
|
+
function fuseRecoveredDetections(full, recovered) {
|
|
5290
|
+
const accepted = [];
|
|
5291
|
+
for (const [index, candidate] of recovered.entries()) {
|
|
5292
|
+
if ([...full, ...accepted].some((existing) => existing.macroClass === candidate.macroClass && sameSubject(candidate.bbox, existing.bbox))) continue;
|
|
5293
|
+
accepted.push({
|
|
5294
|
+
...candidate,
|
|
5295
|
+
id: `recovered-${index}-${candidate.id}`
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
return [...full, ...accepted];
|
|
5299
|
+
}
|
|
5300
|
+
/**
|
|
5301
|
+
* The step the second pass runs: the enabled first-level object detector of
|
|
5302
|
+
* the camera's own tree, with its own model and no children. Same model as
|
|
5303
|
+
* the full pass so a recovered box is comparable to a full-frame one.
|
|
5304
|
+
*/
|
|
5305
|
+
function selectRecoveryStep(steps) {
|
|
5306
|
+
const detector = steps.find((step) => step.addonId === "object-detection" && step.enabled !== false);
|
|
5307
|
+
if (!detector) return null;
|
|
5308
|
+
return {
|
|
5309
|
+
addonId: detector.addonId,
|
|
5310
|
+
...detector.modelId !== void 0 ? { modelId: detector.modelId } : {},
|
|
5311
|
+
enabled: true
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
/**
|
|
5315
|
+
* The analyzer's last regions per camera, normalised, with the instant they
|
|
5316
|
+
* were measured. Refreshed off the motion path; read on the detection path.
|
|
5317
|
+
* A stale entry answers nothing — the regions of two seconds ago are not this
|
|
5318
|
+
* frame's. An EMPTY analyzer result replaces the previous regions: motion that
|
|
5319
|
+
* stopped is not motion.
|
|
5320
|
+
*/
|
|
5321
|
+
var MotionRegionMirror = class {
|
|
5322
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
5323
|
+
note(deviceId, regions, frameWidth, frameHeight, now) {
|
|
5324
|
+
if (frameWidth <= 0 || frameHeight <= 0) return;
|
|
5325
|
+
this.byDevice.set(deviceId, {
|
|
5326
|
+
at: now,
|
|
5327
|
+
regions: regions.map((region) => ({
|
|
5328
|
+
x: region.bbox.x / frameWidth,
|
|
5329
|
+
y: region.bbox.y / frameHeight,
|
|
5330
|
+
w: region.bbox.w / frameWidth,
|
|
5331
|
+
h: region.bbox.h / frameHeight
|
|
5332
|
+
}))
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
fresh(deviceId, now) {
|
|
5336
|
+
const entry = this.byDevice.get(deviceId);
|
|
5337
|
+
if (!entry || now - entry.at > 2e3) return [];
|
|
5338
|
+
return entry.regions;
|
|
5339
|
+
}
|
|
5340
|
+
forget(deviceId) {
|
|
5341
|
+
this.byDevice.delete(deviceId);
|
|
5342
|
+
}
|
|
5343
|
+
};
|
|
5159
5344
|
//#endregion
|
|
5160
5345
|
//#region src/pipeline-runner/stateless-step.ts
|
|
5161
5346
|
function modelServability(step, modelId, format) {
|
|
@@ -5272,6 +5457,47 @@ function isUsable(bbox) {
|
|
|
5272
5457
|
//#endregion
|
|
5273
5458
|
//#region src/pipeline-runner/index.ts
|
|
5274
5459
|
/**
|
|
5460
|
+
* addon-pipeline-runner — per-node detection scheduler.
|
|
5461
|
+
*
|
|
5462
|
+
* Lives on every "vision node" (hub if hub is one, every agent that runs
|
|
5463
|
+
* decoders/cameras). Receives camera assignments from the hub-side
|
|
5464
|
+
* `addon-pipeline-orchestrator` via cap calls, owns the per-camera frame
|
|
5465
|
+
* queues + semaphore + motion → detection phase machine, calls the local
|
|
5466
|
+
* `motion-detection` and `pipeline-executor` capabilities for the actual
|
|
5467
|
+
* work, and emits typed events on the bus
|
|
5468
|
+
* (`detection.motion-analysis`, `pipeline.inference-result`, `pipeline.trace`).
|
|
5469
|
+
*
|
|
5470
|
+
* Design notes:
|
|
5471
|
+
*
|
|
5472
|
+
* - All cap inputs/outputs are serializable so the runner is transparently
|
|
5473
|
+
* routable across the cluster: when the runner is co-located with the
|
|
5474
|
+
* broker/decoder/motion/detection (the default and recommended deploy),
|
|
5475
|
+
* capability lookups return live singletons and frame data never crosses
|
|
5476
|
+
* a process boundary. When a deploy splits the runner from the executor
|
|
5477
|
+
* (rare), the lookup returns null and the runner falls back to
|
|
5478
|
+
* `this.ctx.api` (broker proxy) at the cost of serialization.
|
|
5479
|
+
*
|
|
5480
|
+
* - The scheduler core (`PipelineRunner`) is callback-driven and framework
|
|
5481
|
+
* agnostic. The addon class injects the inference + motion callbacks at
|
|
5482
|
+
* construction time, capturing references to the relevant providers
|
|
5483
|
+
* lazily on each invocation.
|
|
5484
|
+
*
|
|
5485
|
+
* - Result post-processing (analysis pipeline, class filters, notifications)
|
|
5486
|
+
* is intentionally NOT done by the runner. The runner emits the raw
|
|
5487
|
+
* inference output as a `pipeline.inference-result` event on the bus;
|
|
5488
|
+
* a hub-side subscriber consumes the event and runs the post-processing
|
|
5489
|
+
* pipeline. This keeps the runner stateless w.r.t. detection-side
|
|
5490
|
+
* business logic and makes it possible to deploy the post-processor
|
|
5491
|
+
* independently from the inference workers.
|
|
5492
|
+
*/
|
|
5493
|
+
/**
|
|
5494
|
+
* How often the small-subject recovery pass reports per camera. The pass runs
|
|
5495
|
+
* up to twice a second; one aggregated line every ten seconds says how often
|
|
5496
|
+
* it ran, what it cost and what it found — the numbers a "did the second
|
|
5497
|
+
* pass help on 615?" question is answered from.
|
|
5498
|
+
*/
|
|
5499
|
+
var RECOVERY_LOG_INTERVAL_MS = 1e4;
|
|
5500
|
+
/**
|
|
5275
5501
|
* How often a device's package zone-union geometry is re-read. An operator
|
|
5276
5502
|
* redrawing a package zone takes effect within one interval; the read is a
|
|
5277
5503
|
* cross-addon cap call reached from the per-frame package gate, so it must be
|
|
@@ -5325,7 +5551,7 @@ function writeRetainedDetectionFrame(store, deviceId, frame, retention) {
|
|
|
5325
5551
|
if (retention) return retention.admitFrame(deviceId, frame);
|
|
5326
5552
|
if (!store) return void 0;
|
|
5327
5553
|
if (!isContinuousRetainEnabled()) return store.allocateHandle(deviceId, frame) ?? void 0;
|
|
5328
|
-
return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(
|
|
5554
|
+
return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(require_default_detection_model.localFrameRegistry, frame.frameRef) : void 0) ?? void 0;
|
|
5329
5555
|
}
|
|
5330
5556
|
var DETECTION_KEEPWARM_MS = 15e3;
|
|
5331
5557
|
/**
|
|
@@ -5386,7 +5612,7 @@ function collectEnabledStepIds(steps, acc) {
|
|
|
5386
5612
|
*/
|
|
5387
5613
|
function tryGetStepDefinition(stepId) {
|
|
5388
5614
|
try {
|
|
5389
|
-
return
|
|
5615
|
+
return require_default_detection_model.getStepDefinition(stepId);
|
|
5390
5616
|
} catch {
|
|
5391
5617
|
return null;
|
|
5392
5618
|
}
|
|
@@ -5686,6 +5912,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5686
5912
|
* only (otherwise we'd emit on every analyzer frame). */
|
|
5687
5913
|
lastAnalyzerDetected = /* @__PURE__ */ new Map();
|
|
5688
5914
|
/**
|
|
5915
|
+
* The analyzer's last motion regions per camera — what the small-subject
|
|
5916
|
+
* recovery pass judges a full-frame result against. Written on the motion
|
|
5917
|
+
* path, read on the detection path (`small-subject-recovery.ts`).
|
|
5918
|
+
*/
|
|
5919
|
+
motionRegions = new MotionRegionMirror();
|
|
5920
|
+
/** Wall clock of the last recovery pass per camera (`RECOVERY_MIN_INTERVAL_MS`). */
|
|
5921
|
+
lastRecoveryAt = /* @__PURE__ */ new Map();
|
|
5922
|
+
/** Per-camera counters since the last recovery report line. */
|
|
5923
|
+
recoveryStats = /* @__PURE__ */ new Map();
|
|
5924
|
+
/**
|
|
5689
5925
|
* Gates the root-plane `package-detection` step to motion-settle edges +
|
|
5690
5926
|
* a low-cadence safety poll instead of every frame (packages are
|
|
5691
5927
|
* stationary — per-frame parcel inference is wasted cost). Consulted in
|
|
@@ -5855,7 +6091,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5855
6091
|
});
|
|
5856
6092
|
this.retention = new OnDemandRetentionSession({
|
|
5857
6093
|
store: this.retainedFrames,
|
|
5858
|
-
registry:
|
|
6094
|
+
registry: require_default_detection_model.localFrameRegistry,
|
|
5859
6095
|
telemetry: this.frameLazy,
|
|
5860
6096
|
nodeId: this.nodeId,
|
|
5861
6097
|
continuousRetain: isContinuousRetainEnabled()
|
|
@@ -5868,7 +6104,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
5868
6104
|
minThrottledFps: this.config.minThrottledFps,
|
|
5869
6105
|
processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
|
|
5870
6106
|
analyzeMotion: (deviceId, frame, handle) => this.runMotionAnalysis(deviceId, frame, handle),
|
|
5871
|
-
releaseFrameRef: (ref, reason) =>
|
|
6107
|
+
releaseFrameRef: (ref, reason) => require_default_detection_model.localFrameRegistry.release(ref, reason),
|
|
5872
6108
|
releaseRetention: (handle, reason) => {
|
|
5873
6109
|
this.retention?.release(handle, reason);
|
|
5874
6110
|
this.frameCutters.drop(handle);
|
|
@@ -7406,6 +7642,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7406
7642
|
this.packageZoneUnionByDevice.delete(deviceId);
|
|
7407
7643
|
this.packageZoneUnionAt.delete(deviceId);
|
|
7408
7644
|
this.lastMotionNearPackageZone.delete(deviceId);
|
|
7645
|
+
this.motionRegions.forget(deviceId);
|
|
7646
|
+
this.lastRecoveryAt.delete(deviceId);
|
|
7647
|
+
this.recoveryStats.delete(deviceId);
|
|
7409
7648
|
const warnPrefix = `${deviceId}:`;
|
|
7410
7649
|
for (const key of this.warnedNoJumpDevice) if (key.startsWith(warnPrefix)) this.warnedNoJumpDevice.delete(key);
|
|
7411
7650
|
this.attached.delete(deviceId);
|
|
@@ -7812,7 +8051,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7812
8051
|
return materialize;
|
|
7813
8052
|
},
|
|
7814
8053
|
...useLocalFrameRef ? {
|
|
7815
|
-
frameRegistry:
|
|
8054
|
+
frameRegistry: require_default_detection_model.localFrameRegistry,
|
|
7816
8055
|
inlineWhen: () => runner.detectionBackpressure(config.deviceId),
|
|
7817
8056
|
inlineViewSpec: SPLIT_DETECTION_VIEW,
|
|
7818
8057
|
onFrameRef: async (frame, nativeCrop) => {
|
|
@@ -7830,8 +8069,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7830
8069
|
workerToRunnerBytes: 0
|
|
7831
8070
|
});
|
|
7832
8071
|
const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
|
|
8072
|
+
if (admitted) nativeCrop?.markInferred();
|
|
7833
8073
|
if (current.motion) {
|
|
7834
|
-
const motion = await resolveMotionFrameView(
|
|
8074
|
+
const motion = await resolveMotionFrameView(require_default_detection_model.localFrameRegistry, frame.frameRef);
|
|
7835
8075
|
if (motion.kind === "view") runner.enqueueMotionFrame(config.deviceId, {
|
|
7836
8076
|
data: Buffer.from(motion.data),
|
|
7837
8077
|
width: motion.width,
|
|
@@ -7841,7 +8081,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7841
8081
|
capturedAt: frame.capturedAt
|
|
7842
8082
|
}, void 0);
|
|
7843
8083
|
}
|
|
7844
|
-
if (admitted) nativeCrop?.markInferred();
|
|
7845
8084
|
return admitted;
|
|
7846
8085
|
}
|
|
7847
8086
|
} : { inlineViewSpec: SPLIT_DETECTION_VIEW },
|
|
@@ -8126,8 +8365,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8126
8365
|
raw: frame.data.byteLength,
|
|
8127
8366
|
jpeg: 0
|
|
8128
8367
|
});
|
|
8368
|
+
let result;
|
|
8129
8369
|
try {
|
|
8130
|
-
|
|
8370
|
+
result = await api.pipelineExecutor.runPipeline.mutate({
|
|
8131
8371
|
steps: [...dispatchSteps],
|
|
8132
8372
|
...image !== void 0 ? { image } : selectExecutorFrameSource(executorCoLocated, frame.frameRef, frame),
|
|
8133
8373
|
deviceId,
|
|
@@ -8144,6 +8384,190 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8144
8384
|
} });
|
|
8145
8385
|
return null;
|
|
8146
8386
|
}
|
|
8387
|
+
if (result === null) return null;
|
|
8388
|
+
return this.recoverSmallSubjects(deviceId, frame, result, dispatchSteps, camConfig?.deviceKey);
|
|
8389
|
+
}
|
|
8390
|
+
/**
|
|
8391
|
+
* The second detector pass at native resolution — `small-subject-recovery.ts`
|
|
8392
|
+
* for the why and the geometry. This method owns only what is not pure: the
|
|
8393
|
+
* per-camera rate limit, the `frameRef` lease that yields the crop view, the
|
|
8394
|
+
* executor call and the report line.
|
|
8395
|
+
*
|
|
8396
|
+
* Runs on the frame the full pass just ran on, in this process, through the
|
|
8397
|
+
* same process-local lease. Anything that makes the pass impossible (no
|
|
8398
|
+
* local frame, no detector in the tree, a stale or empty motion mirror, a
|
|
8399
|
+
* lease miss, an executor error) leaves the full-frame result EXACTLY as it
|
|
8400
|
+
* was: the pass only ever adds.
|
|
8401
|
+
*
|
|
8402
|
+
* The crop is dispatched WITHOUT `deviceId` on purpose. The executor's
|
|
8403
|
+
* provider applies the detection-stage zone gate to whatever result it
|
|
8404
|
+
* builds, in that result's own coordinates — for a crop those are crop
|
|
8405
|
+
* coordinates, and a zone polygon evaluated against them gates the wrong
|
|
8406
|
+
* pixels. The recovered boxes are re-projected into frame space here, and
|
|
8407
|
+
* the tracker on the analytics side applies the same detection rules in
|
|
8408
|
+
* frame space (`frame-processor.ts`, zone filter), so nothing is gated
|
|
8409
|
+
* twice and nothing is gated wrong. The cost is that the crop pass runs on
|
|
8410
|
+
* the node's default thresholds rather than a per-device override.
|
|
8411
|
+
*/
|
|
8412
|
+
async recoverSmallSubjects(deviceId, frame, result, steps, deviceKey) {
|
|
8413
|
+
const api = this.ctx?.api;
|
|
8414
|
+
if (!api || !frame.frameRef) return result;
|
|
8415
|
+
const now = Date.now();
|
|
8416
|
+
if (now - (this.lastRecoveryAt.get(deviceId) ?? 0) < 500) return result;
|
|
8417
|
+
const regions = this.motionRegions.fresh(deviceId, now);
|
|
8418
|
+
if (regions.length === 0) return result;
|
|
8419
|
+
const step = selectRecoveryStep(steps);
|
|
8420
|
+
if (step === null) return result;
|
|
8421
|
+
if (result.width <= 0 || result.height <= 0) return result;
|
|
8422
|
+
const native = {
|
|
8423
|
+
width: frame.frameRef.width,
|
|
8424
|
+
height: frame.frameRef.height
|
|
8425
|
+
};
|
|
8426
|
+
const crops = planRecoveryCrops({
|
|
8427
|
+
regions,
|
|
8428
|
+
detections: result.detections.filter((d) => d.kind === "first-level").map((d) => ({
|
|
8429
|
+
x: d.bbox.x / result.width,
|
|
8430
|
+
y: d.bbox.y / result.height,
|
|
8431
|
+
w: d.bbox.width / result.width,
|
|
8432
|
+
h: d.bbox.height / result.height
|
|
8433
|
+
})),
|
|
8434
|
+
native
|
|
8435
|
+
});
|
|
8436
|
+
if (crops.length === 0) return result;
|
|
8437
|
+
this.lastRecoveryAt.set(deviceId, now);
|
|
8438
|
+
const log = this.ctx.logger.withTags({ deviceId });
|
|
8439
|
+
const started = Date.now();
|
|
8440
|
+
const recovered = [];
|
|
8441
|
+
let cropsRun = 0;
|
|
8442
|
+
for (const crop of crops) {
|
|
8443
|
+
const view = await this.resolveRecoveryCropView(frame.frameRef, crop);
|
|
8444
|
+
if (view === null) continue;
|
|
8445
|
+
cropsRun += 1;
|
|
8446
|
+
try {
|
|
8447
|
+
const cropResult = await api.pipelineExecutor.runPipeline.mutate({
|
|
8448
|
+
steps: [step],
|
|
8449
|
+
image: view.jpeg,
|
|
8450
|
+
plane: "frame",
|
|
8451
|
+
...deviceKey ? { deviceKey } : {}
|
|
8452
|
+
});
|
|
8453
|
+
if (!cropResult) continue;
|
|
8454
|
+
recovered.push(...remapCropDetections(cropResult.detections, crop, {
|
|
8455
|
+
width: view.width,
|
|
8456
|
+
height: view.height
|
|
8457
|
+
}, {
|
|
8458
|
+
width: result.width,
|
|
8459
|
+
height: result.height
|
|
8460
|
+
}, native));
|
|
8461
|
+
} catch (err) {
|
|
8462
|
+
log.warn("small-subject recovery: crop pass failed — full-frame result kept", { meta: {
|
|
8463
|
+
error: require_dist.errMsg(err),
|
|
8464
|
+
crop,
|
|
8465
|
+
nodeId: this.nodeId
|
|
8466
|
+
} });
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
const fused = fuseRecoveredDetections(result.detections, recovered);
|
|
8470
|
+
const added = fused.length - result.detections.length;
|
|
8471
|
+
this.reportRecovery(deviceId, {
|
|
8472
|
+
crops: cropsRun,
|
|
8473
|
+
recovered: added,
|
|
8474
|
+
ms: Date.now() - started,
|
|
8475
|
+
now,
|
|
8476
|
+
classes: fused.slice(result.detections.length).map((d) => d.macroClass)
|
|
8477
|
+
});
|
|
8478
|
+
return added > 0 ? {
|
|
8479
|
+
...result,
|
|
8480
|
+
detections: fused
|
|
8481
|
+
} : result;
|
|
8482
|
+
}
|
|
8483
|
+
/**
|
|
8484
|
+
* One compressed ≤{@link RECOVERY_VIEW_PX} px view of a native square, from
|
|
8485
|
+
* the same process-local lease the full pass resolved its view from. `null`
|
|
8486
|
+
* on a miss — the frame is gone, and the full-frame result stands.
|
|
8487
|
+
*/
|
|
8488
|
+
async resolveRecoveryCropView(ref, crop) {
|
|
8489
|
+
const lease = require_default_detection_model.localFrameRegistry.acquire(ref);
|
|
8490
|
+
if (!lease) return null;
|
|
8491
|
+
let reason = "error";
|
|
8492
|
+
try {
|
|
8493
|
+
const view = await lease.resolve({
|
|
8494
|
+
crop: {
|
|
8495
|
+
left: crop.left,
|
|
8496
|
+
top: crop.top,
|
|
8497
|
+
width: crop.side,
|
|
8498
|
+
height: crop.side
|
|
8499
|
+
},
|
|
8500
|
+
content: {
|
|
8501
|
+
width: 640,
|
|
8502
|
+
height: 640
|
|
8503
|
+
},
|
|
8504
|
+
fit: "contain",
|
|
8505
|
+
format: "jpeg"
|
|
8506
|
+
});
|
|
8507
|
+
if (view.kind !== "view") {
|
|
8508
|
+
reason = "drop";
|
|
8509
|
+
return null;
|
|
8510
|
+
}
|
|
8511
|
+
reason = "success";
|
|
8512
|
+
const jpeg = new Uint8Array(new ArrayBuffer(view.data.byteLength));
|
|
8513
|
+
jpeg.set(view.data);
|
|
8514
|
+
return {
|
|
8515
|
+
jpeg,
|
|
8516
|
+
width: view.width,
|
|
8517
|
+
height: view.height
|
|
8518
|
+
};
|
|
8519
|
+
} catch {
|
|
8520
|
+
return null;
|
|
8521
|
+
} finally {
|
|
8522
|
+
lease.release(reason);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
/** Aggregate the pass per camera and report once per {@link RECOVERY_LOG_INTERVAL_MS}. */
|
|
8526
|
+
reportRecovery(deviceId, sample) {
|
|
8527
|
+
const stats = this.recoveryStats.get(deviceId) ?? {
|
|
8528
|
+
passes: 0,
|
|
8529
|
+
crops: 0,
|
|
8530
|
+
recovered: 0,
|
|
8531
|
+
ms: 0,
|
|
8532
|
+
reportedAt: sample.now
|
|
8533
|
+
};
|
|
8534
|
+
const next = {
|
|
8535
|
+
passes: stats.passes + 1,
|
|
8536
|
+
crops: stats.crops + sample.crops,
|
|
8537
|
+
recovered: stats.recovered + sample.recovered,
|
|
8538
|
+
ms: stats.ms + sample.ms,
|
|
8539
|
+
reportedAt: stats.reportedAt
|
|
8540
|
+
};
|
|
8541
|
+
if (sample.recovered > 0) this.ctx.logger.info("small-subject recovery: the native crop pass found what the full frame missed", {
|
|
8542
|
+
tags: { deviceId },
|
|
8543
|
+
meta: {
|
|
8544
|
+
crops: sample.crops,
|
|
8545
|
+
recovered: sample.recovered,
|
|
8546
|
+
classes: sample.classes,
|
|
8547
|
+
ms: sample.ms
|
|
8548
|
+
}
|
|
8549
|
+
});
|
|
8550
|
+
if (sample.now - stats.reportedAt >= RECOVERY_LOG_INTERVAL_MS) {
|
|
8551
|
+
this.ctx.logger.info("small-subject recovery: pass report", {
|
|
8552
|
+
tags: { deviceId },
|
|
8553
|
+
meta: {
|
|
8554
|
+
passes: next.passes,
|
|
8555
|
+
crops: next.crops,
|
|
8556
|
+
recovered: next.recovered,
|
|
8557
|
+
avgMsPerPass: next.passes > 0 ? Math.round(next.ms / next.passes) : 0,
|
|
8558
|
+
windowMs: sample.now - stats.reportedAt
|
|
8559
|
+
}
|
|
8560
|
+
});
|
|
8561
|
+
this.recoveryStats.set(deviceId, {
|
|
8562
|
+
passes: 0,
|
|
8563
|
+
crops: 0,
|
|
8564
|
+
recovered: 0,
|
|
8565
|
+
ms: 0,
|
|
8566
|
+
reportedAt: sample.now
|
|
8567
|
+
});
|
|
8568
|
+
return;
|
|
8569
|
+
}
|
|
8570
|
+
this.recoveryStats.set(deviceId, next);
|
|
8147
8571
|
}
|
|
8148
8572
|
/** Sticky true once the executor's provider shows up in THIS process' local
|
|
8149
8573
|
* registry (the `detection` execution.group) — from then on the per-frame
|
|
@@ -8224,6 +8648,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8224
8648
|
});
|
|
8225
8649
|
if (!result) return;
|
|
8226
8650
|
const detected = result.regions.length > 0;
|
|
8651
|
+
this.motionRegions.note(deviceId, result.regions, result.frameWidth, result.frameHeight, Date.now());
|
|
8227
8652
|
this.lastMotionNearPackageZone.set(deviceId, motionNearPackageZone({
|
|
8228
8653
|
regions: result.regions,
|
|
8229
8654
|
frameWidth: result.frameWidth,
|
|
@@ -8496,7 +8921,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8496
8921
|
format: "rgb"
|
|
8497
8922
|
},
|
|
8498
8923
|
...useLocalFrameRef ? {
|
|
8499
|
-
frameRegistry:
|
|
8924
|
+
frameRegistry: require_default_detection_model.localFrameRegistry,
|
|
8500
8925
|
inlineWhen: () => runner.detectionBackpressure(config.deviceId),
|
|
8501
8926
|
inlineViewSpec: SPLIT_DETECTION_VIEW,
|
|
8502
8927
|
onFrameRef: (frame, nativeCrop) => {
|