@camstack/addon-pipeline 1.2.196 → 1.2.197
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/{default-detection-model-CJdc8j83.js → default-detection-model-CM-8U0Bb.js} +1 -1
- package/dist/{default-detection-model-BGBAmtEp.mjs → default-detection-model-CplzzTmN.mjs} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-HG5geL9t.js → dist-CBa1j3On.js} +141 -35
- package/dist/{dist-mYDK58Jp.mjs → dist-Y4BsF_EC.mjs} +130 -36
- package/dist/{lazy-sharp-DmAwZ0s9.js → lazy-sharp-CS5WE9mI.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-xRsaPwBa.mjs → node-B5ouEQYN.mjs} +1 -1
- package/dist/{node--my8JTOW.js → node-C-B8VuGi.js} +1 -1
- package/dist/pipeline-runner/index.js +363 -6
- package/dist/pipeline-runner/index.mjs +362 -5
- package/dist/{process-memory-CoCSce63.js → process-memory-CmeKr_MZ.js} +1 -1
- package/dist/{process-memory-BDHR3lxe.mjs → process-memory-DeO2OZ3-.mjs} +1 -1
- package/dist/recorder/index.js +131 -18
- package/dist/recorder/index.mjs +131 -18
- package/dist/{retire-root-keys-KE6D6Xh_.js → retire-root-keys-Csb8z2f3.js} +19 -0
- package/dist/{retire-root-keys-DMolfhsP.mjs → retire-root-keys-DZvlso2l.mjs} +19 -0
- package/dist/{segment-demux-js-CXvgKfsn.mjs → segment-demux-js-DXPgq2cf.mjs} +1 -1
- package/dist/{segment-demux-js-CBynn9Gh.js → segment-demux-js-DiIsywGz.js} +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-BMsB2MVD.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Q-6HzWJ0.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_NVJXAy.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cv5McbTI.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 → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs} +1 -1
- 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-CZig_taJ.mjs → hostInit-C-2Eo_nr.mjs} +3 -3
- package/dist/stream-broker/index.js +4 -4
- package/dist/stream-broker/index.mjs +4 -4
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DCsi4YS1.js → worker-protocol-CXsdriRX.js} +1 -1
- package/dist/{worker-protocol-C0R1DDC0.mjs → worker-protocol-_7zrToTo.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $t as
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
3
|
-
import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-
|
|
4
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { $t as literal, Dt as errMsg, Jt as _enum, Pt as BaseAddon, Qt as lazy, R as customAction, Rt as createEvent, U as deriveDetailCropRect, Ut as nodePin, V as defineCustomActions, Xt as boolean, Yt as array, Z as failureContributionCapability, _ as FailureCounters, dt as pickDetailCropConvention, en as number, ft as pickNativeLeaseOverride, in as string, m as DEVICE_BACKEND_TO_FORMAT, mt as pipelineRunnerCapability, on as EventCategory, p as DEFAULT_NATIVE_LEASE_SETTINGS, tn as object, tt as loadContributionCapability, u as DEFAULT_DETAIL_CROP_CONVENTION } from "../dist-Y4BsF_EC.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-B5ouEQYN.mjs";
|
|
3
|
+
import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-CplzzTmN.mjs";
|
|
4
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-_7zrToTo.mjs";
|
|
5
5
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
6
6
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
7
7
|
import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
|
|
@@ -1970,6 +1970,169 @@ function buildBenchRunPipelineInput(req) {
|
|
|
1970
1970
|
...req.plane ? { plane: req.plane } : {}
|
|
1971
1971
|
};
|
|
1972
1972
|
}
|
|
1973
|
+
/**
|
|
1974
|
+
* How long a box stays "seen".
|
|
1975
|
+
*
|
|
1976
|
+
* Sized on the gate's own clock, not on a guess: `maxDeferralMs` defaults to
|
|
1977
|
+
* 2 000 ms, so a birth's whole life as a candidate is 2 s. A window shorter
|
|
1978
|
+
* than that would re-cut the same subject inside one deferral; a much longer
|
|
1979
|
+
* one would stop a subject that genuinely left and returned from being cut
|
|
1980
|
+
* again. 3 000 ms is the deferral budget plus one 1 s margin.
|
|
1981
|
+
*/
|
|
1982
|
+
var BIRTH_EVIDENCE_NOVELTY_WINDOW_MS = 3e3;
|
|
1983
|
+
/**
|
|
1984
|
+
* Padding around the box, as a fraction of its size.
|
|
1985
|
+
*
|
|
1986
|
+
* 0.15 deliberately equals `CONFIRMATION_CROP_PADDING` in post-analysis: the
|
|
1987
|
+
* gate promotes a secondary subject by mapping a crop-space box back into the
|
|
1988
|
+
* frame through the rectangle the crop was cut from, and the two derivations
|
|
1989
|
+
* disagreeing is how a promoted box lands somewhere the pixels never were.
|
|
1990
|
+
*/
|
|
1991
|
+
var BIRTH_EVIDENCE_PADDING = .15;
|
|
1992
|
+
/**
|
|
1993
|
+
* Boxes remembered per camera. A frame with more subjects than this keeps the
|
|
1994
|
+
* most recent — the cap is about bounding a crowd, and a box that fell out is
|
|
1995
|
+
* merely eligible for a cut it will not get twice (the encode budget above is
|
|
1996
|
+
* the real spend). 64 is 16x the tile tier's per-frame subject cap.
|
|
1997
|
+
*/
|
|
1998
|
+
var REMEMBERED_BOXES_PER_DEVICE = 64;
|
|
1999
|
+
/**
|
|
2000
|
+
* Per-camera memory of the boxes recently on screen.
|
|
2001
|
+
*
|
|
2002
|
+
* Per CAMERA and never global: a box on 617 says nothing about 615, and a
|
|
2003
|
+
* shared structure would make one camera's traffic decide another's cuts —
|
|
2004
|
+
* which is the exact shape of the node-wide sharing this whole change moves
|
|
2005
|
+
* away from.
|
|
2006
|
+
*
|
|
2007
|
+
* Judged per CLASS. A dog at its owner's knee is 100% contained in the person's
|
|
2008
|
+
* box; a class-blind test would call it "already seen" and it would reach the
|
|
2009
|
+
* gate with no pixels — the failure D372, D377 and D378 each fought at a
|
|
2010
|
+
* different layer.
|
|
2011
|
+
*/
|
|
2012
|
+
var BirthNoveltyMemory = class {
|
|
2013
|
+
byDevice = /* @__PURE__ */ new Map();
|
|
2014
|
+
/**
|
|
2015
|
+
* Is this box a subject the camera has not been showing?
|
|
2016
|
+
*
|
|
2017
|
+
* Pure: it does not record anything. The caller records the whole frame with
|
|
2018
|
+
* {@link note}, once, so a box that lost the encode budget is still counted
|
|
2019
|
+
* as seen — otherwise the cap turns into a permanent re-cut of the same
|
|
2020
|
+
* subjects.
|
|
2021
|
+
*/
|
|
2022
|
+
isNovel(deviceId, box, macroClass, nowMs) {
|
|
2023
|
+
const boxes = this.byDevice.get(deviceId);
|
|
2024
|
+
if (!boxes) return true;
|
|
2025
|
+
for (const remembered of boxes) {
|
|
2026
|
+
if (nowMs - remembered.atMs > 3e3) continue;
|
|
2027
|
+
if (remembered.macroClass !== macroClass) continue;
|
|
2028
|
+
if (iou(remembered.rect, box) >= .3) return false;
|
|
2029
|
+
}
|
|
2030
|
+
return true;
|
|
2031
|
+
}
|
|
2032
|
+
/** Record a frame's detections and drop everything past the window / the cap. */
|
|
2033
|
+
note(deviceId, detections, nowMs) {
|
|
2034
|
+
const kept = (this.byDevice.get(deviceId) ?? []).filter((b) => nowMs - b.atMs <= BIRTH_EVIDENCE_NOVELTY_WINDOW_MS);
|
|
2035
|
+
for (const d of detections) {
|
|
2036
|
+
if (!isUsable$1(d.bbox)) continue;
|
|
2037
|
+
kept.push({
|
|
2038
|
+
rect: d.bbox,
|
|
2039
|
+
macroClass: d.macroClass,
|
|
2040
|
+
atMs: nowMs
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
this.byDevice.set(deviceId, kept.length > REMEMBERED_BOXES_PER_DEVICE ? kept.slice(kept.length - REMEMBERED_BOXES_PER_DEVICE) : kept);
|
|
2044
|
+
}
|
|
2045
|
+
/** How many boxes this camera currently remembers (tests / diagnostics). */
|
|
2046
|
+
rememberedCount(deviceId) {
|
|
2047
|
+
return this.byDevice.get(deviceId)?.length ?? 0;
|
|
2048
|
+
}
|
|
2049
|
+
/** Forget a camera wholesale (detach, pipeline reset). */
|
|
2050
|
+
clearDevice(deviceId) {
|
|
2051
|
+
this.byDevice.delete(deviceId);
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
/**
|
|
2055
|
+
* The cuts worth taking on this frame, highest confidence first, capped.
|
|
2056
|
+
*
|
|
2057
|
+
* Records EVERY usable detection in `memory` — including the ones the cap
|
|
2058
|
+
* refused — so the budget bounds the spend without turning into a leak.
|
|
2059
|
+
*/
|
|
2060
|
+
function planBirthEvidence(deviceId, detections, frame, memory, nowMs, maxPerFrame = 2) {
|
|
2061
|
+
if (frame.width <= 0 || frame.height <= 0) return [];
|
|
2062
|
+
const usable = detections.map((d, index) => ({
|
|
2063
|
+
d,
|
|
2064
|
+
index
|
|
2065
|
+
})).filter(({ d }) => isUsable$1(d.bbox));
|
|
2066
|
+
const novel = usable.filter(({ d }) => memory.isNovel(deviceId, d.bbox, d.macroClass, nowMs));
|
|
2067
|
+
memory.note(deviceId, usable.map(({ d }) => ({
|
|
2068
|
+
bbox: d.bbox,
|
|
2069
|
+
macroClass: d.macroClass
|
|
2070
|
+
})), nowMs);
|
|
2071
|
+
const plan = [];
|
|
2072
|
+
for (const { d, index } of [...novel].sort((a, b) => b.d.score - a.d.score)) {
|
|
2073
|
+
if (plan.length >= maxPerFrame) break;
|
|
2074
|
+
const rect = padAndClamp(d.bbox, frame);
|
|
2075
|
+
if (rect === null) continue;
|
|
2076
|
+
plan.push({
|
|
2077
|
+
index,
|
|
2078
|
+
rect,
|
|
2079
|
+
target: targetFor(rect)
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
return plan.sort((a, b) => a.index - b.index);
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* The padded, clamped source rectangle — or `null` for a box that is not worth
|
|
2086
|
+
* an encode. A degenerate box is DROPPED rather than clamped: a zero-area crop
|
|
2087
|
+
* serves nothing and a NaN one would poison the encoder's geometry.
|
|
2088
|
+
*/
|
|
2089
|
+
function padAndClamp(box, frame) {
|
|
2090
|
+
if (Math.min(box.width, box.height) < 128) return null;
|
|
2091
|
+
const padX = box.width * BIRTH_EVIDENCE_PADDING;
|
|
2092
|
+
const padY = box.height * BIRTH_EVIDENCE_PADDING;
|
|
2093
|
+
const left = Math.max(0, Math.floor(box.x - padX));
|
|
2094
|
+
const top = Math.max(0, Math.floor(box.y - padY));
|
|
2095
|
+
const right = Math.min(frame.width, Math.ceil(box.x + box.width + padX));
|
|
2096
|
+
const bottom = Math.min(frame.height, Math.ceil(box.y + box.height + padY));
|
|
2097
|
+
const width = right - left;
|
|
2098
|
+
const height = bottom - top;
|
|
2099
|
+
if (width <= 0 || height <= 0) return null;
|
|
2100
|
+
return {
|
|
2101
|
+
x: left,
|
|
2102
|
+
y: top,
|
|
2103
|
+
width,
|
|
2104
|
+
height
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
/** Encoded size: capped, aspect-preserving, never upscaled. */
|
|
2108
|
+
function targetFor(rect) {
|
|
2109
|
+
if (rect.width <= 320) return {
|
|
2110
|
+
width: rect.width,
|
|
2111
|
+
height: rect.height
|
|
2112
|
+
};
|
|
2113
|
+
const scale = 320 / rect.width;
|
|
2114
|
+
return {
|
|
2115
|
+
width: 320,
|
|
2116
|
+
height: Math.max(1, Math.round(rect.height * scale))
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
/** A box with finite, positive extent. */
|
|
2120
|
+
function isUsable$1(box) {
|
|
2121
|
+
return Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height) && box.width > 0 && box.height > 0;
|
|
2122
|
+
}
|
|
2123
|
+
/** Intersection over union of two absolute-pixel rectangles. */
|
|
2124
|
+
function iou(a, b) {
|
|
2125
|
+
const left = Math.max(a.x, b.x);
|
|
2126
|
+
const top = Math.max(a.y, b.y);
|
|
2127
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
2128
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
2129
|
+
const w = right - left;
|
|
2130
|
+
const h = bottom - top;
|
|
2131
|
+
if (w <= 0 || h <= 0) return 0;
|
|
2132
|
+
const intersection = w * h;
|
|
2133
|
+
const union = a.width * a.height + b.width * b.height - intersection;
|
|
2134
|
+
return union <= 0 ? 0 : intersection / union;
|
|
2135
|
+
}
|
|
1973
2136
|
//#endregion
|
|
1974
2137
|
//#region src/pipeline-runner/capacity/capacity-runner.ts
|
|
1975
2138
|
/**
|
|
@@ -5532,6 +5695,15 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
|
|
|
5532
5695
|
var MAX_BOUND_FRAME_CUTTERS = 256;
|
|
5533
5696
|
/** How often a camera reports its undispatched cuts. */
|
|
5534
5697
|
var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
|
|
5698
|
+
/**
|
|
5699
|
+
* How often a camera reports what carrying its birth evidence COST (D379).
|
|
5700
|
+
*
|
|
5701
|
+
* The cost of this change is the one thing that must be measured rather than
|
|
5702
|
+
* estimated, so the report is per camera, states encodes / bytes / encode-ms
|
|
5703
|
+
* over the window, and drains its counters — a camera that cut nothing says
|
|
5704
|
+
* nothing until it does.
|
|
5705
|
+
*/
|
|
5706
|
+
var BIRTH_EVIDENCE_REPORT_INTERVAL_MS = 6e5;
|
|
5535
5707
|
var DEFAULT_CONFIG = {
|
|
5536
5708
|
maxQueueDepth: 30,
|
|
5537
5709
|
maxConcurrentInferences: 16,
|
|
@@ -5893,6 +6065,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
5893
6065
|
tileCutDrops = /* @__PURE__ */ new Map();
|
|
5894
6066
|
tileCutDropLoggedAt = /* @__PURE__ */ new Map();
|
|
5895
6067
|
/**
|
|
6068
|
+
* Per-camera memory of the boxes recently on screen — what decides whether a
|
|
6069
|
+
* detection is worth cutting its own evidence for (D379). Per CAMERA, never
|
|
6070
|
+
* global: one camera's traffic must never decide another's cuts.
|
|
6071
|
+
*/
|
|
6072
|
+
birthNovelty = new BirthNoveltyMemory();
|
|
6073
|
+
/** Per-camera cost of the carried evidence, drained by its report line. */
|
|
6074
|
+
birthEvidenceCost = /* @__PURE__ */ new Map();
|
|
6075
|
+
birthEvidenceReportedAt = /* @__PURE__ */ new Map();
|
|
6076
|
+
/**
|
|
5896
6077
|
* Node-level RAM store of retained rgb detection frames, keyed by the
|
|
5897
6078
|
* `FrameHandle` that rides the inference-result event (shm-ring removal
|
|
5898
6079
|
* 2026-07-16 — replaces the `csr.` shm retention ring). Backs the
|
|
@@ -6170,7 +6351,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6170
6351
|
this.runner.onResult(async (deviceId, frame, result, _streamType, handle) => {
|
|
6171
6352
|
this.retention?.noteInferenceResult(deviceId, handle, result);
|
|
6172
6353
|
this.cutSubjectTiles(deviceId, result, handle);
|
|
6173
|
-
this.
|
|
6354
|
+
const carried = await this.attachBirthEvidence(deviceId, frame, result);
|
|
6355
|
+
this.emitInferenceResult(deviceId, frame, carried, handle);
|
|
6174
6356
|
const detectionCount = result.detections?.length ?? 0;
|
|
6175
6357
|
this.runner?.noteDetectionResult(deviceId, detectionCount);
|
|
6176
6358
|
if (detectionCount > 0 && this.occupancyBurstInFlight.has(deviceId)) this.occupancyBurstFound.add(deviceId);
|
|
@@ -7158,6 +7340,178 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7158
7340
|
* stays at debug; a miss the caller is handed as `null` is lost work and is a
|
|
7159
7341
|
* warn, carrying the numbers that decide the fix.
|
|
7160
7342
|
*/
|
|
7343
|
+
/**
|
|
7344
|
+
* Cut each candidate birth's own pixels from the frame that produced it, and
|
|
7345
|
+
* return a result carrying them (D379).
|
|
7346
|
+
*
|
|
7347
|
+
* Returns the SAME result object when there is nothing to cut, which is the
|
|
7348
|
+
* overwhelming majority of frames: a subject already on screen is not a
|
|
7349
|
+
* birth, so it costs a novelty lookup and no encode. A new result object is
|
|
7350
|
+
* built when there is — never a mutation of the one the executor produced.
|
|
7351
|
+
*
|
|
7352
|
+
* Every drop is counted and reported per camera. A branch that silently
|
|
7353
|
+
* dropped the evidence would be indistinguishable from a camera with no
|
|
7354
|
+
* births at all, which is the shape of failure this whole change exists to
|
|
7355
|
+
* remove.
|
|
7356
|
+
*/
|
|
7357
|
+
async attachBirthEvidence(deviceId, frame, result) {
|
|
7358
|
+
const detections = result.detections;
|
|
7359
|
+
if (detections.length === 0) return result;
|
|
7360
|
+
const plan = planBirthEvidence(deviceId, detections.map((d) => ({
|
|
7361
|
+
bbox: d.bbox,
|
|
7362
|
+
score: d.score,
|
|
7363
|
+
macroClass: d.macroClass
|
|
7364
|
+
})), {
|
|
7365
|
+
width: result.width,
|
|
7366
|
+
height: result.height
|
|
7367
|
+
}, this.birthNovelty, Date.now());
|
|
7368
|
+
if (plan.length === 0) return result;
|
|
7369
|
+
const cost = this.birthEvidenceCost.get(deviceId) ?? {
|
|
7370
|
+
cut: 0,
|
|
7371
|
+
failed: 0,
|
|
7372
|
+
skippedNoPixels: 0,
|
|
7373
|
+
bytes: 0,
|
|
7374
|
+
encodeMs: 0
|
|
7375
|
+
};
|
|
7376
|
+
this.birthEvidenceCost.set(deviceId, cost);
|
|
7377
|
+
const byIndex = /* @__PURE__ */ new Map();
|
|
7378
|
+
if (!(frame.frameRef !== void 0 || frame.format === "rgb" && frame.data.byteLength >= result.width * result.height * 3)) {
|
|
7379
|
+
cost.skippedNoPixels += plan.length;
|
|
7380
|
+
this.reportBirthEvidenceCost(deviceId);
|
|
7381
|
+
return result;
|
|
7382
|
+
}
|
|
7383
|
+
for (const entry of plan) {
|
|
7384
|
+
const startedAt = Date.now();
|
|
7385
|
+
const jpeg = await this.encodeEvidenceCrop(frame, result, entry);
|
|
7386
|
+
if (jpeg === null) {
|
|
7387
|
+
cost.failed += 1;
|
|
7388
|
+
continue;
|
|
7389
|
+
}
|
|
7390
|
+
cost.cut += 1;
|
|
7391
|
+
cost.bytes += jpeg.byteLength;
|
|
7392
|
+
cost.encodeMs += Date.now() - startedAt;
|
|
7393
|
+
byIndex.set(entry.index, {
|
|
7394
|
+
jpegBase64: jpeg.toString("base64"),
|
|
7395
|
+
rect: entry.rect,
|
|
7396
|
+
tier: "analysis"
|
|
7397
|
+
});
|
|
7398
|
+
}
|
|
7399
|
+
this.reportBirthEvidenceCost(deviceId);
|
|
7400
|
+
if (byIndex.size === 0) return result;
|
|
7401
|
+
return {
|
|
7402
|
+
...result,
|
|
7403
|
+
detections: detections.map((d, i) => {
|
|
7404
|
+
const evidence = byIndex.get(i);
|
|
7405
|
+
return evidence === void 0 ? d : {
|
|
7406
|
+
...d,
|
|
7407
|
+
birthEvidence: evidence
|
|
7408
|
+
};
|
|
7409
|
+
})
|
|
7410
|
+
};
|
|
7411
|
+
}
|
|
7412
|
+
/**
|
|
7413
|
+
* One evidence crop, or `null` when neither raster can serve it.
|
|
7414
|
+
*
|
|
7415
|
+
* The process-local frame LEASE is tried first. On the co-located path
|
|
7416
|
+
* `DecodedFrame.data` is deliberately empty and the pixels live only behind
|
|
7417
|
+
* `frameRef` — reading `data` there produced `skippedNoPixels` on roughly half
|
|
7418
|
+
* the cluster's cameras in the first minutes after this shipped, which is
|
|
7419
|
+
* exactly the silent gap this whole change exists to close. It is also the
|
|
7420
|
+
* same lease the recovery pass resolves its views from, so it costs no new
|
|
7421
|
+
* mechanism and no new copy of the frame.
|
|
7422
|
+
*
|
|
7423
|
+
* The raw buffer is the fallback for a frame delivered inline. `format` is
|
|
7424
|
+
* checked rather than assumed: a planar frame read as RGB would produce a crop
|
|
7425
|
+
* of noise that re-detects as nothing — a false SUPPRESSION, the one error
|
|
7426
|
+
* this path must never make.
|
|
7427
|
+
*/
|
|
7428
|
+
async encodeEvidenceCrop(frame, result, entry) {
|
|
7429
|
+
const fromLease = frame.frameRef ? await this.encodeEvidenceFromLease(frame.frameRef, entry) : null;
|
|
7430
|
+
if (fromLease !== null) return fromLease;
|
|
7431
|
+
const expectedBytes = result.width * result.height * 3;
|
|
7432
|
+
if (frame.format !== "rgb" || frame.data.byteLength < expectedBytes) return null;
|
|
7433
|
+
try {
|
|
7434
|
+
let pipeline = (await getSharp())(frame.data, { raw: {
|
|
7435
|
+
width: result.width,
|
|
7436
|
+
height: result.height,
|
|
7437
|
+
channels: 3
|
|
7438
|
+
} }).extract({
|
|
7439
|
+
left: entry.rect.x,
|
|
7440
|
+
top: entry.rect.y,
|
|
7441
|
+
width: entry.rect.width,
|
|
7442
|
+
height: entry.rect.height
|
|
7443
|
+
});
|
|
7444
|
+
if (entry.target.width !== entry.rect.width) pipeline = pipeline.resize(entry.target.width, entry.target.height, { fit: "fill" });
|
|
7445
|
+
return await pipeline.jpeg({ quality: 85 }).toBuffer();
|
|
7446
|
+
} catch {
|
|
7447
|
+
return null;
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
/** The evidence crop from the process-local lease, JPEG-encoded by the view. */
|
|
7451
|
+
async encodeEvidenceFromLease(ref, entry) {
|
|
7452
|
+
const lease = localFrameRegistry.acquire(ref);
|
|
7453
|
+
if (!lease) return null;
|
|
7454
|
+
let reason = "error";
|
|
7455
|
+
try {
|
|
7456
|
+
const view = await lease.resolve({
|
|
7457
|
+
crop: {
|
|
7458
|
+
left: entry.rect.x,
|
|
7459
|
+
top: entry.rect.y,
|
|
7460
|
+
width: entry.rect.width,
|
|
7461
|
+
height: entry.rect.height
|
|
7462
|
+
},
|
|
7463
|
+
content: {
|
|
7464
|
+
width: entry.target.width,
|
|
7465
|
+
height: entry.target.height
|
|
7466
|
+
},
|
|
7467
|
+
fit: "stretch",
|
|
7468
|
+
format: "jpeg"
|
|
7469
|
+
});
|
|
7470
|
+
if (view.kind !== "view") {
|
|
7471
|
+
reason = "drop";
|
|
7472
|
+
return null;
|
|
7473
|
+
}
|
|
7474
|
+
reason = "success";
|
|
7475
|
+
return Buffer.from(view.data);
|
|
7476
|
+
} catch {
|
|
7477
|
+
return null;
|
|
7478
|
+
} finally {
|
|
7479
|
+
lease.release(reason);
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
/**
|
|
7483
|
+
* State, per camera, what carrying the evidence cost over the window — and
|
|
7484
|
+
* drain it. The operator's acceptance condition for D379 is that the runner's
|
|
7485
|
+
* memory and the frame path do not grow beyond a declared budget, and a
|
|
7486
|
+
* budget nobody measures is an estimate.
|
|
7487
|
+
*/
|
|
7488
|
+
reportBirthEvidenceCost(deviceId) {
|
|
7489
|
+
const cost = this.birthEvidenceCost.get(deviceId);
|
|
7490
|
+
if (!cost) return;
|
|
7491
|
+
const now = Date.now();
|
|
7492
|
+
const lastAt = this.birthEvidenceReportedAt.get(deviceId) ?? 0;
|
|
7493
|
+
if (lastAt !== 0 && now - lastAt < BIRTH_EVIDENCE_REPORT_INTERVAL_MS) return;
|
|
7494
|
+
const windowMs = lastAt === 0 ? BIRTH_EVIDENCE_REPORT_INTERVAL_MS : now - lastAt;
|
|
7495
|
+
this.birthEvidenceReportedAt.set(deviceId, now);
|
|
7496
|
+
this.ctx.logger.info("birth evidence carried", {
|
|
7497
|
+
tags: { deviceId },
|
|
7498
|
+
meta: {
|
|
7499
|
+
cut: cost.cut,
|
|
7500
|
+
failed: cost.failed,
|
|
7501
|
+
skippedNoPixels: cost.skippedNoPixels,
|
|
7502
|
+
kb: Math.round(cost.bytes / 1024),
|
|
7503
|
+
encodeMs: cost.encodeMs,
|
|
7504
|
+
meanKb: cost.cut > 0 ? Math.round(cost.bytes / cost.cut / 1024) : 0,
|
|
7505
|
+
meanEncodeMs: cost.cut > 0 ? Math.round(cost.encodeMs / cost.cut) : 0,
|
|
7506
|
+
windowMs
|
|
7507
|
+
}
|
|
7508
|
+
});
|
|
7509
|
+
cost.cut = 0;
|
|
7510
|
+
cost.failed = 0;
|
|
7511
|
+
cost.skippedNoPixels = 0;
|
|
7512
|
+
cost.bytes = 0;
|
|
7513
|
+
cost.encodeMs = 0;
|
|
7514
|
+
}
|
|
7161
7515
|
async resolveNativeCropPixels(input) {
|
|
7162
7516
|
const b = input.bbox;
|
|
7163
7517
|
const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
|
|
@@ -7681,6 +8035,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7681
8035
|
this.lastMotionAt.delete(deviceId);
|
|
7682
8036
|
this.lastEmittedCameraMetrics.delete(deviceId);
|
|
7683
8037
|
this.retention?.forgetDevice(deviceId);
|
|
8038
|
+
this.birthNovelty.clearDevice(deviceId);
|
|
8039
|
+
this.birthEvidenceCost.delete(deviceId);
|
|
8040
|
+
this.birthEvidenceReportedAt.delete(deviceId);
|
|
7684
8041
|
this.runner?.unregisterCamera(deviceId);
|
|
7685
8042
|
this.ctx?.logger.info("detachCamera", {
|
|
7686
8043
|
tags: { deviceId },
|