@camstack/addon-pipeline 1.2.27 → 1.2.29
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/detection-pipeline/index.js +71 -12
- package/dist/detection-pipeline/index.mjs +71 -12
- package/dist/pipeline-runner/index.js +91 -12
- package/dist/pipeline-runner/index.mjs +91 -12
- package/dist/{step-definitions-C8A78dBi.mjs → step-definitions-1GIwU6R8.mjs} +12 -1
- package/dist/{step-definitions-Bo1hJQQw.js → step-definitions-Drb9cWbB.js} +12 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
6
|
const require_dist = require("../dist-DUHlZcWd.js");
|
|
7
|
-
const require_step_definitions = require("../step-definitions-
|
|
7
|
+
const require_step_definitions = require("../step-definitions-Drb9cWbB.js");
|
|
8
8
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
9
9
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
|
|
10
10
|
let node_child_process = require("node:child_process");
|
|
@@ -2704,19 +2704,25 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
|
|
|
2704
2704
|
* DROPS detections outright, so it sits between that and a hard cut).
|
|
2705
2705
|
*/
|
|
2706
2706
|
var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
2707
|
+
/** A cut only makes sense in (0, 1); anything else is treated as OFF rather
|
|
2708
|
+
* than dropping every detection on a typo. */
|
|
2709
|
+
function isUsableHardAreaCut(ratio) {
|
|
2710
|
+
return Number.isFinite(ratio) && ratio > 0 && ratio < 1;
|
|
2711
|
+
}
|
|
2707
2712
|
/**
|
|
2708
2713
|
* True when `bbox` is a near-full-frame LOW-confidence phantom that should be
|
|
2709
2714
|
* DROPPED from the frame's detections (overlay / tracker / events / motion-wake).
|
|
2710
2715
|
*
|
|
2711
2716
|
* Rule (drop ⇔ returns true):
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
2717
|
+
* areaRatio ≥ `hardAreaRatio` ← score-independent, OFF by default
|
|
2718
|
+
* OR ( (areaRatio ≥ {@link FULL_FRAME_AREA_RATIO}
|
|
2719
|
+
* OR width ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameWidth
|
|
2720
|
+
* OR height ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameHeight)
|
|
2721
|
+
* AND score < `maxConfidence` )
|
|
2716
2722
|
*
|
|
2717
|
-
*
|
|
2718
|
-
* subject and
|
|
2719
|
-
* to reject on → not a phantom.
|
|
2723
|
+
* Below the hard cut a high-confidence detection (`score ≥ maxConfidence`) is
|
|
2724
|
+
* trusted as a real subject and survives. Degenerate frame dimensions (≤0)
|
|
2725
|
+
* carry no info to reject on → not a phantom.
|
|
2720
2726
|
*
|
|
2721
2727
|
* @param bbox absolute-pixel `[x1, y1, x2, y2]` box in source-frame coords.
|
|
2722
2728
|
* @param frameWidth source frame width in px.
|
|
@@ -2725,10 +2731,26 @@ var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
|
2725
2731
|
* @param maxConfidence confidence bar below which a full-frame box is a phantom
|
|
2726
2732
|
* (defaults to {@link FULL_FRAME_MAX_CONFIDENCE}; overridable by the caller so
|
|
2727
2733
|
* the bar can be threaded from detection settings without new cap methods).
|
|
2734
|
+
* @param hardAreaRatio area fraction at/above which the box is dropped NO
|
|
2735
|
+
* MATTER the score. Defaults to {@link HARD_AREA_CUT_DISABLED} (off) — it is
|
|
2736
|
+
* armed per camera, because a doorbell close-up legitimately fills the frame
|
|
2737
|
+
* while an overhead courtyard box never does. Threaded from step settings as
|
|
2738
|
+
* `fullFrameGuardHardAreaRatio`.
|
|
2728
2739
|
*/
|
|
2729
|
-
|
|
2740
|
+
/**
|
|
2741
|
+
* True when a box trips the guard's GEOMETRY test (near-full-frame, or
|
|
2742
|
+
* edge-to-edge in one axis) but SURVIVES because its score cleared the
|
|
2743
|
+
* confidence bar.
|
|
2744
|
+
*
|
|
2745
|
+
* These are the ones worth counting: on 2026-07-30 device 615 produced six
|
|
2746
|
+
* such boxes in a day, at 0.710-0.787, and they were only found by an offline
|
|
2747
|
+
* sweep of stored events — nothing in production reported them. Arming the
|
|
2748
|
+
* hard-area cut on a camera should be a decision made from this count, not
|
|
2749
|
+
* from one incident.
|
|
2750
|
+
*/
|
|
2751
|
+
function isFullFrameSurvivor(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE) {
|
|
2730
2752
|
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
2731
|
-
if (score
|
|
2753
|
+
if (score < maxConfidence) return false;
|
|
2732
2754
|
const [x1, y1, x2, y2] = bbox;
|
|
2733
2755
|
const w = x2 - x1;
|
|
2734
2756
|
const h = y2 - y1;
|
|
@@ -2737,6 +2759,19 @@ function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxCo
|
|
|
2737
2759
|
if (h >= frameHeight * .98) return true;
|
|
2738
2760
|
return false;
|
|
2739
2761
|
}
|
|
2762
|
+
function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE, hardAreaRatio = 1) {
|
|
2763
|
+
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
2764
|
+
const [x1, y1, x2, y2] = bbox;
|
|
2765
|
+
const w = x2 - x1;
|
|
2766
|
+
const h = y2 - y1;
|
|
2767
|
+
const areaRatio = w * h / (frameWidth * frameHeight);
|
|
2768
|
+
if (isUsableHardAreaCut(hardAreaRatio) && areaRatio >= hardAreaRatio) return true;
|
|
2769
|
+
if (score >= maxConfidence) return false;
|
|
2770
|
+
if (areaRatio >= .9) return true;
|
|
2771
|
+
if (w >= frameWidth * .98) return true;
|
|
2772
|
+
if (h >= frameHeight * .98) return true;
|
|
2773
|
+
return false;
|
|
2774
|
+
}
|
|
2740
2775
|
//#endregion
|
|
2741
2776
|
//#region src/detection-pipeline/pipeline/plate-deskew.ts
|
|
2742
2777
|
/**
|
|
@@ -3584,10 +3619,34 @@ var PipelineExecutor = class {
|
|
|
3584
3619
|
continue;
|
|
3585
3620
|
}
|
|
3586
3621
|
const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
|
|
3587
|
-
|
|
3588
|
-
|
|
3622
|
+
const fullFrameHardArea = rootStep.settings?.["fullFrameGuardHardAreaRatio"];
|
|
3623
|
+
if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0, typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0)) {
|
|
3624
|
+
this.opts.logger?.info("full-frame phantom guard: detection dropped", {
|
|
3625
|
+
tags: { deviceId },
|
|
3626
|
+
meta: {
|
|
3627
|
+
macroClass: mutable.macroClass,
|
|
3628
|
+
score: mutable.score,
|
|
3629
|
+
bbox: mutable.bbox,
|
|
3630
|
+
frameWidth: imageWidth,
|
|
3631
|
+
frameHeight: imageHeight,
|
|
3632
|
+
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,
|
|
3633
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3634
|
+
hardAreaRatio: typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0
|
|
3635
|
+
}
|
|
3636
|
+
});
|
|
3589
3637
|
continue;
|
|
3590
3638
|
}
|
|
3639
|
+
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", {
|
|
3640
|
+
tags: { deviceId },
|
|
3641
|
+
meta: {
|
|
3642
|
+
macroClass: mutable.macroClass,
|
|
3643
|
+
score: mutable.score,
|
|
3644
|
+
bbox: mutable.bbox,
|
|
3645
|
+
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,
|
|
3646
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3647
|
+
hint: "arm fullFrameGuardHardAreaRatio on this camera if these are phantoms"
|
|
3648
|
+
}
|
|
3649
|
+
});
|
|
3591
3650
|
try {
|
|
3592
3651
|
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3593
3652
|
} catch (err) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
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-CsKaXmP8.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-
|
|
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-1GIwU6R8.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
6
|
import { spawn } from "node:child_process";
|
|
@@ -2696,19 +2696,25 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
|
|
|
2696
2696
|
* DROPS detections outright, so it sits between that and a hard cut).
|
|
2697
2697
|
*/
|
|
2698
2698
|
var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
2699
|
+
/** A cut only makes sense in (0, 1); anything else is treated as OFF rather
|
|
2700
|
+
* than dropping every detection on a typo. */
|
|
2701
|
+
function isUsableHardAreaCut(ratio) {
|
|
2702
|
+
return Number.isFinite(ratio) && ratio > 0 && ratio < 1;
|
|
2703
|
+
}
|
|
2699
2704
|
/**
|
|
2700
2705
|
* True when `bbox` is a near-full-frame LOW-confidence phantom that should be
|
|
2701
2706
|
* DROPPED from the frame's detections (overlay / tracker / events / motion-wake).
|
|
2702
2707
|
*
|
|
2703
2708
|
* Rule (drop ⇔ returns true):
|
|
2704
|
-
*
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2707
|
-
*
|
|
2709
|
+
* areaRatio ≥ `hardAreaRatio` ← score-independent, OFF by default
|
|
2710
|
+
* OR ( (areaRatio ≥ {@link FULL_FRAME_AREA_RATIO}
|
|
2711
|
+
* OR width ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameWidth
|
|
2712
|
+
* OR height ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameHeight)
|
|
2713
|
+
* AND score < `maxConfidence` )
|
|
2708
2714
|
*
|
|
2709
|
-
*
|
|
2710
|
-
* subject and
|
|
2711
|
-
* to reject on → not a phantom.
|
|
2715
|
+
* Below the hard cut a high-confidence detection (`score ≥ maxConfidence`) is
|
|
2716
|
+
* trusted as a real subject and survives. Degenerate frame dimensions (≤0)
|
|
2717
|
+
* carry no info to reject on → not a phantom.
|
|
2712
2718
|
*
|
|
2713
2719
|
* @param bbox absolute-pixel `[x1, y1, x2, y2]` box in source-frame coords.
|
|
2714
2720
|
* @param frameWidth source frame width in px.
|
|
@@ -2717,10 +2723,26 @@ var FULL_FRAME_MAX_CONFIDENCE = .7;
|
|
|
2717
2723
|
* @param maxConfidence confidence bar below which a full-frame box is a phantom
|
|
2718
2724
|
* (defaults to {@link FULL_FRAME_MAX_CONFIDENCE}; overridable by the caller so
|
|
2719
2725
|
* the bar can be threaded from detection settings without new cap methods).
|
|
2726
|
+
* @param hardAreaRatio area fraction at/above which the box is dropped NO
|
|
2727
|
+
* MATTER the score. Defaults to {@link HARD_AREA_CUT_DISABLED} (off) — it is
|
|
2728
|
+
* armed per camera, because a doorbell close-up legitimately fills the frame
|
|
2729
|
+
* while an overhead courtyard box never does. Threaded from step settings as
|
|
2730
|
+
* `fullFrameGuardHardAreaRatio`.
|
|
2720
2731
|
*/
|
|
2721
|
-
|
|
2732
|
+
/**
|
|
2733
|
+
* True when a box trips the guard's GEOMETRY test (near-full-frame, or
|
|
2734
|
+
* edge-to-edge in one axis) but SURVIVES because its score cleared the
|
|
2735
|
+
* confidence bar.
|
|
2736
|
+
*
|
|
2737
|
+
* These are the ones worth counting: on 2026-07-30 device 615 produced six
|
|
2738
|
+
* such boxes in a day, at 0.710-0.787, and they were only found by an offline
|
|
2739
|
+
* sweep of stored events — nothing in production reported them. Arming the
|
|
2740
|
+
* hard-area cut on a camera should be a decision made from this count, not
|
|
2741
|
+
* from one incident.
|
|
2742
|
+
*/
|
|
2743
|
+
function isFullFrameSurvivor(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE) {
|
|
2722
2744
|
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
2723
|
-
if (score
|
|
2745
|
+
if (score < maxConfidence) return false;
|
|
2724
2746
|
const [x1, y1, x2, y2] = bbox;
|
|
2725
2747
|
const w = x2 - x1;
|
|
2726
2748
|
const h = y2 - y1;
|
|
@@ -2729,6 +2751,19 @@ function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxCo
|
|
|
2729
2751
|
if (h >= frameHeight * .98) return true;
|
|
2730
2752
|
return false;
|
|
2731
2753
|
}
|
|
2754
|
+
function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE, hardAreaRatio = 1) {
|
|
2755
|
+
if (frameWidth <= 0 || frameHeight <= 0) return false;
|
|
2756
|
+
const [x1, y1, x2, y2] = bbox;
|
|
2757
|
+
const w = x2 - x1;
|
|
2758
|
+
const h = y2 - y1;
|
|
2759
|
+
const areaRatio = w * h / (frameWidth * frameHeight);
|
|
2760
|
+
if (isUsableHardAreaCut(hardAreaRatio) && areaRatio >= hardAreaRatio) return true;
|
|
2761
|
+
if (score >= maxConfidence) return false;
|
|
2762
|
+
if (areaRatio >= .9) return true;
|
|
2763
|
+
if (w >= frameWidth * .98) return true;
|
|
2764
|
+
if (h >= frameHeight * .98) return true;
|
|
2765
|
+
return false;
|
|
2766
|
+
}
|
|
2732
2767
|
//#endregion
|
|
2733
2768
|
//#region src/detection-pipeline/pipeline/plate-deskew.ts
|
|
2734
2769
|
/**
|
|
@@ -3576,10 +3611,34 @@ var PipelineExecutor = class {
|
|
|
3576
3611
|
continue;
|
|
3577
3612
|
}
|
|
3578
3613
|
const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
|
|
3579
|
-
|
|
3580
|
-
|
|
3614
|
+
const fullFrameHardArea = rootStep.settings?.["fullFrameGuardHardAreaRatio"];
|
|
3615
|
+
if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0, typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0)) {
|
|
3616
|
+
this.opts.logger?.info("full-frame phantom guard: detection dropped", {
|
|
3617
|
+
tags: { deviceId },
|
|
3618
|
+
meta: {
|
|
3619
|
+
macroClass: mutable.macroClass,
|
|
3620
|
+
score: mutable.score,
|
|
3621
|
+
bbox: mutable.bbox,
|
|
3622
|
+
frameWidth: imageWidth,
|
|
3623
|
+
frameHeight: imageHeight,
|
|
3624
|
+
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,
|
|
3625
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3626
|
+
hardAreaRatio: typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3581
3629
|
continue;
|
|
3582
3630
|
}
|
|
3631
|
+
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", {
|
|
3632
|
+
tags: { deviceId },
|
|
3633
|
+
meta: {
|
|
3634
|
+
macroClass: mutable.macroClass,
|
|
3635
|
+
score: mutable.score,
|
|
3636
|
+
bbox: mutable.bbox,
|
|
3637
|
+
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,
|
|
3638
|
+
bar: typeof fullFrameBar === "number" ? fullFrameBar : void 0,
|
|
3639
|
+
hint: "arm fullFrameGuardHardAreaRatio on this camera if these are phantoms"
|
|
3640
|
+
}
|
|
3641
|
+
});
|
|
3583
3642
|
try {
|
|
3584
3643
|
await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
|
|
3585
3644
|
} catch (err) {
|
|
@@ -7,7 +7,7 @@ const require_dist = require("../dist-DUHlZcWd.js");
|
|
|
7
7
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
8
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
9
|
const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
|
|
10
|
-
const require_step_definitions = require("../step-definitions-
|
|
10
|
+
const require_step_definitions = require("../step-definitions-Drb9cWbB.js");
|
|
11
11
|
let node_child_process = require("node:child_process");
|
|
12
12
|
let node_url = require("node:url");
|
|
13
13
|
let sharp = require("sharp");
|
|
@@ -658,6 +658,9 @@ function toFrameInput$1(frame) {
|
|
|
658
658
|
* without spinning up Moleculer or the addon framework.
|
|
659
659
|
*/
|
|
660
660
|
var PipelineRunner = class {
|
|
661
|
+
/** Abandoned inference calls that have not yet settled. A BURST reads as
|
|
662
|
+
* one rising number instead of N unrelated lines. */
|
|
663
|
+
inflightTimeouts = 0;
|
|
661
664
|
config;
|
|
662
665
|
cameras = /* @__PURE__ */ new Map();
|
|
663
666
|
semaphore;
|
|
@@ -1144,16 +1147,33 @@ var PipelineRunner = class {
|
|
|
1144
1147
|
timer.unref?.();
|
|
1145
1148
|
});
|
|
1146
1149
|
try {
|
|
1147
|
-
const
|
|
1150
|
+
const inflight = this.config.processFrame(deviceId, frameInput, executorHandle);
|
|
1151
|
+
const outcome = await Promise.race([inflight, expiry]);
|
|
1148
1152
|
if (outcome === TIMED_OUT) {
|
|
1149
1153
|
release();
|
|
1154
|
+
this.inflightTimeouts += 1;
|
|
1150
1155
|
this.logger?.warn("inference timed out — permit released, result discarded", {
|
|
1151
1156
|
tags: { deviceId },
|
|
1152
1157
|
meta: {
|
|
1153
1158
|
timeoutMs,
|
|
1154
|
-
availablePermits: this.semaphore.available
|
|
1159
|
+
availablePermits: this.semaphore.available,
|
|
1160
|
+
outstandingTimeouts: this.inflightTimeouts
|
|
1155
1161
|
}
|
|
1156
1162
|
});
|
|
1163
|
+
const deadlineAt = Date.now();
|
|
1164
|
+
const noteSettled = (fate) => {
|
|
1165
|
+
this.inflightTimeouts = Math.max(0, this.inflightTimeouts - 1);
|
|
1166
|
+
this.logger?.info("inference: abandoned call settled after the deadline", {
|
|
1167
|
+
tags: { deviceId },
|
|
1168
|
+
meta: {
|
|
1169
|
+
lateByMs: Date.now() - deadlineAt,
|
|
1170
|
+
totalMs: timeoutMs + (Date.now() - deadlineAt),
|
|
1171
|
+
fate,
|
|
1172
|
+
outstandingTimeouts: this.inflightTimeouts
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
};
|
|
1176
|
+
inflight.then(() => noteSettled("resolved"), () => noteSettled("rejected"));
|
|
1157
1177
|
}
|
|
1158
1178
|
return outcome;
|
|
1159
1179
|
} finally {
|
|
@@ -1336,7 +1356,24 @@ function forkDecodeWorker(_source) {
|
|
|
1336
1356
|
//#endregion
|
|
1337
1357
|
//#region src/session-decode/guarded-session-decode.ts
|
|
1338
1358
|
/**
|
|
1339
|
-
*
|
|
1359
|
+
* 2s → 5s → 15s → 30s, then 30s forever.
|
|
1360
|
+
*
|
|
1361
|
+
* Indefinite is deliberate. The failure this recovers from is "the broker has
|
|
1362
|
+
* no profile bound yet" or "the broker call failed right now" — both of which
|
|
1363
|
+
* clear on their own, often minutes later (a source re-dialing, a runner
|
|
1364
|
+
* restarting). A bounded retry would turn a slow recovery back into the
|
|
1365
|
+
* permanent outage this exists to prevent, and teardown already stops it: a
|
|
1366
|
+
* detach aborts the loop, so nothing retries for a camera nobody wants.
|
|
1367
|
+
*/
|
|
1368
|
+
var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
|
|
1369
|
+
2e3,
|
|
1370
|
+
5e3,
|
|
1371
|
+
15e3,
|
|
1372
|
+
3e4
|
|
1373
|
+
];
|
|
1374
|
+
/**
|
|
1375
|
+
* Guards the in-flight restream-acquire against a detach/unsubscribe race,
|
|
1376
|
+
* and retries an acquire that fails.
|
|
1340
1377
|
*
|
|
1341
1378
|
* `acquireSessionDecodeRestream` + forking the decode worker both take real
|
|
1342
1379
|
* time (a broker round-trip, then a process fork). If the caller's teardown
|
|
@@ -1348,29 +1385,71 @@ function forkDecodeWorker(_source) {
|
|
|
1348
1385
|
*
|
|
1349
1386
|
* This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
|
|
1350
1387
|
* run in the background; an `aborted` flag recorded by the (possibly
|
|
1351
|
-
* already-called) teardown is checked right after
|
|
1352
|
-
* if set,
|
|
1388
|
+
* already-called) teardown is checked right after each acquire resolves —
|
|
1389
|
+
* if set, any restream is released immediately and the pump is NEVER
|
|
1353
1390
|
* started (no orphan worker). Otherwise the pump starts and the teardown,
|
|
1354
1391
|
* once called, stops the pump then releases the restream.
|
|
1392
|
+
*
|
|
1393
|
+
* **A failed acquire is retried, not swallowed.** The teardown handed back
|
|
1394
|
+
* here is stored by the caller (as `attachment.detectionUnsubscribe`) the
|
|
1395
|
+
* instant this returns, so a `return` on failure left that handle permanently
|
|
1396
|
+
* non-null with no pump behind it: `onEnded` could never fire because nothing
|
|
1397
|
+
* had ever started, the camera's phase stayed `active`, and not one frame
|
|
1398
|
+
* arrived again. The motion path was hardened against this shape by a frame
|
|
1399
|
+
* freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
|
|
1400
|
+
* path's half.
|
|
1355
1401
|
*/
|
|
1356
1402
|
function startGuardedSessionDecode(deps) {
|
|
1357
1403
|
let aborted = false;
|
|
1358
1404
|
let stopPump = null;
|
|
1359
1405
|
let release = null;
|
|
1406
|
+
let retryTimer = null;
|
|
1407
|
+
const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
|
|
1408
|
+
/** Resolves after the backoff for `attempt`, or immediately once aborted. */
|
|
1409
|
+
const waitBackoff = (attempt) => new Promise((resolve) => {
|
|
1410
|
+
const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
|
|
1411
|
+
retryTimer = setTimeout(() => {
|
|
1412
|
+
retryTimer = null;
|
|
1413
|
+
resolve();
|
|
1414
|
+
}, ms);
|
|
1415
|
+
retryTimer.unref?.();
|
|
1416
|
+
});
|
|
1360
1417
|
(async () => {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1418
|
+
for (let attempt = 0; !aborted; attempt++) {
|
|
1419
|
+
let acquired = null;
|
|
1420
|
+
try {
|
|
1421
|
+
acquired = await deps.acquire();
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
|
|
1424
|
+
attempt: attempt + 1,
|
|
1425
|
+
error: require_dist.errMsg(err)
|
|
1426
|
+
} });
|
|
1427
|
+
await waitBackoff(attempt);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (aborted) {
|
|
1431
|
+
if (acquired) acquired.release();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (!acquired) {
|
|
1435
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
|
|
1436
|
+
await waitBackoff(attempt);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
|
|
1440
|
+
release = acquired.release;
|
|
1441
|
+
stopPump = deps.startPump(acquired.source);
|
|
1365
1442
|
return;
|
|
1366
1443
|
}
|
|
1367
|
-
release = acquired.release;
|
|
1368
|
-
stopPump = deps.startPump(acquired.source);
|
|
1369
1444
|
})().catch((err) => {
|
|
1370
1445
|
deps.logger.warn("session-decode: guarded start failed", { meta: { error: require_dist.errMsg(err) } });
|
|
1371
1446
|
});
|
|
1372
1447
|
return () => {
|
|
1373
1448
|
aborted = true;
|
|
1449
|
+
if (retryTimer !== null) {
|
|
1450
|
+
clearTimeout(retryTimer);
|
|
1451
|
+
retryTimer = null;
|
|
1452
|
+
}
|
|
1374
1453
|
stopPump?.();
|
|
1375
1454
|
if (release) release();
|
|
1376
1455
|
};
|
|
@@ -2,7 +2,7 @@ import { B as errMsg, G as createEvent, M as pipelineRunnerCapability, S as defi
|
|
|
2
2
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { t as isWorkerReply } from "../worker-protocol-D7RzZIla.mjs";
|
|
5
|
-
import { a as getStepDefinition } from "../step-definitions-
|
|
5
|
+
import { a as getStepDefinition } from "../step-definitions-1GIwU6R8.mjs";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import sharp from "sharp";
|
|
@@ -652,6 +652,9 @@ function toFrameInput$1(frame) {
|
|
|
652
652
|
* without spinning up Moleculer or the addon framework.
|
|
653
653
|
*/
|
|
654
654
|
var PipelineRunner = class {
|
|
655
|
+
/** Abandoned inference calls that have not yet settled. A BURST reads as
|
|
656
|
+
* one rising number instead of N unrelated lines. */
|
|
657
|
+
inflightTimeouts = 0;
|
|
655
658
|
config;
|
|
656
659
|
cameras = /* @__PURE__ */ new Map();
|
|
657
660
|
semaphore;
|
|
@@ -1138,16 +1141,33 @@ var PipelineRunner = class {
|
|
|
1138
1141
|
timer.unref?.();
|
|
1139
1142
|
});
|
|
1140
1143
|
try {
|
|
1141
|
-
const
|
|
1144
|
+
const inflight = this.config.processFrame(deviceId, frameInput, executorHandle);
|
|
1145
|
+
const outcome = await Promise.race([inflight, expiry]);
|
|
1142
1146
|
if (outcome === TIMED_OUT) {
|
|
1143
1147
|
release();
|
|
1148
|
+
this.inflightTimeouts += 1;
|
|
1144
1149
|
this.logger?.warn("inference timed out — permit released, result discarded", {
|
|
1145
1150
|
tags: { deviceId },
|
|
1146
1151
|
meta: {
|
|
1147
1152
|
timeoutMs,
|
|
1148
|
-
availablePermits: this.semaphore.available
|
|
1153
|
+
availablePermits: this.semaphore.available,
|
|
1154
|
+
outstandingTimeouts: this.inflightTimeouts
|
|
1149
1155
|
}
|
|
1150
1156
|
});
|
|
1157
|
+
const deadlineAt = Date.now();
|
|
1158
|
+
const noteSettled = (fate) => {
|
|
1159
|
+
this.inflightTimeouts = Math.max(0, this.inflightTimeouts - 1);
|
|
1160
|
+
this.logger?.info("inference: abandoned call settled after the deadline", {
|
|
1161
|
+
tags: { deviceId },
|
|
1162
|
+
meta: {
|
|
1163
|
+
lateByMs: Date.now() - deadlineAt,
|
|
1164
|
+
totalMs: timeoutMs + (Date.now() - deadlineAt),
|
|
1165
|
+
fate,
|
|
1166
|
+
outstandingTimeouts: this.inflightTimeouts
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
inflight.then(() => noteSettled("resolved"), () => noteSettled("rejected"));
|
|
1151
1171
|
}
|
|
1152
1172
|
return outcome;
|
|
1153
1173
|
} finally {
|
|
@@ -1330,7 +1350,24 @@ function forkDecodeWorker(_source) {
|
|
|
1330
1350
|
//#endregion
|
|
1331
1351
|
//#region src/session-decode/guarded-session-decode.ts
|
|
1332
1352
|
/**
|
|
1333
|
-
*
|
|
1353
|
+
* 2s → 5s → 15s → 30s, then 30s forever.
|
|
1354
|
+
*
|
|
1355
|
+
* Indefinite is deliberate. The failure this recovers from is "the broker has
|
|
1356
|
+
* no profile bound yet" or "the broker call failed right now" — both of which
|
|
1357
|
+
* clear on their own, often minutes later (a source re-dialing, a runner
|
|
1358
|
+
* restarting). A bounded retry would turn a slow recovery back into the
|
|
1359
|
+
* permanent outage this exists to prevent, and teardown already stops it: a
|
|
1360
|
+
* detach aborts the loop, so nothing retries for a camera nobody wants.
|
|
1361
|
+
*/
|
|
1362
|
+
var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
|
|
1363
|
+
2e3,
|
|
1364
|
+
5e3,
|
|
1365
|
+
15e3,
|
|
1366
|
+
3e4
|
|
1367
|
+
];
|
|
1368
|
+
/**
|
|
1369
|
+
* Guards the in-flight restream-acquire against a detach/unsubscribe race,
|
|
1370
|
+
* and retries an acquire that fails.
|
|
1334
1371
|
*
|
|
1335
1372
|
* `acquireSessionDecodeRestream` + forking the decode worker both take real
|
|
1336
1373
|
* time (a broker round-trip, then a process fork). If the caller's teardown
|
|
@@ -1342,29 +1379,71 @@ function forkDecodeWorker(_source) {
|
|
|
1342
1379
|
*
|
|
1343
1380
|
* This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
|
|
1344
1381
|
* run in the background; an `aborted` flag recorded by the (possibly
|
|
1345
|
-
* already-called) teardown is checked right after
|
|
1346
|
-
* if set,
|
|
1382
|
+
* already-called) teardown is checked right after each acquire resolves —
|
|
1383
|
+
* if set, any restream is released immediately and the pump is NEVER
|
|
1347
1384
|
* started (no orphan worker). Otherwise the pump starts and the teardown,
|
|
1348
1385
|
* once called, stops the pump then releases the restream.
|
|
1386
|
+
*
|
|
1387
|
+
* **A failed acquire is retried, not swallowed.** The teardown handed back
|
|
1388
|
+
* here is stored by the caller (as `attachment.detectionUnsubscribe`) the
|
|
1389
|
+
* instant this returns, so a `return` on failure left that handle permanently
|
|
1390
|
+
* non-null with no pump behind it: `onEnded` could never fire because nothing
|
|
1391
|
+
* had ever started, the camera's phase stayed `active`, and not one frame
|
|
1392
|
+
* arrived again. The motion path was hardened against this shape by a frame
|
|
1393
|
+
* freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
|
|
1394
|
+
* path's half.
|
|
1349
1395
|
*/
|
|
1350
1396
|
function startGuardedSessionDecode(deps) {
|
|
1351
1397
|
let aborted = false;
|
|
1352
1398
|
let stopPump = null;
|
|
1353
1399
|
let release = null;
|
|
1400
|
+
let retryTimer = null;
|
|
1401
|
+
const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
|
|
1402
|
+
/** Resolves after the backoff for `attempt`, or immediately once aborted. */
|
|
1403
|
+
const waitBackoff = (attempt) => new Promise((resolve) => {
|
|
1404
|
+
const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
|
|
1405
|
+
retryTimer = setTimeout(() => {
|
|
1406
|
+
retryTimer = null;
|
|
1407
|
+
resolve();
|
|
1408
|
+
}, ms);
|
|
1409
|
+
retryTimer.unref?.();
|
|
1410
|
+
});
|
|
1354
1411
|
(async () => {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1412
|
+
for (let attempt = 0; !aborted; attempt++) {
|
|
1413
|
+
let acquired = null;
|
|
1414
|
+
try {
|
|
1415
|
+
acquired = await deps.acquire();
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
|
|
1418
|
+
attempt: attempt + 1,
|
|
1419
|
+
error: errMsg(err)
|
|
1420
|
+
} });
|
|
1421
|
+
await waitBackoff(attempt);
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (aborted) {
|
|
1425
|
+
if (acquired) acquired.release();
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (!acquired) {
|
|
1429
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
|
|
1430
|
+
await waitBackoff(attempt);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
|
|
1434
|
+
release = acquired.release;
|
|
1435
|
+
stopPump = deps.startPump(acquired.source);
|
|
1359
1436
|
return;
|
|
1360
1437
|
}
|
|
1361
|
-
release = acquired.release;
|
|
1362
|
-
stopPump = deps.startPump(acquired.source);
|
|
1363
1438
|
})().catch((err) => {
|
|
1364
1439
|
deps.logger.warn("session-decode: guarded start failed", { meta: { error: errMsg(err) } });
|
|
1365
1440
|
});
|
|
1366
1441
|
return () => {
|
|
1367
1442
|
aborted = true;
|
|
1443
|
+
if (retryTimer !== null) {
|
|
1444
|
+
clearTimeout(retryTimer);
|
|
1445
|
+
retryTimer = null;
|
|
1446
|
+
}
|
|
1368
1447
|
stopPump?.();
|
|
1369
1448
|
if (release) release();
|
|
1370
1449
|
};
|
|
@@ -1296,7 +1296,18 @@ var ObjectDetectionStep = class {
|
|
|
1296
1296
|
min: 0,
|
|
1297
1297
|
max: 1,
|
|
1298
1298
|
step: .05,
|
|
1299
|
-
default: .
|
|
1299
|
+
default: .65,
|
|
1300
|
+
showValue: true
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
type: "slider",
|
|
1304
|
+
key: "fullFrameGuardHardAreaRatio",
|
|
1305
|
+
label: "Drop full-frame boxes above",
|
|
1306
|
+
description: "Fraction of the frame at or above which a detection is dropped WHATEVER its score. 1 = off. Use on cameras where a subject can never fill the frame; leave off for doorbells and close-up lenses.",
|
|
1307
|
+
min: .5,
|
|
1308
|
+
max: 1,
|
|
1309
|
+
step: .01,
|
|
1310
|
+
default: 1,
|
|
1300
1311
|
showValue: true
|
|
1301
1312
|
},
|
|
1302
1313
|
{
|
|
@@ -1296,7 +1296,18 @@ var ObjectDetectionStep = class {
|
|
|
1296
1296
|
min: 0,
|
|
1297
1297
|
max: 1,
|
|
1298
1298
|
step: .05,
|
|
1299
|
-
default: .
|
|
1299
|
+
default: .65,
|
|
1300
|
+
showValue: true
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
type: "slider",
|
|
1304
|
+
key: "fullFrameGuardHardAreaRatio",
|
|
1305
|
+
label: "Drop full-frame boxes above",
|
|
1306
|
+
description: "Fraction of the frame at or above which a detection is dropped WHATEVER its score. 1 = off. Use on cameras where a subject can never fill the frame; leave off for doorbells and close-up lenses.",
|
|
1307
|
+
min: .5,
|
|
1308
|
+
max: 1,
|
|
1309
|
+
step: .01,
|
|
1310
|
+
default: 1,
|
|
1300
1311
|
showValue: true
|
|
1301
1312
|
},
|
|
1302
1313
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.29",
|
|
4
4
|
"description": "CamStack Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|