@camstack/addon-pipeline 1.2.41 → 1.2.43

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.
Files changed (33) hide show
  1. package/dist/audio-analyzer/index.js +4 -4
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/detection-pipeline/index.js +9 -9
  4. package/dist/detection-pipeline/index.mjs +3 -3
  5. package/dist/{dist-DvUTPZ8J.mjs → dist-B-VVBzrL.mjs} +14015 -12502
  6. package/dist/{dist-BS7wLqoV.js → dist-UHbKZiSa.js} +15254 -13705
  7. package/dist/{event-loop-stall-monitor-C5P2bcwP.js → event-loop-stall-monitor-C7_L6vHO.js} +1 -1
  8. package/dist/{event-loop-stall-monitor-a2u9e1qx.mjs → event-loop-stall-monitor-DfKdC7G4.mjs} +1 -1
  9. package/dist/{model-download-service-Cp9f4dk6-fsdDExML.js → model-download-service-D8B-4ktF-7-YxE9Wx.js} +2 -2
  10. package/dist/{model-download-service-Cp9f4dk6-CwUb5v3Y.mjs → model-download-service-D8B-4ktF-BB7oZL3y.mjs} +2 -2
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +348 -27
  14. package/dist/pipeline-runner/index.mjs +348 -27
  15. package/dist/recorder/index.js +6 -6
  16. package/dist/recorder/index.mjs +3 -3
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CS82x0lt.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D7iQbuKY.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCC9fqHH.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-54IklEep.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-lj4kKHrb.mjs → hostInit-CKYS9yZO.mjs} +2 -2
  22. package/dist/stream-broker/index.js +16202 -15821
  23. package/dist/stream-broker/index.mjs +16201 -15820
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BRQ5u_mo.js → MaskShapeCanvas-DI4BY7W2-DxZ75vB8.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-KbOg5nD9.js → MotionZonesSettings-NcxxQN8r-BBY2Ztz5.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CKSPn3_D.js → PrivacyMaskSettings-APgPLF7p-BC-3QmHa.js} +1 -1
  28. package/embed-dist/assets/index-on1wyOnd.js +112 -0
  29. package/embed-dist/index.html +1 -1
  30. package/package.json +1 -1
  31. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CDzxDzcv.mjs +0 -26
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CciPNnL7.mjs +0 -26
  33. package/embed-dist/assets/index-CbrEyYGW.js +0 -112
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BS7wLqoV.js");
1
+ const require_dist = require("./dist-UHbKZiSa.js");
2
2
  let node_perf_hooks = require("node:perf_hooks");
3
3
  //#region src/detection-pipeline/registry/model-catalogs.ts
4
4
  var HF_REPO = "camstack/camstack-models";
