@camstack/addon-pipeline 1.2.327 → 1.2.329

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 (44) hide show
  1. package/THIRD_PARTY_MODELS.md +35 -27
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/{default-detection-model-CBIYdAJ5.js → default-detection-model-ChH7Y219.js} +130 -61
  5. package/dist/{default-detection-model-hql6BX9-.mjs → default-detection-model-j0nBpRoB.mjs} +130 -61
  6. package/dist/detection-pipeline/index.js +458 -16
  7. package/dist/detection-pipeline/index.mjs +458 -16
  8. package/dist/{dist-B9LnBPz9.mjs → dist-0HoA3wAi.mjs} +503 -370
  9. package/dist/{dist-BBZwYfSN.js → dist-BlIWhjnu.js} +550 -369
  10. package/dist/motion-wasm/index.js +1 -1
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-mJQBUiEc.js → node-BPpVMnZy.js} +1 -1
  13. package/dist/{node-Ci0E-0VP.mjs → node-HOMWRPzW.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +215 -78
  15. package/dist/pipeline-runner/index.mjs +215 -78
  16. package/dist/{process-memory-DgEfKdtC.js → process-memory-BW06vOIQ.js} +1 -1
  17. package/dist/{process-memory-B63Jlpev.mjs → process-memory-DuF8pa2w.mjs} +1 -1
  18. package/dist/recorder/index.js +2 -2
  19. package/dist/recorder/index.mjs +2 -2
  20. package/dist/{segment-demux-js-DzwHW4Ey.mjs → segment-demux-js-BU2MXpln.mjs} +1 -1
  21. package/dist/{segment-demux-js-C2ufKMu0.js → segment-demux-js-Dl52Zz8w.js} +1 -1
  22. package/dist/session-decode/decode-worker-main.js +13 -21
  23. package/dist/session-decode/decode-worker-main.mjs +13 -21
  24. package/dist/stream-broker/_stub.js +1 -1
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B4fPzS-8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vNfS65In.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DfeHvW4k.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-Cg4IdtE5.mjs → hostInit-Ciacp783.mjs} +3 -3
  30. package/dist/stream-broker/index.js +4 -3
  31. package/dist/stream-broker/index.mjs +4 -3
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/python/edgetpu_dequant.py +38 -1
  35. package/python/inference_pool.py +69 -138
  36. package/python/postprocessors/test_bird_edgetpu.py +124 -0
  37. package/python/postprocessors/test_yolox_plate_edgetpu.py +126 -0
  38. package/python/postprocessors/test_yunet_edgetpu.py +149 -0
  39. package/python/postprocessors/testdata/bird_classifier_int8_outputs.json +1 -0
  40. package/python/postprocessors/testdata/yolox_plate_int8_outputs.json +1 -0
  41. package/python/postprocessors/testdata/yunet_int8_outputs.json +1 -0
  42. package/python/postprocessors/yolo.py +44 -0
  43. package/python/test_inference_pool_static_batch.py +2 -4
  44. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDGzcgLP.mjs +0 -26
