@camstack/addon-pipeline 1.2.40 → 1.2.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DcMBfoCg.js → dist-BdDd3ql_.js} +14390 -13469
- package/dist/{dist-C6p8jcji.mjs → dist-COK_1Ot8.mjs} +13159 -12256
- package/dist/{event-loop-stall-monitor-BHqCesa1.mjs → event-loop-stall-monitor-CWOJtTNc.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-oL9xanvX.js → event-loop-stall-monitor-DNFRgNef.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +192 -27
- package/dist/pipeline-runner/index.mjs +192 -27
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +39 -2
- package/dist/session-decode/decode-worker-child.mjs +39 -2
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bt1YkfUW.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C8ovZN0g.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-SU_6F3Y7.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DAjAxXHm.mjs +26 -0
- package/dist/stream-broker/{hostInit-Dyj8Vaql.mjs → hostInit-mEtjQWcr.mjs} +3 -3
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-m1RaK_7F.js → MaskShapeCanvas-DI4BY7W2-C0TIfr0G.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-C00UIgFo.js → MotionZonesSettings-NcxxQN8r-x0SkfHwv.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DWYllToj.js → PrivacyMaskSettings-APgPLF7p-BoaAB4Ta.js} +1 -1
- package/embed-dist/assets/index-BgRdCCVy.js +112 -0
- package/embed-dist/assets/index-CGdwTcwE.css +2 -0
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DJnh97n9.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsNhZnfZ.mjs +0 -26
- package/embed-dist/assets/index-4fJzjj4H.css +0 -2
- package/embed-dist/assets/index-BaUgbxWl.js +0 -112
package/dist/{event-loop-stall-monitor-BHqCesa1.mjs → event-loop-stall-monitor-CWOJtTNc.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as COCO_TO_MACRO, i as COCO_80_LABELS, k as hfModelUrl, r as AUDIO_MACRO_LABELS } from "./dist-COK_1Ot8.mjs";
|
|
2
2
|
import { PerformanceObserver } from "node:perf_hooks";
|
|
3
3
|
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
4
4
|
var HF_REPO = "camstack/camstack-models";
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BdDd3ql_.js");
|
|
6
6
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { M as motionDetectionCapability, O as evaluateZoneRules, W as BaseAddon, Y as hydrateSchema, q as DeviceType } from "../dist-COK_1Ot8.mjs";
|
|
2
2
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-
|
|
6
|
+
const require_dist = require("../dist-BdDd3ql_.js");
|
|
7
|
+
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DNFRgNef.js");
|
|
8
8
|
const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
10
|
const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
|
|
@@ -1687,27 +1687,108 @@ function startStallSampler(opts = {}) {
|
|
|
1687
1687
|
};
|
|
1688
1688
|
}
|
|
1689
1689
|
//#endregion
|
|
1690
|
-
//#region src/pipeline-runner/
|
|
1691
|
-
/** Relative padding applied to the parent bbox before cutting the crop — more context for the child model. */
|
|
1692
|
-
var DETAIL_CROP_PADDING_RATIO = .15;
|
|
1690
|
+
//#region src/pipeline-runner/crop-convention-source.ts
|
|
1693
1691
|
/**
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1692
|
+
* Where the runner gets the ONE detail-crop convention.
|
|
1693
|
+
*
|
|
1694
|
+
* ## Why the runner, and only the runner
|
|
1695
|
+
*
|
|
1696
|
+
* Every detail crop is now cut inside `runDetailSubtree` — the live plane from
|
|
1697
|
+
* a frame handle, the embedding rebuild from a stored key frame it ships in as
|
|
1698
|
+
* `frameJpeg`. One cutter means one reader: nothing else needs the value, so
|
|
1699
|
+
* nothing else can drift from it.
|
|
1700
|
+
*
|
|
1701
|
+
* ## Why a settings read and not the attach config
|
|
1702
|
+
*
|
|
1703
|
+
* `RunnerCameraConfig` is the obvious orchestrator→runner channel, and it is
|
|
1704
|
+
* the wrong one here. Eleven separate call sites construct a
|
|
1705
|
+
* `RunnerCameraConfig` (session dispatch, standing dispatch, zone refresh,
|
|
1706
|
+
* node reconnect, placement correction, …). A cluster-wide constant threaded
|
|
1707
|
+
* through eleven builders is a constant that eventually reaches ten of them,
|
|
1708
|
+
* and the failure is invisible: the cameras attached by the forgotten builder
|
|
1709
|
+
* quietly cut a different crop and write it into the same index. That is the
|
|
1710
|
+
* bug this whole change exists to remove, so it must not be reintroduced as
|
|
1711
|
+
* the delivery mechanism.
|
|
1712
|
+
*
|
|
1713
|
+
* Instead the runner reads the OWNER addon's global settings directly, the way
|
|
1714
|
+
* both decoder addons read their backend selector
|
|
1715
|
+
* (`decoder-backend.ts`) — hub-routed by `AddonCallGateway`, so an agent runner
|
|
1716
|
+
* transparently reads the hub-central store with no placement branching.
|
|
1717
|
+
*
|
|
1718
|
+
* ## Deliberately NOT node-scoped
|
|
1719
|
+
*
|
|
1720
|
+
* The read passes no `nodeId`. With one, `addon-settings` would project that
|
|
1721
|
+
* node's scoped value and two nodes could cut different crops into the same
|
|
1722
|
+
* `object-clip` index — the original defect, relocated to the transport.
|
|
1696
1723
|
*/
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1724
|
+
/**
|
|
1725
|
+
* The addon whose GLOBAL settings own the convention. `pipeline-orchestrator`
|
|
1726
|
+
* is hub-resident and neutral — it already owns the per-node decoder-backend
|
|
1727
|
+
* selector for the same reason (neither consumer should own a value the other
|
|
1728
|
+
* depends on).
|
|
1729
|
+
*/
|
|
1730
|
+
var CROP_CONVENTION_OWNER_ADDON_ID = "pipeline-orchestrator";
|
|
1731
|
+
/**
|
|
1732
|
+
* TTL-cached read of the cluster crop convention.
|
|
1733
|
+
*
|
|
1734
|
+
* {@link current} is synchronous because the crop derivation is, and because a
|
|
1735
|
+
* value that is never missing is easier to reason about than one that is
|
|
1736
|
+
* sometimes a promise. {@link refresh} is what actually talks to the hub;
|
|
1737
|
+
* the runner awaits it at the top of a detail dispatch.
|
|
1738
|
+
*/
|
|
1739
|
+
var CropConventionSource = class {
|
|
1740
|
+
convention = require_dist.DEFAULT_DETAIL_CROP_CONVENTION;
|
|
1741
|
+
lastReadAtMs = Number.NEGATIVE_INFINITY;
|
|
1742
|
+
inFlight = null;
|
|
1743
|
+
logger;
|
|
1744
|
+
now;
|
|
1745
|
+
ttlMs;
|
|
1746
|
+
constructor(options) {
|
|
1747
|
+
this.logger = options.logger;
|
|
1748
|
+
this.now = options.now ?? (() => Date.now());
|
|
1749
|
+
this.ttlMs = options.ttlMs ?? 6e4;
|
|
1750
|
+
}
|
|
1751
|
+
/** The convention in force right now — the default until a read lands. */
|
|
1752
|
+
current() {
|
|
1753
|
+
return this.convention;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Re-read if the cached value has expired. Concurrent callers share one
|
|
1757
|
+
* in-flight read; a failure keeps the LAST KNOWN value rather than reverting
|
|
1758
|
+
* to the default, because a mid-pass revert would silently change what every
|
|
1759
|
+
* remaining vector of that pass means.
|
|
1760
|
+
*/
|
|
1761
|
+
async refresh(api) {
|
|
1762
|
+
if (api === void 0) return this.convention;
|
|
1763
|
+
if (this.now() - this.lastReadAtMs < this.ttlMs) return this.convention;
|
|
1764
|
+
this.inFlight ??= this.read(api).finally(() => {
|
|
1765
|
+
this.inFlight = null;
|
|
1766
|
+
});
|
|
1767
|
+
await this.inFlight;
|
|
1768
|
+
return this.convention;
|
|
1769
|
+
}
|
|
1770
|
+
async read(api) {
|
|
1771
|
+
try {
|
|
1772
|
+
const next = require_dist.pickDetailCropConvention(await api.addonSettings.getGlobalSettings.query({ addonId: CROP_CONVENTION_OWNER_ADDON_ID }));
|
|
1773
|
+
this.lastReadAtMs = this.now();
|
|
1774
|
+
if (next.paddingRatio !== this.convention.paddingRatio || next.square !== this.convention.square) {
|
|
1775
|
+
this.logger.info("detail crop convention changed — stored vectors need a rebuild", { meta: {
|
|
1776
|
+
from: { ...this.convention },
|
|
1777
|
+
to: { ...next }
|
|
1778
|
+
} });
|
|
1779
|
+
this.convention = next;
|
|
1780
|
+
}
|
|
1781
|
+
} catch (err) {
|
|
1782
|
+
this.logger.warn("detail crop convention read failed — keeping the last known value", { meta: {
|
|
1783
|
+
owner: CROP_CONVENTION_OWNER_ADDON_ID,
|
|
1784
|
+
inUse: { ...this.convention },
|
|
1785
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1786
|
+
} });
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
//#endregion
|
|
1791
|
+
//#region src/pipeline-runner/detail-subtree.ts
|
|
1711
1792
|
/** Normalize a frame-space pixel rect to `[0,1]` by the given frame dims. */
|
|
1712
1793
|
function normalizeRect(rect, frameWidth, frameHeight) {
|
|
1713
1794
|
return {
|
|
@@ -1806,15 +1887,52 @@ function toDetailResult(detection, stepId, crop) {
|
|
|
1806
1887
|
...detection.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: detection.nativeFaceShortSidePx } : {}
|
|
1807
1888
|
};
|
|
1808
1889
|
}
|
|
1809
|
-
|
|
1890
|
+
/**
|
|
1891
|
+
* Cut the crop out of a caller-supplied FULL FRAME, applying the one crop
|
|
1892
|
+
* convention. This is the branch that made the rebuild comparable with live
|
|
1893
|
+
* traffic: before it existed the only way to feed stored pixels in was a
|
|
1894
|
+
* pre-cut tile, so the caller decided the rectangle and every caller decided
|
|
1895
|
+
* differently.
|
|
1896
|
+
*/
|
|
1897
|
+
async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
1898
|
+
const image = (await require_lazy_sharp.getSharp())(Buffer.from(frameJpegBase64, "base64"));
|
|
1899
|
+
const meta = await image.metadata();
|
|
1900
|
+
const frameWidth = meta.width ?? 0;
|
|
1901
|
+
const frameHeight = meta.height ?? 0;
|
|
1902
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
1903
|
+
const rect = require_dist.deriveDetailCropRect(bbox, frameWidth, frameHeight, convention);
|
|
1904
|
+
const left = Math.max(0, Math.min(Math.round(rect.x), frameWidth - 1));
|
|
1905
|
+
const top = Math.max(0, Math.min(Math.round(rect.y), frameHeight - 1));
|
|
1906
|
+
const width = Math.max(1, Math.min(Math.round(rect.w), frameWidth - left));
|
|
1907
|
+
const height = Math.max(1, Math.min(Math.round(rect.h), frameHeight - top));
|
|
1908
|
+
return {
|
|
1909
|
+
jpeg: await image.extract({
|
|
1910
|
+
left,
|
|
1911
|
+
top,
|
|
1912
|
+
width,
|
|
1913
|
+
height
|
|
1914
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
1915
|
+
width,
|
|
1916
|
+
height,
|
|
1917
|
+
frameSpace: {
|
|
1918
|
+
x: left,
|
|
1919
|
+
y: top,
|
|
1920
|
+
w: width,
|
|
1921
|
+
h: height
|
|
1922
|
+
},
|
|
1923
|
+
source: "derived"
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
async function resolveCrop(deps, input, convention) {
|
|
1810
1927
|
if (input.frameHandle) {
|
|
1811
|
-
const normalized = normalizeRect(
|
|
1928
|
+
const normalized = normalizeRect(require_dist.deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
|
|
1812
1929
|
const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
|
|
1813
1930
|
if (native) return {
|
|
1814
1931
|
...native,
|
|
1815
1932
|
source: "native"
|
|
1816
1933
|
};
|
|
1817
1934
|
}
|
|
1935
|
+
if (input.frameJpeg) return cutFromFullFrame(input.frameJpeg, input.parent.bbox, convention);
|
|
1818
1936
|
if (input.cropJpeg) {
|
|
1819
1937
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
1820
1938
|
const meta = await (await require_lazy_sharp.getSharp())(jpeg).metadata();
|
|
@@ -1849,11 +1967,42 @@ async function resolveCrop(deps, input) {
|
|
|
1849
1967
|
*/
|
|
1850
1968
|
async function runDetailSubtree(deps, input) {
|
|
1851
1969
|
const matched = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
|
|
1852
|
-
if (!matched
|
|
1970
|
+
if (!matched) {
|
|
1971
|
+
deps.logger.warn("runDetailSubtree: camera is not attached — no detail steps to run", {
|
|
1972
|
+
tags: { deviceId: input.deviceId },
|
|
1973
|
+
meta: {
|
|
1974
|
+
parentClass: input.parent.className,
|
|
1975
|
+
...input.steps ? { requestedSteps: [...input.steps] } : {}
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
return null;
|
|
1979
|
+
}
|
|
1980
|
+
if (matched.length === 0) {
|
|
1981
|
+
deps.logger.warn("runDetailSubtree: no matching detail step is enabled for this parent class", {
|
|
1982
|
+
tags: { deviceId: input.deviceId },
|
|
1983
|
+
meta: {
|
|
1984
|
+
parentClass: input.parent.className,
|
|
1985
|
+
...input.steps ? { requestedSteps: [...input.steps] } : {},
|
|
1986
|
+
hint: "the step is either absent from this camera step tree or disabled on it"
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1853
1991
|
const requested = input.steps;
|
|
1854
1992
|
const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
|
|
1855
|
-
const crop = await resolveCrop(deps, input);
|
|
1856
|
-
if (!crop)
|
|
1993
|
+
const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
1994
|
+
if (!crop) {
|
|
1995
|
+
deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
|
|
1996
|
+
tags: { deviceId: input.deviceId },
|
|
1997
|
+
meta: {
|
|
1998
|
+
parentClass: input.parent.className,
|
|
1999
|
+
hadFrameHandle: input.frameHandle !== void 0,
|
|
2000
|
+
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
2001
|
+
hadCropJpeg: input.cropJpeg !== void 0
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
1857
2006
|
const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
|
|
1858
2007
|
handle: input.frameHandle,
|
|
1859
2008
|
cropFrameSpace: crop.frameSpace
|
|
@@ -4605,14 +4754,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
4605
4754
|
* the frame's owning process), and the node-local `pipelineExecutor`.
|
|
4606
4755
|
*/
|
|
4607
4756
|
async runDetailSubtree(input) {
|
|
4757
|
+
const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
|
|
4608
4758
|
return runDetailSubtree({
|
|
4609
4759
|
resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
|
|
4610
4760
|
getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
|
|
4611
4761
|
runPipeline: (steps, imageJpeg, deviceId, deviceKey, nativeCropRef) => this.runDetailPipeline(steps, imageJpeg, deviceId, deviceKey, nativeCropRef),
|
|
4612
|
-
resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step)
|
|
4762
|
+
resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step),
|
|
4763
|
+
resolveCropConvention: () => convention,
|
|
4764
|
+
logger: this.detailSubtreeLogger()
|
|
4613
4765
|
}, input);
|
|
4614
4766
|
}
|
|
4615
4767
|
/**
|
|
4768
|
+
* The cluster-wide detail-crop convention — read from the orchestrator's
|
|
4769
|
+
* global settings, NOT from the attach config. See `crop-convention-source.ts`
|
|
4770
|
+
* for why eleven attach builders make the attach config the wrong carrier.
|
|
4771
|
+
*/
|
|
4772
|
+
cropConvention = new CropConventionSource({ logger: {
|
|
4773
|
+
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
4774
|
+
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
4775
|
+
} });
|
|
4776
|
+
/** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
|
|
4777
|
+
detailSubtreeLogger() {
|
|
4778
|
+
return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4616
4781
|
* Step-tree device jump (phase 1) — pick which same-node inference device an
|
|
4617
4782
|
* enrichment `step` runs on. AUTO-jumps only when the camera's elected device
|
|
4618
4783
|
* cannot run the step's model (its format has no build); an operator
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-
|
|
1
|
+
import { $ as nodePin, C as defineCustomActions, F as pipelineRunnerCapability, J as createEvent, N as pickDetailCropConvention, U as errMsg, W as BaseAddon, at as array, c as DEVICE_BACKEND_TO_FORMAT, ct as lazy, dt as object, ht as EventCategory, it as _enum, lt as literal, ot as boolean, pt as string, s as DEFAULT_DETAIL_CROP_CONVENTION, ut as number, w as deriveDetailCropRect, x as customAction } from "../dist-COK_1Ot8.mjs";
|
|
2
|
+
import { c as getStepDefinition, t as attributeStall } from "../event-loop-stall-monitor-CWOJtTNc.mjs";
|
|
3
3
|
import { t as isWorkerReply } from "../worker-protocol-D7RzZIla.mjs";
|
|
4
4
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
5
5
|
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
@@ -1679,27 +1679,108 @@ function startStallSampler(opts = {}) {
|
|
|
1679
1679
|
};
|
|
1680
1680
|
}
|
|
1681
1681
|
//#endregion
|
|
1682
|
-
//#region src/pipeline-runner/
|
|
1683
|
-
/** Relative padding applied to the parent bbox before cutting the crop — more context for the child model. */
|
|
1684
|
-
var DETAIL_CROP_PADDING_RATIO = .15;
|
|
1682
|
+
//#region src/pipeline-runner/crop-convention-source.ts
|
|
1685
1683
|
/**
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1684
|
+
* Where the runner gets the ONE detail-crop convention.
|
|
1685
|
+
*
|
|
1686
|
+
* ## Why the runner, and only the runner
|
|
1687
|
+
*
|
|
1688
|
+
* Every detail crop is now cut inside `runDetailSubtree` — the live plane from
|
|
1689
|
+
* a frame handle, the embedding rebuild from a stored key frame it ships in as
|
|
1690
|
+
* `frameJpeg`. One cutter means one reader: nothing else needs the value, so
|
|
1691
|
+
* nothing else can drift from it.
|
|
1692
|
+
*
|
|
1693
|
+
* ## Why a settings read and not the attach config
|
|
1694
|
+
*
|
|
1695
|
+
* `RunnerCameraConfig` is the obvious orchestrator→runner channel, and it is
|
|
1696
|
+
* the wrong one here. Eleven separate call sites construct a
|
|
1697
|
+
* `RunnerCameraConfig` (session dispatch, standing dispatch, zone refresh,
|
|
1698
|
+
* node reconnect, placement correction, …). A cluster-wide constant threaded
|
|
1699
|
+
* through eleven builders is a constant that eventually reaches ten of them,
|
|
1700
|
+
* and the failure is invisible: the cameras attached by the forgotten builder
|
|
1701
|
+
* quietly cut a different crop and write it into the same index. That is the
|
|
1702
|
+
* bug this whole change exists to remove, so it must not be reintroduced as
|
|
1703
|
+
* the delivery mechanism.
|
|
1704
|
+
*
|
|
1705
|
+
* Instead the runner reads the OWNER addon's global settings directly, the way
|
|
1706
|
+
* both decoder addons read their backend selector
|
|
1707
|
+
* (`decoder-backend.ts`) — hub-routed by `AddonCallGateway`, so an agent runner
|
|
1708
|
+
* transparently reads the hub-central store with no placement branching.
|
|
1709
|
+
*
|
|
1710
|
+
* ## Deliberately NOT node-scoped
|
|
1711
|
+
*
|
|
1712
|
+
* The read passes no `nodeId`. With one, `addon-settings` would project that
|
|
1713
|
+
* node's scoped value and two nodes could cut different crops into the same
|
|
1714
|
+
* `object-clip` index — the original defect, relocated to the transport.
|
|
1688
1715
|
*/
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1716
|
+
/**
|
|
1717
|
+
* The addon whose GLOBAL settings own the convention. `pipeline-orchestrator`
|
|
1718
|
+
* is hub-resident and neutral — it already owns the per-node decoder-backend
|
|
1719
|
+
* selector for the same reason (neither consumer should own a value the other
|
|
1720
|
+
* depends on).
|
|
1721
|
+
*/
|
|
1722
|
+
var CROP_CONVENTION_OWNER_ADDON_ID = "pipeline-orchestrator";
|
|
1723
|
+
/**
|
|
1724
|
+
* TTL-cached read of the cluster crop convention.
|
|
1725
|
+
*
|
|
1726
|
+
* {@link current} is synchronous because the crop derivation is, and because a
|
|
1727
|
+
* value that is never missing is easier to reason about than one that is
|
|
1728
|
+
* sometimes a promise. {@link refresh} is what actually talks to the hub;
|
|
1729
|
+
* the runner awaits it at the top of a detail dispatch.
|
|
1730
|
+
*/
|
|
1731
|
+
var CropConventionSource = class {
|
|
1732
|
+
convention = DEFAULT_DETAIL_CROP_CONVENTION;
|
|
1733
|
+
lastReadAtMs = Number.NEGATIVE_INFINITY;
|
|
1734
|
+
inFlight = null;
|
|
1735
|
+
logger;
|
|
1736
|
+
now;
|
|
1737
|
+
ttlMs;
|
|
1738
|
+
constructor(options) {
|
|
1739
|
+
this.logger = options.logger;
|
|
1740
|
+
this.now = options.now ?? (() => Date.now());
|
|
1741
|
+
this.ttlMs = options.ttlMs ?? 6e4;
|
|
1742
|
+
}
|
|
1743
|
+
/** The convention in force right now — the default until a read lands. */
|
|
1744
|
+
current() {
|
|
1745
|
+
return this.convention;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Re-read if the cached value has expired. Concurrent callers share one
|
|
1749
|
+
* in-flight read; a failure keeps the LAST KNOWN value rather than reverting
|
|
1750
|
+
* to the default, because a mid-pass revert would silently change what every
|
|
1751
|
+
* remaining vector of that pass means.
|
|
1752
|
+
*/
|
|
1753
|
+
async refresh(api) {
|
|
1754
|
+
if (api === void 0) return this.convention;
|
|
1755
|
+
if (this.now() - this.lastReadAtMs < this.ttlMs) return this.convention;
|
|
1756
|
+
this.inFlight ??= this.read(api).finally(() => {
|
|
1757
|
+
this.inFlight = null;
|
|
1758
|
+
});
|
|
1759
|
+
await this.inFlight;
|
|
1760
|
+
return this.convention;
|
|
1761
|
+
}
|
|
1762
|
+
async read(api) {
|
|
1763
|
+
try {
|
|
1764
|
+
const next = pickDetailCropConvention(await api.addonSettings.getGlobalSettings.query({ addonId: CROP_CONVENTION_OWNER_ADDON_ID }));
|
|
1765
|
+
this.lastReadAtMs = this.now();
|
|
1766
|
+
if (next.paddingRatio !== this.convention.paddingRatio || next.square !== this.convention.square) {
|
|
1767
|
+
this.logger.info("detail crop convention changed — stored vectors need a rebuild", { meta: {
|
|
1768
|
+
from: { ...this.convention },
|
|
1769
|
+
to: { ...next }
|
|
1770
|
+
} });
|
|
1771
|
+
this.convention = next;
|
|
1772
|
+
}
|
|
1773
|
+
} catch (err) {
|
|
1774
|
+
this.logger.warn("detail crop convention read failed — keeping the last known value", { meta: {
|
|
1775
|
+
owner: CROP_CONVENTION_OWNER_ADDON_ID,
|
|
1776
|
+
inUse: { ...this.convention },
|
|
1777
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1778
|
+
} });
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region src/pipeline-runner/detail-subtree.ts
|
|
1703
1784
|
/** Normalize a frame-space pixel rect to `[0,1]` by the given frame dims. */
|
|
1704
1785
|
function normalizeRect(rect, frameWidth, frameHeight) {
|
|
1705
1786
|
return {
|
|
@@ -1798,15 +1879,52 @@ function toDetailResult(detection, stepId, crop) {
|
|
|
1798
1879
|
...detection.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: detection.nativeFaceShortSidePx } : {}
|
|
1799
1880
|
};
|
|
1800
1881
|
}
|
|
1801
|
-
|
|
1882
|
+
/**
|
|
1883
|
+
* Cut the crop out of a caller-supplied FULL FRAME, applying the one crop
|
|
1884
|
+
* convention. This is the branch that made the rebuild comparable with live
|
|
1885
|
+
* traffic: before it existed the only way to feed stored pixels in was a
|
|
1886
|
+
* pre-cut tile, so the caller decided the rectangle and every caller decided
|
|
1887
|
+
* differently.
|
|
1888
|
+
*/
|
|
1889
|
+
async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
|
|
1890
|
+
const image = (await getSharp())(Buffer.from(frameJpegBase64, "base64"));
|
|
1891
|
+
const meta = await image.metadata();
|
|
1892
|
+
const frameWidth = meta.width ?? 0;
|
|
1893
|
+
const frameHeight = meta.height ?? 0;
|
|
1894
|
+
if (frameWidth <= 0 || frameHeight <= 0) return null;
|
|
1895
|
+
const rect = deriveDetailCropRect(bbox, frameWidth, frameHeight, convention);
|
|
1896
|
+
const left = Math.max(0, Math.min(Math.round(rect.x), frameWidth - 1));
|
|
1897
|
+
const top = Math.max(0, Math.min(Math.round(rect.y), frameHeight - 1));
|
|
1898
|
+
const width = Math.max(1, Math.min(Math.round(rect.w), frameWidth - left));
|
|
1899
|
+
const height = Math.max(1, Math.min(Math.round(rect.h), frameHeight - top));
|
|
1900
|
+
return {
|
|
1901
|
+
jpeg: await image.extract({
|
|
1902
|
+
left,
|
|
1903
|
+
top,
|
|
1904
|
+
width,
|
|
1905
|
+
height
|
|
1906
|
+
}).jpeg({ quality: 90 }).toBuffer(),
|
|
1907
|
+
width,
|
|
1908
|
+
height,
|
|
1909
|
+
frameSpace: {
|
|
1910
|
+
x: left,
|
|
1911
|
+
y: top,
|
|
1912
|
+
w: width,
|
|
1913
|
+
h: height
|
|
1914
|
+
},
|
|
1915
|
+
source: "derived"
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
async function resolveCrop(deps, input, convention) {
|
|
1802
1919
|
if (input.frameHandle) {
|
|
1803
|
-
const normalized = normalizeRect(
|
|
1920
|
+
const normalized = normalizeRect(deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
|
|
1804
1921
|
const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
|
|
1805
1922
|
if (native) return {
|
|
1806
1923
|
...native,
|
|
1807
1924
|
source: "native"
|
|
1808
1925
|
};
|
|
1809
1926
|
}
|
|
1927
|
+
if (input.frameJpeg) return cutFromFullFrame(input.frameJpeg, input.parent.bbox, convention);
|
|
1810
1928
|
if (input.cropJpeg) {
|
|
1811
1929
|
const jpeg = Buffer.from(input.cropJpeg, "base64");
|
|
1812
1930
|
const meta = await (await getSharp())(jpeg).metadata();
|
|
@@ -1841,11 +1959,42 @@ async function resolveCrop(deps, input) {
|
|
|
1841
1959
|
*/
|
|
1842
1960
|
async function runDetailSubtree(deps, input) {
|
|
1843
1961
|
const matched = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
|
|
1844
|
-
if (!matched
|
|
1962
|
+
if (!matched) {
|
|
1963
|
+
deps.logger.warn("runDetailSubtree: camera is not attached — no detail steps to run", {
|
|
1964
|
+
tags: { deviceId: input.deviceId },
|
|
1965
|
+
meta: {
|
|
1966
|
+
parentClass: input.parent.className,
|
|
1967
|
+
...input.steps ? { requestedSteps: [...input.steps] } : {}
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
return null;
|
|
1971
|
+
}
|
|
1972
|
+
if (matched.length === 0) {
|
|
1973
|
+
deps.logger.warn("runDetailSubtree: no matching detail step is enabled for this parent class", {
|
|
1974
|
+
tags: { deviceId: input.deviceId },
|
|
1975
|
+
meta: {
|
|
1976
|
+
parentClass: input.parent.className,
|
|
1977
|
+
...input.steps ? { requestedSteps: [...input.steps] } : {},
|
|
1978
|
+
hint: "the step is either absent from this camera step tree or disabled on it"
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1845
1983
|
const requested = input.steps;
|
|
1846
1984
|
const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
|
|
1847
|
-
const crop = await resolveCrop(deps, input);
|
|
1848
|
-
if (!crop)
|
|
1985
|
+
const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
|
|
1986
|
+
if (!crop) {
|
|
1987
|
+
deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
|
|
1988
|
+
tags: { deviceId: input.deviceId },
|
|
1989
|
+
meta: {
|
|
1990
|
+
parentClass: input.parent.className,
|
|
1991
|
+
hadFrameHandle: input.frameHandle !== void 0,
|
|
1992
|
+
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
1993
|
+
hadCropJpeg: input.cropJpeg !== void 0
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1849
1998
|
const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
|
|
1850
1999
|
handle: input.frameHandle,
|
|
1851
2000
|
cropFrameSpace: crop.frameSpace
|
|
@@ -4597,14 +4746,30 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
4597
4746
|
* the frame's owning process), and the node-local `pipelineExecutor`.
|
|
4598
4747
|
*/
|
|
4599
4748
|
async runDetailSubtree(input) {
|
|
4749
|
+
const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
|
|
4600
4750
|
return runDetailSubtree({
|
|
4601
4751
|
resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
|
|
4602
4752
|
getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
|
|
4603
4753
|
runPipeline: (steps, imageJpeg, deviceId, deviceKey, nativeCropRef) => this.runDetailPipeline(steps, imageJpeg, deviceId, deviceKey, nativeCropRef),
|
|
4604
|
-
resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step)
|
|
4754
|
+
resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step),
|
|
4755
|
+
resolveCropConvention: () => convention,
|
|
4756
|
+
logger: this.detailSubtreeLogger()
|
|
4605
4757
|
}, input);
|
|
4606
4758
|
}
|
|
4607
4759
|
/**
|
|
4760
|
+
* The cluster-wide detail-crop convention — read from the orchestrator's
|
|
4761
|
+
* global settings, NOT from the attach config. See `crop-convention-source.ts`
|
|
4762
|
+
* for why eleven attach builders make the attach config the wrong carrier.
|
|
4763
|
+
*/
|
|
4764
|
+
cropConvention = new CropConventionSource({ logger: {
|
|
4765
|
+
info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
|
|
4766
|
+
warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
|
|
4767
|
+
} });
|
|
4768
|
+
/** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
|
|
4769
|
+
detailSubtreeLogger() {
|
|
4770
|
+
return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
|
|
4771
|
+
}
|
|
4772
|
+
/**
|
|
4608
4773
|
* Step-tree device jump (phase 1) — pick which same-node inference device an
|
|
4609
4774
|
* enrichment `step` runs on. AUTO-jumps only when the camera's elected device
|
|
4610
4775
|
* cannot run the step's model (its format has no build); an operator
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-BdDd3ql_.js");
|
|
3
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
4
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
|
|
5
5
|
let node_child_process = require("node:child_process");
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as nodePin, I as recordingCapability, L as recordingExportCapability, T as deriveRecordingMode, U as errMsg, W as BaseAddon, Y as hydrateSchema, at as array, d as ExportRecordSchema, ft as record, ht as EventCategory, l as EVENT_PAD_MS, m as RecordingConfigSchema, nt as selectAssignedProfileSlots, p as OpsLogEntrySchema, pt as string, q as DeviceType, z as storageEvictableCapability } from "../dist-COK_1Ot8.mjs";
|
|
2
2
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
3
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -682,6 +682,15 @@ function toLeasedRgbNavFrame(frame) {
|
|
|
682
682
|
};
|
|
683
683
|
}
|
|
684
684
|
/** Re-dial backoff on a transient dial error/EOF (mirrors the decoder's PULL_REDIAL_MS). */
|
|
685
|
+
/**
|
|
686
|
+
* RSS at which a decode worker is reported anomalous.
|
|
687
|
+
*
|
|
688
|
+
* Chosen from the incident rather than from theory: on 2026-08-05 the healthy
|
|
689
|
+
* fleet sat at 1.1-1.3 GB per worker while the one that broke the host reached
|
|
690
|
+
* 4.9 GB. 2 GB is clear of the normal band and well below the point where it
|
|
691
|
+
* costs anyone anything.
|
|
692
|
+
*/
|
|
693
|
+
var WORKER_RSS_WARN_MB = 2048;
|
|
685
694
|
var REDIAL_MS = 3e3;
|
|
686
695
|
/** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
|
|
687
696
|
var METRICS_INTERVAL_MS = 1e4;
|
|
@@ -1079,6 +1088,12 @@ var DecodeWorkerChild = class {
|
|
|
1079
1088
|
/** Frames actually handed to the parent as a `frame` reply. */
|
|
1080
1089
|
framesDelivered = 0;
|
|
1081
1090
|
lastMetricsAt = 0;
|
|
1091
|
+
/** Process start, for the `ageS` field. A worker that outlives its siblings
|
|
1092
|
+
* by two orders of magnitude is the signal this exists to surface. */
|
|
1093
|
+
startedAt = Date.now();
|
|
1094
|
+
/** One-shot, so a legitimately long session does not repeat the warning
|
|
1095
|
+
* every ten seconds. */
|
|
1096
|
+
warnedLarge = false;
|
|
1082
1097
|
lastDeliveredSnapshot = 0;
|
|
1083
1098
|
metricsTimer = null;
|
|
1084
1099
|
demuxer = null;
|
|
@@ -1170,16 +1185,38 @@ var DecodeWorkerChild = class {
|
|
|
1170
1185
|
this.send({ kind: "ended" });
|
|
1171
1186
|
}
|
|
1172
1187
|
}
|
|
1173
|
-
/**
|
|
1188
|
+
/**
|
|
1189
|
+
* Emit the compact throughput line (window `deliveredFps`); cheap,
|
|
1190
|
+
* integer-only.
|
|
1191
|
+
*
|
|
1192
|
+
* **`rssMb` and `ageS` are here because their absence cost a Home Assistant
|
|
1193
|
+
* outage.** On 2026-08-05 one worker stayed alive 1h58m and grew to 4.9 GiB
|
|
1194
|
+
* at ~46 MB/min while its eleven siblings recycled every ~30 s; the host ran
|
|
1195
|
+
* out of memory and the kernel killed the largest process on the box, which
|
|
1196
|
+
* was somebody else's VM. Nothing in any log said a decode worker was large,
|
|
1197
|
+
* or old — the leak was only found by sampling `ps` by hand.
|
|
1198
|
+
*
|
|
1199
|
+
* This line already runs every 10 s per session and is already grouped by
|
|
1200
|
+
* device in Loki, so carrying the worker's own RSS and age turns "a runner
|
|
1201
|
+
* is big" into "THIS camera's worker has been climbing for an hour", which
|
|
1202
|
+
* is the question that was unanswerable. Retained-store sizes ride along
|
|
1203
|
+
* because they are the first thing to suspect and they are free to read.
|
|
1204
|
+
*/
|
|
1174
1205
|
logMetrics(final) {
|
|
1175
1206
|
const now = Date.now();
|
|
1176
1207
|
const windowMs = Math.max(1, now - this.lastMetricsAt);
|
|
1177
1208
|
const deliveredDelta = this.framesDelivered - this.lastDeliveredSnapshot;
|
|
1178
1209
|
const deliveredFps = Math.round(deliveredDelta / windowMs * 1e3 * 10) / 10;
|
|
1179
1210
|
const skipped = this.framesSkipped + this.frames.droppedCount;
|
|
1180
|
-
|
|
1211
|
+
const rssMb = Math.round(process.memoryUsage().rss / 1048576);
|
|
1212
|
+
const ageS = Math.round((now - this.startedAt) / 1e3);
|
|
1213
|
+
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
1181
1214
|
this.lastMetricsAt = now;
|
|
1182
1215
|
this.lastDeliveredSnapshot = this.framesDelivered;
|
|
1216
|
+
if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
|
|
1217
|
+
this.warnedLarge = true;
|
|
1218
|
+
this.emitStderr(`decode-worker-child: WORKER IS LARGE — rssMb=${rssMb} ageS=${ageS} framesDecoded=${this.framesDecoded}. Siblings normally recycle in ~30s; a worker this size at this age is how the host ran out of memory on 2026-08-05.\n`);
|
|
1219
|
+
}
|
|
1183
1220
|
}
|
|
1184
1221
|
handleStart(source, opts) {
|
|
1185
1222
|
if (this.started) {
|