@@ -1,4 +1,4 @@
1
- import { D as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-DvUTPZ8J.mjs";
1
+ import { a as COCO_80_LABELS, j as hfModelUrl, o as COCO_TO_MACRO, r as AUDIO_MACRO_LABELS } from "./dist-B-VVBzrL.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";
@@ -1,10 +1,10 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
+ require("node:crypto");
2
3
  let node_fs = require("node:fs");
3
4
  node_fs = require_chunk.__toESM(node_fs, 1);
4
5
  let node_path = require("node:path");
5
6
  node_path = require_chunk.__toESM(node_path, 1);
6
- require("node:crypto");
7
- //#region ../system/dist/model-download-service-Cp9f4dk6.mjs
7
+ //#region ../system/dist/model-download-service-D8B-4ktF.mjs
8
8
  /**
9
9
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
10
10
  * path stays within (traversal guard). The handler serves the first candidate
@@ -1,9 +1,9 @@
1
+ import "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import { createReadStream, promises } from "node:fs";
3
4
  import * as path$1 from "node:path";
4
5
  import path from "node:path";
5
- import "node:crypto";
6
- //#region ../system/dist/model-download-service-Cp9f4dk6.mjs
6
+ //#region ../system/dist/model-download-service-D8B-4ktF.mjs
7
7
  /**
8
8
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
9
9
  * path stays within (traversal guard). The handler serves the first candidate
@@ -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-BS7wLqoV.js");
5
+ const require_dist = require("../dist-UHbKZiSa.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 { A as motionDetectionCapability, E as evaluateZoneRules, K as hydrateSchema, V as BaseAddon, W as DeviceType } from "../dist-DvUTPZ8J.mjs";
1
+ import { A as evaluateZoneRules, F as motionDetectionCapability, Q as hydrateSchema, X as DeviceType, q as BaseAddon } from "../dist-B-VVBzrL.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-BS7wLqoV.js");
7
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-C5P2bcwP.js");
6
+ const require_dist = require("../dist-UHbKZiSa.js");
7
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-C7_L6vHO.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/detail-subtree.ts
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
1691
+ /**
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.
1723
+ */
1693
1724
  /**
1694
- * Pad `bbox` by {@link DETAIL_CROP_PADDING_RATIO} of its own size, then clamp
1695
- * to `[0, frameWidth] x [0, frameHeight]`. Pure always returns a new rect.
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).
1696
1729
  */
1697
- function padAndClampFrameBbox(bbox, frameWidth, frameHeight) {
1698
- const padX = DETAIL_CROP_PADDING_RATIO * bbox.w;
1699
- const padY = DETAIL_CROP_PADDING_RATIO * bbox.h;
1700
- const x1 = Math.max(0, bbox.x - padX);
1701
- const y1 = Math.max(0, bbox.y - padY);
1702
- const x2 = Math.min(frameWidth, bbox.x + bbox.w + padX);
1703
- const y2 = Math.min(frameHeight, bbox.y + bbox.h + padY);
1704
- return {
1705
- x: x1,
1706
- y: y1,
1707
- w: Math.max(0, x2 - x1),
1708
- h: Math.max(0, y2 - y1)
1709
- };
1710
- }
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 {
@@ -1793,6 +1874,15 @@ function encodeEmbeddingBase64(embedding) {
1793
1874
  function pickLabel(detection) {
1794
1875
  return detection.labels.find((l) => !l.label.startsWith("embedding:"))?.label;
1795
1876
  }
1877
+ /**
1878
+ * Turn one executor detection into the cap's `DetailResult`, reprojecting its
1879
+ * crop-space bbox back onto the frame the crop came from.
1880
+ *
1881
+ * Exported because the STATELESS step path (`stateless-step.ts`) produces the
1882
+ * same shape from the same executor output and must not grow its own mapping —
1883
+ * a second reprojection is a second set of coordinates, and the two would agree
1884
+ * right up until one of them changed.
1885
+ */
1796
1886
  function toDetailResult(detection, stepId, crop) {
1797
1887
  const label = pickLabel(detection);
1798
1888
  return {
@@ -1806,15 +1896,62 @@ function toDetailResult(detection, stepId, crop) {
1806
1896
  ...detection.nativeFaceShortSidePx !== void 0 ? { nativeFaceShortSidePx: detection.nativeFaceShortSidePx } : {}
1807
1897
  };
1808
1898
  }
1809
- async function resolveCrop(deps, input) {
1899
+ /**
1900
+ * Cut the crop out of a caller-supplied FULL FRAME, applying the one crop
1901
+ * convention. This is the branch that made the rebuild comparable with live
1902
+ * traffic: before it existed the only way to feed stored pixels in was a
1903
+ * pre-cut tile, so the caller decided the rectangle and every caller decided
1904
+ * differently.
1905
+ *
1906
+ * Exported so the STATELESS step path can reach `deriveDetailCropRect` through
1907
+ * this function rather than importing it — `scripts/check-clip-crop-single-owner.ts`
1908
+ * permits exactly one importer, and that is the enforcement of
1909
+ * [D52](../../../../docs/decisions/adr-0052.md). A second caller of the
1910
+ * derivation is a second cutter no matter how similar its arithmetic looks
1911
+ * today.
1912
+ */
1913
+ async function cutFromFullFrame(frameJpegBase64, bbox, convention) {
1914
+ const image = (await require_lazy_sharp.getSharp())(Buffer.from(frameJpegBase64, "base64"));
1915
+ const meta = await image.metadata();
1916
+ const frameWidth = meta.width ?? 0;
1917
+ const frameHeight = meta.height ?? 0;
1918
+ if (frameWidth <= 0 || frameHeight <= 0) return null;
1919
+ const rect = require_dist.deriveDetailCropRect(bbox.kind === "frame-pixels" ? bbox.rect : denormalizeRect(bbox.rect, frameWidth, frameHeight), frameWidth, frameHeight, convention);
1920
+ const left = Math.max(0, Math.min(Math.round(rect.x), frameWidth - 1));
1921
+ const top = Math.max(0, Math.min(Math.round(rect.y), frameHeight - 1));
1922
+ const width = Math.max(1, Math.min(Math.round(rect.w), frameWidth - left));
1923
+ const height = Math.max(1, Math.min(Math.round(rect.h), frameHeight - top));
1924
+ return {
1925
+ jpeg: await image.extract({
1926
+ left,
1927
+ top,
1928
+ width,
1929
+ height
1930
+ }).jpeg({ quality: 90 }).toBuffer(),
1931
+ width,
1932
+ height,
1933
+ frameSpace: {
1934
+ x: left,
1935
+ y: top,
1936
+ w: width,
1937
+ h: height
1938
+ },
1939
+ source: "derived"
1940
+ };
1941
+ }
1942
+ async function resolveCrop(deps, input, convention) {
1810
1943
  if (input.frameHandle) {
1811
- const normalized = normalizeRect(padAndClampFrameBbox(input.parent.bbox, input.frameHandle.width, input.frameHandle.height), input.frameHandle.width, input.frameHandle.height);
1944
+ const normalized = normalizeRect(require_dist.deriveDetailCropRect(input.parent.bbox, input.frameHandle.width, input.frameHandle.height, convention), input.frameHandle.width, input.frameHandle.height);
1812
1945
  const native = await deps.getNativeCrop(input.deviceId, input.frameHandle, normalized);
1813
1946
  if (native) return {
1814
1947
  ...native,
1815
1948
  source: "native"
1816
1949
  };
1817
1950
  }
1951
+ if (input.frameJpeg) return cutFromFullFrame(input.frameJpeg, {
1952
+ kind: "frame-pixels",
1953
+ rect: input.parent.bbox
1954
+ }, convention);
1818
1955
  if (input.cropJpeg) {
1819
1956
  const jpeg = Buffer.from(input.cropJpeg, "base64");
1820
1957
  const meta = await (await require_lazy_sharp.getSharp())(jpeg).metadata();
@@ -1849,11 +1986,42 @@ async function resolveCrop(deps, input) {
1849
1986
  */
1850
1987
  async function runDetailSubtree(deps, input) {
1851
1988
  const matched = deps.resolveChildSteps(input.deviceId, input.parent.className, input.steps);
1852
- if (!matched || matched.length === 0) return null;
1989
+ if (!matched) {
1990
+ deps.logger.warn("runDetailSubtree: camera is not attached — no detail steps to run", {
1991
+ tags: { deviceId: input.deviceId },
1992
+ meta: {
1993
+ parentClass: input.parent.className,
1994
+ ...input.steps ? { requestedSteps: [...input.steps] } : {}
1995
+ }
1996
+ });
1997
+ return null;
1998
+ }
1999
+ if (matched.length === 0) {
2000
+ deps.logger.warn("runDetailSubtree: no matching detail step is enabled for this parent class", {
2001
+ tags: { deviceId: input.deviceId },
2002
+ meta: {
2003
+ parentClass: input.parent.className,
2004
+ ...input.steps ? { requestedSteps: [...input.steps] } : {},
2005
+ hint: "the step is either absent from this camera step tree or disabled on it"
2006
+ }
2007
+ });
2008
+ return null;
2009
+ }
1853
2010
  const requested = input.steps;
1854
2011
  const steps = requested === void 0 ? matched : matched.map((step) => pruneChildStepsToRequested(step, requested));
1855
- const crop = await resolveCrop(deps, input);
1856
- if (!crop) return null;
2012
+ const crop = await resolveCrop(deps, input, deps.resolveCropConvention(input.deviceId));
2013
+ if (!crop) {
2014
+ deps.logger.warn("runDetailSubtree: no crop source resolved — the dispatch produced nothing", {
2015
+ tags: { deviceId: input.deviceId },
2016
+ meta: {
2017
+ parentClass: input.parent.className,
2018
+ hadFrameHandle: input.frameHandle !== void 0,
2019
+ hadFrameJpeg: input.frameJpeg !== void 0,
2020
+ hadCropJpeg: input.cropJpeg !== void 0
2021
+ }
2022
+ });
2023
+ return null;
2024
+ }
1857
2025
  const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
1858
2026
  handle: input.frameHandle,
1859
2027
  cropFrameSpace: crop.frameSpace
@@ -3248,6 +3416,83 @@ var PipelineRunner = class {
3248
3416
  }
3249
3417
  };
3250
3418
  //#endregion
3419
+ //#region src/pipeline-runner/stateless-step.ts
3420
+ function modelServability(step, modelId, format) {
3421
+ const entry = step.models.find((m) => m.id === modelId);
3422
+ if (entry === void 0) return { kind: "custom-model" };
3423
+ if (entry.formats[format] !== void 0) return { kind: "catalog-build" };
3424
+ return {
3425
+ kind: "no-build",
3426
+ availableFormats: Object.keys(entry.formats)
3427
+ };
3428
+ }
3429
+ /** Build the refusal answer, log it, and return it — never one without the other. */
3430
+ function refuse(deps, input, reason, detail) {
3431
+ deps.logger.warn("runStatelessStep refused", {
3432
+ tags: { deviceId: input.sourceDeviceId },
3433
+ meta: {
3434
+ nodeId: deps.nodeId,
3435
+ stepId: input.stepId,
3436
+ modelId: input.modelId,
3437
+ reason,
3438
+ detail
3439
+ }
3440
+ });
3441
+ return {
3442
+ kind: "refused",
3443
+ nodeId: deps.nodeId,
3444
+ reason,
3445
+ detail
3446
+ };
3447
+ }
3448
+ function errorMessage(err) {
3449
+ return err instanceof Error ? err.message : String(err);
3450
+ }
3451
+ /**
3452
+ * Run one step over one subject. See the module header for the two invariants.
3453
+ *
3454
+ * Never throws: every failure mode is a named, logged refusal, because the
3455
+ * caller is a bulk pass whose counters are the only signal an operator gets and
3456
+ * a thrown error there reads as "the whole pass broke".
3457
+ */
3458
+ async function runStatelessStep(deps, input) {
3459
+ const definition = deps.lookupStep(input.stepId);
3460
+ if (definition === null) return refuse(deps, input, "unknown-step", `this node's step catalog has no "${input.stepId}" — version skew, or a custom step that is not installed here`);
3461
+ if (input.bbox.w <= 0 || input.bbox.h <= 0) return refuse(deps, input, "unreadable-frame", `degenerate normalized bbox ${JSON.stringify(input.bbox)} — nothing to crop`);
3462
+ const format = await deps.executionFormat();
3463
+ if (format === null) return refuse(deps, input, "model-not-servable", "this node could not report the engine format it would dispatch on, so the model pin cannot be verified — running anyway would risk a silent format substitution");
3464
+ const servability = modelServability(definition, input.modelId, format);
3465
+ if (servability.kind === "no-build") return refuse(deps, input, "model-not-servable", `model "${input.modelId}" has no ${format} build (catalog ships: ${servability.availableFormats.join(", ") || "none"}) — this node would have substituted the ${format} default`);
3466
+ let decodeError = null;
3467
+ const crop = await cutFromFullFrame(input.frameJpeg, {
3468
+ kind: "normalized",
3469
+ rect: input.bbox
3470
+ }, deps.resolveCropConvention()).catch((err) => {
3471
+ decodeError = errorMessage(err);
3472
+ return null;
3473
+ });
3474
+ if (crop === null) return refuse(deps, input, "unreadable-frame", decodeError ?? "the supplied frame decoded to no readable dimensions");
3475
+ const steps = [{
3476
+ addonId: input.stepId,
3477
+ modelId: input.modelId,
3478
+ enabled: true
3479
+ }];
3480
+ let result;
3481
+ try {
3482
+ result = await deps.runPipeline(steps, crop.jpeg, input.sourceDeviceId);
3483
+ } catch (err) {
3484
+ return refuse(deps, input, "execution-failed", errorMessage(err));
3485
+ }
3486
+ if (result === null) return refuse(deps, input, "execution-failed", "the executor returned no frame result for the crop");
3487
+ const details = result.detections.map((detection) => toDetailResult(detection, input.stepId, crop));
3488
+ return {
3489
+ kind: "ran",
3490
+ nodeId: deps.nodeId,
3491
+ modelId: input.modelId,
3492
+ details
3493
+ };
3494
+ }
3495
+ //#endregion
3251
3496
  //#region src/pipeline-runner/index.ts
3252
3497
  var DEFAULT_CONFIG = {
3253
3498
  maxQueueDepth: 30,
@@ -4605,14 +4850,90 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4605
4850
  * the frame's owning process), and the node-local `pipelineExecutor`.
4606
4851
  */
4607
4852
  async runDetailSubtree(input) {
4853
+ const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
4608
4854
  return runDetailSubtree({
4609
4855
  resolveChildSteps: (deviceId, parentClassName, filter) => this.resolveDetailChildSteps(deviceId, parentClassName, filter),
4610
4856
  getNativeCrop: (deviceId, handle, paddedBbox) => this.getNativeCropForDetail(deviceId, handle, paddedBbox),
4611
4857
  runPipeline: (steps, imageJpeg, deviceId, deviceKey, nativeCropRef) => this.runDetailPipeline(steps, imageJpeg, deviceId, deviceKey, nativeCropRef),
4612
- resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step)
4858
+ resolveStepDevice: (deviceId, step) => this.resolveDetailStepDevice(deviceId, step),
4859
+ resolveCropConvention: () => convention,
4860
+ logger: this.detailSubtreeLogger()
4861
+ }, input);
4862
+ }
4863
+ /**
4864
+ * Stateless single-step execution — no attach, no handle, no device
4865
+ * affinity. Delegates the decision table (catalog lookup, MODEL PIN
4866
+ * servability, crop, dispatch) to the deps-injected pure function in
4867
+ * {@link runStatelessStepImpl}; this method only wires the real deps.
4868
+ *
4869
+ * Note what is NOT wired, deliberately: no `resolveChildSteps` (the step
4870
+ * comes from the catalog, not from an attachment) and no `deviceKey` /
4871
+ * device jump (the jump roster is part of the attach config). The step runs
4872
+ * on the node's default engine, which is exactly the engine
4873
+ * `executionFormat` reports — so the servability check and the dispatch
4874
+ * cannot disagree about which format was verified.
4875
+ */
4876
+ async runStatelessStep(input) {
4877
+ const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
4878
+ return runStatelessStep({
4879
+ nodeId: this.nodeId,
4880
+ lookupStep: (stepId) => tryGetStepDefinition(stepId),
4881
+ executionFormat: () => this.resolveNodeExecutionFormat(),
4882
+ resolveCropConvention: () => convention,
4883
+ runPipeline: (steps, imageJpeg, sourceDeviceId) => this.runStatelessPipeline(steps, imageJpeg, sourceDeviceId),
4884
+ logger: this.detailSubtreeLogger()
4613
4885
  }, input);
4614
4886
  }
4615
4887
  /**
4888
+ * The model format this node dispatches on when no `deviceKey` is given —
4889
+ * its selected engine's format. `null` on any failure, which the caller
4890
+ * turns into a refusal rather than a guess: a model pin that cannot be
4891
+ * checked is a substitution waiting to happen.
4892
+ */
4893
+ async resolveNodeExecutionFormat() {
4894
+ const api = this.ctxIfReady?.api;
4895
+ if (!api) return null;
4896
+ try {
4897
+ return (await api.pipelineExecutor.getSelectedEngine.query()).format;
4898
+ } catch (err) {
4899
+ this.ctxIfReady?.logger.warn("runStatelessStep: could not read this node selected engine — refusing rather than guessing a format", { meta: { error: require_dist.errMsg(err) } });
4900
+ return null;
4901
+ }
4902
+ }
4903
+ /**
4904
+ * Real `runPipeline` dep for the stateless path: node-local
4905
+ * `pipelineExecutor.runPipeline` on the `'full'` plane with NO `deviceKey`.
4906
+ *
4907
+ * `deviceId` is passed so the executor's own gate/substitution lines are
4908
+ * tagged with the source camera. It is a routing hint on other transports,
4909
+ * but not here: the executor is co-located in the `detection` group, so
4910
+ * `localProviderLink` answers in-process before any router sees it — the
4911
+ * call cannot leave the node this one was pinned to.
4912
+ */
4913
+ async runStatelessPipeline(steps, imageJpeg, sourceDeviceId) {
4914
+ const api = this.ctxIfReady?.api;
4915
+ if (!api) return null;
4916
+ return api.pipelineExecutor.runPipeline.mutate({
4917
+ steps: [...steps],
4918
+ image: new Uint8Array(imageJpeg),
4919
+ deviceId: sourceDeviceId,
4920
+ plane: "full"
4921
+ });
4922
+ }
4923
+ /**
4924
+ * The cluster-wide detail-crop convention — read from the orchestrator's
4925
+ * global settings, NOT from the attach config. See `crop-convention-source.ts`
4926
+ * for why eleven attach builders make the attach config the wrong carrier.
4927
+ */
4928
+ cropConvention = new CropConventionSource({ logger: {
4929
+ info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
4930
+ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
4931
+ } });
4932
+ /** A warn sink that survives a not-yet-ready ctx (boot-order dispatches). */
4933
+ detailSubtreeLogger() {
4934
+ return { warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) };
4935
+ }
4936
+ /**
4616
4937
  * Step-tree device jump (phase 1) — pick which same-node inference device an
4617
4938
  * enrichment `step` runs on. AUTO-jumps only when the camera's elected device
4618
4939
  * cannot run the step's model (its format has no build); an operator