@@ -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-BBZwYfSN.js");
5
+ const require_dist = require("../dist-BlIWhjnu.js");
6
6
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Sn as hydrateSchema, ct as evaluateZoneRules, dn as DeviceType, kt as motionDetectionCapability, mn as BaseAddon } from "../dist-B9LnBPz9.mjs";
1
+ import { An as hydrateSchema, It as motionDetectionCapability, Sn as BaseAddon, pt as evaluateZoneRules, yn as DeviceType } from "../dist-0HoA3wAi.mjs";
2
2
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-BBZwYfSN.js");
2
+ const require_dist = require("./dist-BlIWhjnu.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs, 1);
@@ -1,4 +1,4 @@
1
- import { an as Fmp4BoxSplitter, fn as errMsg, ln as isSoftwareDecode, on as buildFfmpegArgs } from "./dist-B9LnBPz9.mjs";
1
+ import { _n as isSoftwareDecode, bn as errMsg, mn as buildFfmpegArgs, pn as Fmp4BoxSplitter } from "./dist-0HoA3wAi.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
@@ -3,9 +3,9 @@ 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-BBZwYfSN.js");
7
- const require_node = require("../node-mJQBUiEc.js");
8
- const require_default_detection_model = require("../default-detection-model-CBIYdAJ5.js");
6
+ const require_dist = require("../dist-BlIWhjnu.js");
7
+ const require_node = require("../node-BPpVMnZy.js");
8
+ const require_default_detection_model = require("../default-detection-model-ChH7Y219.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
10
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
11
11
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
@@ -903,10 +903,12 @@ function classifyNativeCropLeaseAge(ageMs, maxAgeMs = NATIVE_CROP_LEASE_MAX_AGE_
903
903
  }
904
904
  //#endregion
905
905
  //#region src/session-decode/native-crop-registry.ts
906
- var DEFAULT_NATIVE_CROP_REGISTRY_CAP = (() => {
907
- const raw = Number(process.env["CAMSTACK_NATIVE_CROP_REGISTRY_CAP"]);
908
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4096;
909
- })();
906
+ /**
907
+ * Default max entries retained across all active decode sessions on a node
908
+ * (64 → 256 → 4096; see the class doc for the two windows the cap has been
909
+ * sized to).
910
+ */
911
+ var DEFAULT_NATIVE_CROP_REGISTRY_CAP = 4096;
910
912
  /** Invoke a requester's best-effort `release()`, swallowing any transport error. */
911
913
  function safeRelease(request) {
912
914
  try {
@@ -1173,20 +1175,6 @@ function createRetainedContentFallback(registry, ref) {
1173
1175
  }
1174
1176
  } };
1175
1177
  }
1176
- //#endregion
1177
- //#region src/session-decode/parked-frame-store.ts
1178
- /** Per-camera parcel bound. 24 live tracks × two kinds on one camera is a
1179
- * crowd scene; past that the camera's oldest parcel leaves first. Read ONCE
1180
- * from `CAMSTACK_PARKED_FRAMES_PER_DEVICE`. */
1181
- var PARKED_FRAMES_PER_DEVICE_MAX = (() => {
1182
- const raw = Number(process.env["CAMSTACK_PARKED_FRAMES_PER_DEVICE"]);
1183
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 48;
1184
- })();
1185
- /** {@link PARKED_FRAME_BUDGET_MB} in bytes. */
1186
- var PARKED_FRAME_BUDGET_BYTES = (() => {
1187
- const raw = Number(process.env["CAMSTACK_PARKED_FRAME_MB"]);
1188
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 64;
1189
- })() * 1024 * 1024;
1190
1178
  function keyOf$1(deviceId, trackId, kind) {
1191
1179
  return `${deviceId}|${trackId}|${kind}`;
1192
1180
  }
@@ -1203,8 +1191,8 @@ var ParkedFrameStore = class {
1203
1191
  perDeviceKeys = /* @__PURE__ */ new Map();
1204
1192
  totalBytes = 0;
1205
1193
  constructor(options = {}) {
1206
- this.budgetBytes = options.budgetBytes ?? PARKED_FRAME_BUDGET_BYTES;
1207
- this.perDeviceMax = options.perDeviceMax ?? PARKED_FRAMES_PER_DEVICE_MAX;
1194
+ this.budgetBytes = options.budgetBytes ?? 67108864;
1195
+ this.perDeviceMax = options.perDeviceMax ?? 48;
1208
1196
  this.maxAgeMs = options.maxAgeMs ?? 6e5;
1209
1197
  this.now = options.now ?? Date.now;
1210
1198
  this.onEvict = options.onEvict;
@@ -1389,47 +1377,6 @@ var PartitionedParkedFrameStore = class {
1389
1377
  this.faceSource.clear();
1390
1378
  }
1391
1379
  };
1392
- /** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
1393
- var RETAINED_FRAME_BUDGET_BYTES = (() => {
1394
- const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
1395
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 160;
1396
- })() * 1024 * 1024;
1397
- /**
1398
- * How many of each device's MOST RECENT frames are protected from a busy
1399
- * neighbour's budget eviction. Sized to cover the late two-plane / cross-process
1400
- * enrichment horizon (~150-500 ms) at the REAL delivered rate: the store writes
1401
- * every DELIVERED rgb frame, and a high-fps camera delivers ~25 fps (observed on
1402
- * the 4K outdoor cams), so the old floor of 6 protected only ~240 ms there and
1403
- * its late captures hit "frame recycled before resolve" (2026-07-22 triage —
1404
- * missing thumbnails/firstFrames concentrated on exactly those cameras).
1405
- * 16 frames ≈ 0.64 s @25 fps (1.6 s @10 fps). Only ACTIVE cameras hold frames,
1406
- * so the worst-case protected footprint stays well inside the byte budget for
1407
- * realistic concurrency (4 active cams ≈ 44 MB of 160); the hard ceiling always
1408
- * overrides the floor regardless. Read ONCE from `CAMSTACK_SESSION_RETAIN_FLOOR`;
1409
- * `0` disables the floor (pure global FIFO — the pre-2026-07-19 behaviour).
1410
- */
1411
- var RETAINED_FRAME_PER_DEVICE_FLOOR = (() => {
1412
- const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR"]);
1413
- return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 16;
1414
- })();
1415
- /**
1416
- * TIME-based per-device protection (2026-07-23 cold-window fix): a frame
1417
- * YOUNGER than this survives budget eviction regardless of how many frames
1418
- * its device holds. The COUNT floor above protects a fixed number of frames —
1419
- * but the wall-clock window that buys depends on the delivered fps (16 frames
1420
- * = 1.6 s @10 fps but only 0.64 s @25 fps, and the 4K street cams deliver
1421
- * ~25). The late-capture horizon is TIME (session attach + enrichment +
1422
- * round-trip ≈ 0.5–1.5 s, worst on cold on-motion dials), so the guarantee
1423
- * must be time-denominated: 12/13 of the zero-media/missing-firstFrame tracks
1424
- * in the 2026-07-23 audit were ≤6 s cold-session subjects whose handles were
1425
- * recycled before their first captures ran. Bounded by the hard byte ceiling
1426
- * (pass 2) exactly like the count floor — worst case ~26 MB per ACTIVE camera
1427
- * @25 fps. `CAMSTACK_SESSION_RETAIN_FLOOR_MS`; `0` disables (count floor only).
1428
- */
1429
- var RETAINED_FRAME_TIME_FLOOR_MS = (() => {
1430
- const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_FLOOR_MS"]);
1431
- return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 3e3;
1432
- })();
1433
1380
  /** Opaque RAM-key prefix stamped into a retained frame's `FrameHandle.shmId`. */
1434
1381
  var RETAINED_FRAME_KEY_PREFIX = "ram:";
1435
1382
  /** A retention frame is uniquely identified by its key + slot + committed seq. */
@@ -1468,9 +1415,9 @@ var RetainedFrameStore = class RetainedFrameStore {
1468
1415
  static SLOT_SPREAD = 1024;
1469
1416
  constructor(options) {
1470
1417
  this.nodeId = options.nodeId;
1471
- this.budgetBytes = options.budgetBytes ?? RETAINED_FRAME_BUDGET_BYTES;
1472
- this.perDeviceFloor = options.perDeviceFloor ?? RETAINED_FRAME_PER_DEVICE_FLOOR;
1473
- this.timeFloorMs = options.timeFloorMs ?? RETAINED_FRAME_TIME_FLOOR_MS;
1418
+ this.budgetBytes = options.budgetBytes ?? 167772160;
1419
+ this.perDeviceFloor = options.perDeviceFloor ?? 16;
1420
+ this.timeFloorMs = options.timeFloorMs ?? 3e3;
1474
1421
  this.now = options.now ?? Date.now;
1475
1422
  this.onEvict = options.onEvict;
1476
1423
  }
@@ -2029,6 +1976,154 @@ function onboardRootDetections(slot, frame, gate) {
2029
1976
  };
2030
1977
  }
2031
1978
  //#endregion
1979
+ //#region src/pipeline-runner/label-link.ts
1980
+ /**
1981
+ * The runner's half of the detector ↔ step label link (D715).
1982
+ *
1983
+ * The runner holds the camera's WHOLE step tree (the attach config), so it is
1984
+ * the one place that can answer "who writes this effect?" for a camera: the
1985
+ * root model's declaration (`fillsClassifierEffects`) against the filled
1986
+ * step's own `labelLink` policy and class list (`resolveLabelLink`). Three
1987
+ * consumers read that one answer:
1988
+ *
1989
+ * - the frame result — a linked label the executor wrote at score 0 is raised
1990
+ * to the detection's score when the detector is authoritative;
1991
+ * - the `detailSteps` announce — a skipped classifier is announced with
1992
+ * `filledBy`, so post-analysis does not dispatch it for a subject the
1993
+ * detector already named;
1994
+ * - `package-detection` — when the root fills it, the package root is not
1995
+ * dispatched at all and the root's own `package` boxes ARE the package
1996
+ * result (the operator's package threshold moves onto the root's gate, and
1997
+ * the frame says it looked).
1998
+ *
1999
+ * Pure: the caller passes the model resolver; no I/O, no clock.
2000
+ */
2001
+ /** The root step the fills are read from — the always-on camera root. */
2002
+ var LABEL_LINK_ROOT_STEP_ID = "object-detection";
2003
+ var PACKAGE_STEP_ID = "package-detection";
2004
+ var PACKAGE_THRESHOLD_KEY = "minConfidencePackage";
2005
+ var EMPTY_LABEL_LINK_PLAN = { decisions: /* @__PURE__ */ new Map() };
2006
+ function findStep(steps, addonId) {
2007
+ for (const step of steps) {
2008
+ if (step.addonId === addonId) return step;
2009
+ const nested = step.children !== void 0 ? findStep(step.children, addonId) : void 0;
2010
+ if (nested !== void 0) return nested;
2011
+ }
2012
+ }
2013
+ /**
2014
+ * Plan a camera's label links from its configured tree. A root with no pinned
2015
+ * model runs the static default — a COCO model, which fills nothing.
2016
+ */
2017
+ function planLabelLinks(steps, resolveModel) {
2018
+ if (steps === void 0) return EMPTY_LABEL_LINK_PLAN;
2019
+ const root = steps.find((s) => s.addonId === "object-detection" && s.enabled !== false);
2020
+ if (root?.modelId === void 0) return EMPTY_LABEL_LINK_PLAN;
2021
+ const fills = resolveModel("object-detection", root.modelId)?.fillsClassifierEffects ?? [];
2022
+ if (fills.length === 0) return {
2023
+ rootModelId: root.modelId,
2024
+ decisions: /* @__PURE__ */ new Map()
2025
+ };
2026
+ const decisions = /* @__PURE__ */ new Map();
2027
+ for (const fill of fills) {
2028
+ const step = findStep(steps, fill.stepId);
2029
+ decisions.set(fill.stepId, require_dist.resolveLabelLink(fill, step === void 0 ? void 0 : {
2030
+ enabled: step.enabled !== false,
2031
+ ...step.settings !== void 0 ? { settings: step.settings } : {}
2032
+ }));
2033
+ }
2034
+ return {
2035
+ rootModelId: root.modelId,
2036
+ decisions
2037
+ };
2038
+ }
2039
+ /** Steps this plan says must not be dispatched, with the model that fills them. */
2040
+ function filledStepIds(plan) {
2041
+ const out = /* @__PURE__ */ new Map();
2042
+ if (plan.rootModelId === void 0) return out;
2043
+ for (const [stepId, decision] of plan.decisions) if (decision.skip) out.set(stepId, plan.rootModelId);
2044
+ return out;
2045
+ }
2046
+ function raiseLinkedLabels(detection, plan) {
2047
+ let changed = false;
2048
+ const labels = detection.labels.map((label) => {
2049
+ const stepId = require_dist.linkedStepOf(label.stepId);
2050
+ if (stepId === void 0 || plan.decisions.get(stepId)?.authoritative !== true) return label;
2051
+ if (label.score === detection.score) return label;
2052
+ changed = true;
2053
+ return {
2054
+ ...label,
2055
+ score: detection.score
2056
+ };
2057
+ });
2058
+ return changed ? labels : void 0;
2059
+ }
2060
+ /**
2061
+ * Raise each linked label to its detection's score where the detector is the
2062
+ * authoritative writer of that effect. Every other linked label stays the
2063
+ * score-0 fallback the executor wrote. Never mutates; returns the input when
2064
+ * nothing changes.
2065
+ */
2066
+ function applyLabelLinkAuthority(result, plan) {
2067
+ if (plan.decisions.size === 0) return result;
2068
+ let changed = false;
2069
+ const detections = result.detections.map((d) => {
2070
+ const labels = raiseLinkedLabels(d, plan);
2071
+ if (labels === void 0) return d;
2072
+ changed = true;
2073
+ return {
2074
+ ...d,
2075
+ labels
2076
+ };
2077
+ });
2078
+ return changed ? {
2079
+ ...result,
2080
+ detections
2081
+ } : result;
2082
+ }
2083
+ /** The root step's schema default for its class allow-list. */
2084
+ function rootDefaultMacroClasses() {
2085
+ const field = require_default_detection_model.getStep(LABEL_LINK_ROOT_STEP_ID).getConfigSchema().find((f) => "key" in f && f.key === "enabledMacroClasses");
2086
+ const value = field !== void 0 && "default" in field ? field.default : void 0;
2087
+ return Array.isArray(value) ? value : [];
2088
+ }
2089
+ function withPackageAdmitted(root, packageStep) {
2090
+ const settings = root.settings ?? {};
2091
+ const stored = settings["enabledMacroClasses"];
2092
+ const enabled = Array.isArray(stored) ? stored : rootDefaultMacroClasses();
2093
+ const admitted = enabled.length > 0 && !enabled.includes("package") ? { enabledMacroClasses: [...enabled, "package"] } : {};
2094
+ const threshold = packageStep.settings?.[PACKAGE_THRESHOLD_KEY];
2095
+ const gate = typeof threshold === "number" ? { [PACKAGE_THRESHOLD_KEY]: threshold } : {};
2096
+ return {
2097
+ ...root,
2098
+ settings: {
2099
+ ...settings,
2100
+ ...admitted,
2101
+ ...gate
2102
+ }
2103
+ };
2104
+ }
2105
+ /**
2106
+ * When the root fills `package-detection` and that step's policy lets it be
2107
+ * skipped, drop the package root from the dispatch and let the camera root
2108
+ * report packages: its gate admits `package` at the package step's threshold.
2109
+ * Otherwise the tree is returned untouched.
2110
+ */
2111
+ function applyPackageFill(steps, plan) {
2112
+ if (plan.decisions.get(PACKAGE_STEP_ID)?.skip !== true) return {
2113
+ steps,
2114
+ filledByRoot: false
2115
+ };
2116
+ const packageStep = steps.find((s) => s.addonId === PACKAGE_STEP_ID);
2117
+ if (packageStep === void 0) return {
2118
+ steps,
2119
+ filledByRoot: false
2120
+ };
2121
+ return {
2122
+ steps: steps.filter((s) => s.addonId !== PACKAGE_STEP_ID).map((s) => s.addonId === "object-detection" && s.enabled !== false ? withPackageAdmitted(s, packageStep) : s),
2123
+ filledByRoot: true
2124
+ };
2125
+ }
2126
+ //#endregion
2032
2127
  //#region src/pipeline-runner/bench-step-schema.ts
2033
2128
  /**
2034
2129
  * Shared bench-facing schemas: the minimal recursive step tree. Extracted from
@@ -4545,9 +4640,9 @@ var PACKAGE_DETECTION_STEP_ID = "package-detection";
4545
4640
  * through) and the frame was not shed. Post-analysis reads it as "an empty
4546
4641
  * doormat on this frame is an ABSENCE, not silence" (D687). Never mutates.
4547
4642
  */
4548
- function withPackageLook(result, dispatched) {
4643
+ function withPackageLook(result, dispatched, filledByRoot = false) {
4549
4644
  if (result.shed === true) return result;
4550
- return dispatched.some((s) => s.addonId === "package-detection" && s.enabled !== false) ? {
4645
+ return filledByRoot || dispatched.some((s) => s.addonId === "package-detection" && s.enabled !== false) ? {
4551
4646
  ...result,
4552
4647
  packageLooked: true
4553
4648
  } : result;
@@ -7247,20 +7342,23 @@ function isDetailPlaneStepId(stepId) {
7247
7342
  * Extracted as a pure function so the unit suite can exercise the
7248
7343
  * derivation without instantiating the full addon.
7249
7344
  */
7250
- function deriveDetailSteps(steps) {
7345
+ function deriveDetailSteps(steps, plan = EMPTY_LABEL_LINK_PLAN) {
7251
7346
  const stepIds = /* @__PURE__ */ new Set();
7252
7347
  collectEnabledStepIds(steps, stepIds);
7348
+ const filled = filledStepIds(plan);
7253
7349
  const announces = [];
7254
7350
  for (const stepId of stepIds) {
7255
7351
  const def = tryGetStepDefinition(stepId);
7256
7352
  if (!def || def.inputClasses === null) continue;
7353
+ const filledBy = filled.get(def.id);
7257
7354
  announces.push({
7258
7355
  stepId: def.id,
7259
7356
  inputClasses: def.inputClasses,
7260
7357
  cadence: def.cadence ?? {
7261
7358
  trigger: "once",
7262
7359
  maxPerTrack: 3
7263
- }
7360
+ },
7361
+ ...filledBy !== void 0 ? { filledBy } : {}
7264
7362
  });
7265
7363
  }
7266
7364
  return announces;
@@ -9576,6 +9674,42 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9576
9674
  * read synchronously by the placement.
9577
9675
  */
9578
9676
  customModels = new require_default_detection_model.CustomModelCatalog({ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras) });
9677
+ /**
9678
+ * Per camera, the label-link plan (D715) and the announce derived from it,
9679
+ * keyed on the two inputs it reads: the attach tree and the custom-model
9680
+ * snapshot (a Frigate+ root is a custom model). Recomputed when either
9681
+ * changes identity — at most once per catalog refresh.
9682
+ */
9683
+ labelLinkCache = /* @__PURE__ */ new Map();
9684
+ /** The camera's label-link plan; logs once per change of what is filled. */
9685
+ labelLinkPlanFor(deviceId, steps) {
9686
+ const byStep = this.customModels.current();
9687
+ const cached = this.labelLinkCache.get(deviceId);
9688
+ if (cached !== void 0 && cached.steps === steps && cached.byStep === byStep) return cached.plan;
9689
+ const plan = planLabelLinks(steps, (stepId, modelId) => this.customModels.stepDefinition(stepId)?.models.find((m) => m.id === modelId));
9690
+ const announces = deriveDetailSteps(steps, plan);
9691
+ this.labelLinkCache.set(deviceId, {
9692
+ steps,
9693
+ byStep,
9694
+ plan,
9695
+ announces
9696
+ });
9697
+ const describe = (p) => p === void 0 ? "" : [...p.decisions].map(([id, d]) => `${id}:${d.skip ? "skip" : d.authoritative ? "sole" : "fallback"}`).join(",");
9698
+ if (describe(cached?.plan) !== describe(plan) && plan.decisions.size > 0) this.ctxIfReady?.logger.info("label link: the root model fills pipeline steps", {
9699
+ tags: { deviceId },
9700
+ meta: {
9701
+ rootModelId: plan.rootModelId ?? null,
9702
+ steps: [...plan.decisions].map(([stepId, d]) => ({
9703
+ stepId,
9704
+ policy: d.policy,
9705
+ authoritative: d.authoritative,
9706
+ skip: d.skip,
9707
+ openGaps: d.openGaps
9708
+ }))
9709
+ }
9710
+ });
9711
+ return plan;
9712
+ }
9579
9713
  cropConvention = new CropConventionSource({ logger: {
9580
9714
  info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
9581
9715
  warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
@@ -9971,6 +10105,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9971
10105
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
9972
10106
  this.packageGovernor.forget(deviceId);
9973
10107
  this.packageGateSkips.delete(deviceId);
10108
+ this.labelLinkCache.delete(deviceId);
9974
10109
  this.packageZoneUnionByDevice.delete(deviceId);
9975
10110
  this.packageZoneUnionAt.delete(deviceId);
9976
10111
  this.lastMotionNearPackageZone.delete(deviceId);
@@ -10367,7 +10502,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10367
10502
  acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config), true),
10368
10503
  startPump: (source, hooks) => {
10369
10504
  const dialMs = Date.now() - requestedAt;
10370
- const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
10505
+ const useLocalFrameRef = this.executorCoLocated();
10371
10506
  log.info("session-decode: detection routed through per-session decode worker (rgb)", { meta: {
10372
10507
  streamId: config.detectionStreamId,
10373
10508
  codec: source.codec,
@@ -10686,8 +10821,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10686
10821
  return null;
10687
10822
  }
10688
10823
  if (steps.length === 0) return null;
10689
- let dispatchSteps = steps;
10690
- if (steps.some((s) => s.addonId === "package-detection" && s.enabled !== false)) {
10824
+ const labelLinkPlan = this.labelLinkPlanFor(deviceId, steps);
10825
+ const packageFill = applyPackageFill(steps, labelLinkPlan);
10826
+ let dispatchSteps = packageFill.steps;
10827
+ if (dispatchSteps.some((s) => s.addonId === "package-detection" && s.enabled !== false)) {
10691
10828
  this.refreshPackageZoneUnion(deviceId);
10692
10829
  const motionInZone = this.lastMotionNearPackageZone.get(deviceId) ?? this.lastAnalyzerDetected.get(deviceId) ?? false;
10693
10830
  const decision = this.packageGovernor.evaluate({
@@ -10697,7 +10834,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10697
10834
  });
10698
10835
  const skipped = this.packageGateSkips.get(deviceId) ?? 0;
10699
10836
  if (!decision.run) {
10700
- dispatchSteps = steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
10837
+ dispatchSteps = dispatchSteps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
10701
10838
  if (skipped === 0) log.info("package-detection gated off by the motion-settle governor", {
10702
10839
  tags: { deviceId },
10703
10840
  meta: { motionInZone: this.lastAnalyzerDetected.get(deviceId) ?? false }
@@ -10774,7 +10911,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10774
10911
  return null;
10775
10912
  }
10776
10913
  if (result === null) return null;
10777
- const looked = withPackageLook(result, dispatchSteps);
10914
+ const looked = withPackageLook(applyLabelLinkAuthority(result, labelLinkPlan), dispatchSteps, packageFill.filledByRoot);
10778
10915
  return this.recoverSmallSubjects(deviceId, frame, looked, dispatchSteps, camConfig?.deviceKey);
10779
10916
  }
10780
10917
  /**
@@ -11421,7 +11558,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11421
11558
  ...this.liveWorkerSlotWaiter(deviceId),
11422
11559
  acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:occupancy:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
11423
11560
  startPump: (source, hooks) => {
11424
- const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
11561
+ const useLocalFrameRef = this.executorCoLocated();
11425
11562
  return startSessionDecodePump({
11426
11563
  fork: this.forkDecodeWorkerWithLease,
11427
11564
  lease: this.resolveSessionLeaseKnobs,
@@ -11471,7 +11608,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11471
11608
  emitInferenceResult(deviceId, frame, result, handle, source, occasion) {
11472
11609
  if (!this.ctx?.eventBus) return;
11473
11610
  const capturedAt = frame.capturedAt;
11474
- const detailSteps = this.attached.get(deviceId)?.detailSteps;
11611
+ const detailSteps = this.labelLinkCache.get(deviceId)?.announces ?? this.attached.get(deviceId)?.detailSteps;
11475
11612
  const payload = {
11476
11613
  deviceId,
11477
11614
  frame: result,