@camstack/addon-pipeline 1.1.64 → 1.1.66

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 (42) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +188 -26
  4. package/dist/detection-pipeline/index.mjs +188 -26
  5. package/dist/{dist-BzVZ2I4P.mjs → dist-BpWot5-w.mjs} +13 -55
  6. package/dist/{dist-B2jTt7Lq.js → dist-D5o9fGxq.js} +13 -55
  7. package/dist/motion-wasm/index.js +1 -1
  8. package/dist/motion-wasm/index.mjs +1 -1
  9. package/dist/pipeline-runner/index.js +88 -22
  10. package/dist/pipeline-runner/index.mjs +88 -22
  11. package/dist/recorder/index.js +141 -1
  12. package/dist/recorder/index.mjs +141 -1
  13. package/dist/session-decode/decode-worker-child.js +9 -3
  14. package/dist/session-decode/decode-worker-child.mjs +9 -3
  15. package/dist/{step-definitions-DXkY5Zor.js → step-definitions-81eYyJbW.js} +138 -8
  16. package/dist/{step-definitions-B07M9qhj.mjs → step-definitions-D6R9X0V7.mjs} +138 -8
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-dTMki_1D.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CIIIVPzB.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-CPbLcza9.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-DlALQ_wp.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-BY2oQAEX.mjs → hostInit-CeBNudbY.mjs} +2 -2
  22. package/dist/stream-broker/index.js +51 -3
  23. package/dist/stream-broker/index.mjs +51 -3
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C03fx7Sp.js → MaskShapeCanvas-DI4BY7W2-CbBbcdZb.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BTxzn1rQ.js → MotionZonesSettings-NcxxQN8r-aA6zvms_.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-GKO8O7rC.js → PrivacyMaskSettings-APgPLF7p-CcGB7iAE.js} +1 -1
  28. package/embed-dist/assets/index-BRG_Fqb5.js +114 -0
  29. package/embed-dist/assets/index-D2pF9Z3W.css +2 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +30 -0
  33. package/python/postprocessors/scrfd.py +4 -1
  34. package/python/postprocessors/softmax.py +17 -5
  35. package/python/postprocessors/test_softmax.py +52 -0
  36. package/python/postprocessors/test_yolo_nms.py +45 -0
  37. package/python/postprocessors/yolo.py +4 -1
  38. package/python/postprocessors/yolo_seg.py +4 -1
  39. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bAh96WNb.mjs +0 -26
  40. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CV2WbUaO.mjs +0 -26
  41. package/embed-dist/assets/index-Bm9LaQPH.css +0 -2
  42. package/embed-dist/assets/index-QdssILAA.js +0 -81
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B2jTt7Lq.js");
6
+ const require_dist = require("../dist-D5o9fGxq.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-BzVZ2I4P.mjs";
2
+ import { B as errMsg, K as hydrateSchema, T as mapAudioLabelToMacro, V as BaseAddon, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-BpWot5-w.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { i as downloadFile } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
5
5
  import * as fs from "node:fs";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-B2jTt7Lq.js");
7
- const require_step_definitions = require("../step-definitions-DXkY5Zor.js");
6
+ const require_dist = require("../dist-D5o9fGxq.js");
7
+ const require_step_definitions = require("../step-definitions-81eYyJbW.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
9
  const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
10
10
  let node_child_process = require("node:child_process");
@@ -316,7 +316,7 @@ function projectNodeEngine(store, nodeId) {
316
316
  //#endregion
317
317
  //#region src/detection-pipeline/default-detection-model.ts
318
318
  /** The object-detection step id — the only slot this resolver applies to. */
319
- var OBJECT_DETECTION_STEP_ID = "object-detection";
319
+ var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
320
320
  /**
321
321
  * Balanced default object-detection model per accelerator class (measured on
322
322
  * COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
@@ -376,7 +376,7 @@ function classifyAccelerator(backend, device) {
376
376
  function resolveDefaultDetectionModel(backend, device, format, getStepDef = require_step_definitions.getStepDefinition) {
377
377
  let def;
378
378
  try {
379
- def = getStepDef(OBJECT_DETECTION_STEP_ID);
379
+ def = getStepDef(OBJECT_DETECTION_STEP_ID$1);
380
380
  } catch {
381
381
  return "yolo26n";
382
382
  }
@@ -951,6 +951,9 @@ function serializeModelConfig(config) {
951
951
  postprocessor: config.postprocessor,
952
952
  confidence: config.confidence
953
953
  };
954
+ if (config.nmsIouThreshold !== void 0) result["nmsIouThreshold"] = config.nmsIouThreshold;
955
+ if (config.inputNormalization !== void 0) result["inputNormalization"] = config.inputNormalization;
956
+ if (config.outputProbabilities !== void 0) result["outputProbabilities"] = config.outputProbabilities;
954
957
  if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
955
958
  if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
956
959
  if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
@@ -1258,6 +1261,55 @@ var RUNTIME_TO_FORMAT = {
1258
1261
  onnxruntime: "onnx",
1259
1262
  edgetpu: "tflite"
1260
1263
  };
1264
+ /** The step whose detections are re-filtered downstream by the executor's
1265
+ * per-macro `minConfidence*` sliders (`matchesMacroFilter`). Its pool-side
1266
+ * floor must stay BELOW those sliders or it silently pre-empts them. */
1267
+ var OBJECT_DETECTION_STEP_ID = "object-detection";
1268
+ /**
1269
+ * Pool-side confidence FLOOR for the object-detection step (#4 foot-gun fix).
1270
+ *
1271
+ * The pool used to receive `def.defaultConfidence` (0.5) as its floor and drop
1272
+ * every box < 0.5 BEFORE the executor's per-macro `minConfidence*` sliders run —
1273
+ * so a slider set below 0.5 was a silent no-op. Lower the pool floor to a small
1274
+ * constant so the operator-visible per-macro sliders are the authoritative
1275
+ * filter across their full 0–1 range. The per-macro slider DEFAULTS stay at 0.5
1276
+ * (see `step-definitions.ts` getConfigSchema), so default UI behaviour is
1277
+ * unchanged; only sub-0.5 sliders start having an effect. Non-object-detection
1278
+ * steps keep their own `def.defaultConfidence` floor (no per-macro re-filter). */
1279
+ var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
1280
+ /**
1281
+ * Resolve the pool-side confidence FLOOR for a step (#4). The object-detection
1282
+ * step gets the small {@link OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR} so its
1283
+ * per-macro `minConfidence*` sliders (applied downstream by the executor) are
1284
+ * the authoritative filter; every other step keeps its own `defaultConfidence`.
1285
+ */
1286
+ function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
1287
+ return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
1288
+ }
1289
+ /**
1290
+ * Load a model's runtime label list from its `-labels.json` extraFile, if any.
1291
+ *
1292
+ * Accepts BOTH on-disk shapes:
1293
+ * - a flat `string[]` (legacy label files), and
1294
+ * - a `{ "labels": string[], ... }` metadata object (the newer format the
1295
+ * animal / bird / vehicle classifiers ship, which also carries preprocess /
1296
+ * license metadata alongside the label array).
1297
+ *
1298
+ * Returns `undefined` when the model declares no `-labels.json`, the file is
1299
+ * absent, or its contents are not a recognised label shape — the caller then
1300
+ * falls back to the step's `def.labels`.
1301
+ */
1302
+ var LabelsFileSchema = require_dist.union([require_dist.array(require_dist.string()).readonly(), require_dist.object({ labels: require_dist.array(require_dist.string()).readonly() })]);
1303
+ function readModelLabels(modelsDir, modelEntry) {
1304
+ const labelsFile = modelEntry.extraFiles?.find((f) => f.filename.endsWith("-labels.json"));
1305
+ if (!labelsFile) return void 0;
1306
+ const labelsPath = node_path.join(modelsDir, labelsFile.filename);
1307
+ if (!node_fs.existsSync(labelsPath)) return void 0;
1308
+ const parsed = LabelsFileSchema.safeParse(JSON.parse(node_fs.readFileSync(labelsPath, "utf8")));
1309
+ if (!parsed.success) return void 0;
1310
+ const data = parsed.data;
1311
+ return "labels" in data ? data.labels : data;
1312
+ }
1261
1313
  var EngineFactory = class {
1262
1314
  pool = null;
1263
1315
  poolManager = null;
@@ -1425,14 +1477,7 @@ var EngineFactory = class {
1425
1477
  const inputHeight = modelEntry.inputSize.height;
1426
1478
  const inputSize = Math.max(inputWidth, inputHeight);
1427
1479
  const inputChannels = modelEntry.inputChannels ?? 3;
1428
- let labels = def.labels;
1429
- if (!labels && modelEntry.extraFiles) {
1430
- const labelsFile = modelEntry.extraFiles.find((f) => f.filename.endsWith("-labels.json"));
1431
- if (labelsFile) {
1432
- const labelsPath = node_path.join(this.opts.modelsDir, labelsFile.filename);
1433
- if (node_fs.existsSync(labelsPath)) labels = JSON.parse(node_fs.readFileSync(labelsPath, "utf8"));
1434
- }
1435
- }
1480
+ const labels = readModelLabels(this.opts.modelsDir, modelEntry) ?? def.labels;
1436
1481
  return {
1437
1482
  path: modelPath,
1438
1483
  inputSize,
@@ -1440,8 +1485,11 @@ var EngineFactory = class {
1440
1485
  inputHeight,
1441
1486
  inputChannels,
1442
1487
  preprocessMode: modelEntry.preprocessMode ?? "letterbox",
1488
+ inputNormalization: modelEntry.inputNormalization,
1489
+ outputProbabilities: modelEntry.outputProbabilities,
1443
1490
  postprocessor: modelEntry.postprocessor ?? def.postprocessor,
1444
- confidence: def.defaultConfidence,
1491
+ confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
1492
+ nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
1445
1493
  labels,
1446
1494
  charset: def.charset,
1447
1495
  numClasses: labels?.length,
@@ -1560,6 +1608,9 @@ var EngineProvisioner = class {
1560
1608
  };
1561
1609
  //#endregion
1562
1610
  //#region src/detection-pipeline/postprocess/dispatch.ts
1611
+ /** Detector NMS IoU fallback — the historical hardcoded literal, used when the
1612
+ * step carries no `nmsIouThreshold` setting (unset == today). */
1613
+ var DEFAULT_NMS_IOU = .45;
1563
1614
  var VALID_KINDS = new Set([
1564
1615
  "detections",
1565
1616
  "classifications",
@@ -1587,7 +1638,7 @@ function tryStructured(output) {
1587
1638
  if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
1588
1639
  return output.structured;
1589
1640
  }
1590
- function postprocessYolo(output, stepDef) {
1641
+ function postprocessYolo(output, stepDef, nmsIouThreshold) {
1591
1642
  const structured = tryStructured(output);
1592
1643
  if (structured) return structured;
1593
1644
  const tensor = output.tensor;
@@ -1636,7 +1687,7 @@ function postprocessYolo(output, stepDef) {
1636
1687
  }
1637
1688
  return {
1638
1689
  kind: "detections",
1639
- detections: simpleNms(dets, .45)
1690
+ detections: simpleNms(dets, nmsIouThreshold)
1640
1691
  };
1641
1692
  }
1642
1693
  function postprocessSsd(output, _stepDef) {
@@ -1644,7 +1695,7 @@ function postprocessSsd(output, _stepDef) {
1644
1695
  if (structured) return structured;
1645
1696
  throw new Error("SSD postprocessing runs in the Python inference pool (ssd.py); no TypeScript raw-tensor path.");
1646
1697
  }
1647
- function postprocessScrfd(output, stepDef) {
1698
+ function postprocessScrfd(output, stepDef, nmsIouThreshold) {
1648
1699
  const structured = tryStructured(output);
1649
1700
  if (structured) return structured;
1650
1701
  if (!output.tensors) throw new Error("SCRFD postprocessor: no tensors in engine output");
@@ -1722,7 +1773,7 @@ function postprocessScrfd(output, stepDef) {
1722
1773
  score: c.score,
1723
1774
  bbox: c.bbox,
1724
1775
  ...c.landmarks ? { landmarks: c.landmarks } : {}
1725
- })), .45)
1776
+ })), nmsIouThreshold)
1726
1777
  };
1727
1778
  }
1728
1779
  /** Match ONNX output tensors to SCRFD strides by expected anchor count. */
@@ -1927,7 +1978,7 @@ function postprocessSaliency(output, _stepDef) {
1927
1978
  maskHeight: side
1928
1979
  };
1929
1980
  }
1930
- function postprocessYoloSeg(output, stepDef) {
1981
+ function postprocessYoloSeg(output, stepDef, nmsIouThreshold) {
1931
1982
  const structured = tryStructured(output);
1932
1983
  if (structured) return structured;
1933
1984
  const tensors = output.tensors;
@@ -2052,7 +2103,7 @@ function postprocessYoloSeg(output, stepDef) {
2052
2103
  class: d.class,
2053
2104
  score: d.score,
2054
2105
  bbox: d.bbox
2055
- })), .45);
2106
+ })), nmsIouThreshold);
2056
2107
  const keptSet = new Set(keptBoxes.map((d) => `${d.bbox[0]}_${d.bbox[1]}_${d.bbox[2]}_${d.bbox[3]}_${d.score}`));
2057
2108
  return {
2058
2109
  kind: "detections",
@@ -2085,10 +2136,11 @@ var DISPATCH = {
2085
2136
  * Both paths pass through normalizeOutput() to ensure consistent format
2086
2137
  * (e.g., classification top-K, alternates).
2087
2138
  */
2088
- function dispatchPostprocess(output, stepDef) {
2139
+ function dispatchPostprocess(output, stepDef, settings) {
2089
2140
  const fn = DISPATCH[stepDef.postprocessor];
2090
2141
  if (!fn) throw new Error(`Unknown postprocessor type: "${stepDef.postprocessor}"`);
2091
- return normalizeOutput(fn(output, stepDef));
2142
+ const raw = settings?.["nmsIouThreshold"];
2143
+ return normalizeOutput(fn(output, stepDef, typeof raw === "number" && raw > 0 ? raw : DEFAULT_NMS_IOU));
2092
2144
  }
2093
2145
  /**
2094
2146
  * Normalize any StepOutput to a consistent format.
@@ -2553,7 +2605,8 @@ function applyChildOutput(parent, childStep, output, stepLatencyMs, ctx) {
2553
2605
  case "classifications": {
2554
2606
  const rawEnabled = childStep.settings?.["enabledClasses"];
2555
2607
  const enabledSet = Array.isArray(rawEnabled) && rawEnabled.length > 0 ? new Set(rawEnabled.filter((v) => typeof v === "string")) : null;
2556
- const passesClass = (c) => enabledSet === null || enabledSet.has(c.class);
2608
+ const rejectSet = childStep.definition.rejectClasses && childStep.definition.rejectClasses.length > 0 ? new Set(childStep.definition.rejectClasses.map((c) => c.toLowerCase())) : /* @__PURE__ */ new Set();
2609
+ const passesClass = (c) => (enabledSet === null || enabledSet.has(c.class)) && !rejectSet.has(c.class.toLowerCase());
2557
2610
  const minConfidence = typeof childStep.settings?.["minConfidence"] === "number" ? childStep.settings["minConfidence"] : 0;
2558
2611
  const winner = output.classifications[0];
2559
2612
  if (winner && passesClass(winner) && winner.score >= minConfidence) parent.ownLabels.push({
@@ -2889,6 +2942,26 @@ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
2889
2942
  function isEnrichmentOutput(output) {
2890
2943
  return output.kind === "classifications" || output.kind === "embedding" || output.kind === "text";
2891
2944
  }
2945
+ /**
2946
+ * Re-project a `crop-zone` root's detections from crop-relative coordinates
2947
+ * back to full-frame image space by the crop's origin — the root analogue of
2948
+ * the crop-roi child re-projection (`transformBboxToImageSpace`). Landmarks (if
2949
+ * any) are shifted by the same origin. Immutable: returns a fresh output.
2950
+ */
2951
+ function reprojectCropZoneDetections(output, cropOrigin) {
2952
+ const [ox, oy] = cropOrigin;
2953
+ return {
2954
+ kind: "detections",
2955
+ detections: output.detections.map((det) => ({
2956
+ ...det,
2957
+ bbox: transformBboxToImageSpace(det.bbox, cropOrigin),
2958
+ ...det.landmarks ? { landmarks: det.landmarks.map((l) => ({
2959
+ x: l.x + ox,
2960
+ y: l.y + oy
2961
+ })) } : {}
2962
+ }))
2963
+ };
2964
+ }
2892
2965
  var PipelineExecutor = class {
2893
2966
  opts;
2894
2967
  constructor(opts) {
@@ -2942,7 +3015,7 @@ var PipelineExecutor = class {
2942
3015
  *
2943
3016
  * @returns FrameResult + optional trace
2944
3017
  */
2945
- async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
3018
+ async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider, cropZoneBbox) {
2946
3019
  const startMs = Date.now();
2947
3020
  const verbosity = runOpts?.traceVerbosity ?? "off";
2948
3021
  const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
@@ -2962,9 +3035,11 @@ var PipelineExecutor = class {
2962
3035
  };
2963
3036
  for (const rootStep of tree.roots) {
2964
3037
  if (debug) console.log(`[executor] rootStep=${rootStep.stepId} settings=${JSON.stringify(rootStep.settings ?? {})}`);
3038
+ const cropPrep = rootStep.definition.extractMode === "crop-zone" && cropZoneBbox !== void 0 ? await this.prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
2965
3039
  const rootStart = Date.now();
2966
- const rootOutput = await this.executeStep(rootStep, rootInput, imageWidth, imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
3040
+ const rawRootOutput = await this.executeStep(rootStep, cropPrep?.input ?? rootInput, cropPrep?.width ?? imageWidth, cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
2967
3041
  const rootMs = Date.now() - rootStart;
3042
+ const rootOutput = cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
2968
3043
  if (rootOutput.kind !== "detections") {
2969
3044
  if (isEnrichmentOutput(rootOutput)) {
2970
3045
  const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
@@ -3059,6 +3134,39 @@ var PipelineExecutor = class {
3059
3134
  ]
3060
3135
  }, rootStep, idGen, rootStepLatencyMs);
3061
3136
  }
3137
+ /**
3138
+ * Crop the full frame to a `crop-zone` root's absolute-pixel zone bbox and
3139
+ * return the cropped JPEG input + the exact origin used, so detections can be
3140
+ * re-projected. Returns null (⇒ full-frame fallback) when the bbox is
3141
+ * degenerate or the crop/encode fails — a package frame is never dropped for
3142
+ * a crop failure. The origin is computed to match `cropJpeg`'s clamp+round
3143
+ * exactly so the re-projection is pixel-accurate.
3144
+ */
3145
+ async prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) {
3146
+ if (isBboxDegenerate(cropZoneBbox)) return null;
3147
+ try {
3148
+ const crop = await cropJpeg(await fullFrameJpegProvider(), cropZoneBbox, imageWidth, imageHeight);
3149
+ const ox = Math.max(0, Math.round(cropZoneBbox[0]));
3150
+ const oy = Math.max(0, Math.round(cropZoneBbox[1]));
3151
+ return {
3152
+ input: {
3153
+ kind: "jpeg",
3154
+ data: crop.jpeg
3155
+ },
3156
+ width: crop.width,
3157
+ height: crop.height,
3158
+ origin: [
3159
+ ox,
3160
+ oy,
3161
+ ox + crop.width,
3162
+ oy + crop.height
3163
+ ]
3164
+ };
3165
+ } catch (err) {
3166
+ this.throttledWarn(`crop-zone-prep:${deviceId}`, 3e4, "crop-zone frame preparation failed — running package-detection full-frame", { error: require_dist.errMsg(err) }, deviceId);
3167
+ return null;
3168
+ }
3169
+ }
3062
3170
  async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg) {
3063
3171
  const preprocessStart = Date.now();
3064
3172
  const preprocessMs = Date.now() - preprocessStart;
@@ -3101,7 +3209,7 @@ var PipelineExecutor = class {
3101
3209
  if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3102
3210
  if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3103
3211
  const postprocessStart = Date.now();
3104
- const output = dispatchPostprocess(engineOutput, step.definition);
3212
+ const output = dispatchPostprocess(engineOutput, step.definition, step.settings);
3105
3213
  const postprocessMs = Date.now() - postprocessStart;
3106
3214
  stepTimings.push({
3107
3215
  source: step.stepId,
@@ -3248,6 +3356,54 @@ function capitalize(s) {
3248
3356
  return s.charAt(0).toUpperCase() + s.slice(1);
3249
3357
  }
3250
3358
  //#endregion
3359
+ //#region src/detection-pipeline/pipeline/crop-zone.ts
3360
+ /**
3361
+ * Resolve the absolute-pixel crop bbox for a `crop-zone` root detector: the
3362
+ * bounding box of the UNION of every polygon referenced by the device's
3363
+ * ENABLED `package`-stage zone rules. Zone polygons are normalized (0..1); the
3364
+ * union is clamped to [0,1] then scaled to the frame's pixel dimensions.
3365
+ *
3366
+ * Returns `null` when there is nothing usable to crop to — no enabled rules, no
3367
+ * referenced geometry, or a degenerate result — in which case the caller falls
3368
+ * back to full-frame inference (behaviour identical to before the crop-zone
3369
+ * plumbing).
3370
+ */
3371
+ function resolvePackageCropBbox(zones, packageRules, frameWidth, frameHeight) {
3372
+ if (frameWidth <= 0 || frameHeight <= 0) return null;
3373
+ const enabledRules = packageRules.filter((r) => r.enabled !== false);
3374
+ if (enabledRules.length === 0) return null;
3375
+ const zoneIds = /* @__PURE__ */ new Set();
3376
+ for (const rule of enabledRules) for (const id of rule.zoneIds) zoneIds.add(id);
3377
+ if (zoneIds.size === 0) return null;
3378
+ const zoneById = new Map(zones.map((z) => [z.id, z]));
3379
+ let minX = Number.POSITIVE_INFINITY;
3380
+ let minY = Number.POSITIVE_INFINITY;
3381
+ let maxX = Number.NEGATIVE_INFINITY;
3382
+ let maxY = Number.NEGATIVE_INFINITY;
3383
+ let sawPoint = false;
3384
+ for (const id of zoneIds) {
3385
+ const zone = zoneById.get(id);
3386
+ if (!zone || zone.polygon.length === 0) continue;
3387
+ for (const point of zone.polygon) {
3388
+ sawPoint = true;
3389
+ if (point.x < minX) minX = point.x;
3390
+ if (point.y < minY) minY = point.y;
3391
+ if (point.x > maxX) maxX = point.x;
3392
+ if (point.y > maxY) maxY = point.y;
3393
+ }
3394
+ }
3395
+ if (!sawPoint) return null;
3396
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
3397
+ const bbox = [
3398
+ clamp01(minX) * frameWidth,
3399
+ clamp01(minY) * frameHeight,
3400
+ clamp01(maxX) * frameWidth,
3401
+ clamp01(maxY) * frameHeight
3402
+ ];
3403
+ if (isBboxDegenerate(bbox)) return null;
3404
+ return bbox;
3405
+ }
3406
+ //#endregion
3251
3407
  //#region src/detection-pipeline/pipeline/tree-builder.ts
3252
3408
  /**
3253
3409
  * Build an executable tree from user config.
@@ -4985,10 +5141,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4985
5141
  const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
4986
5142
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
4987
5143
  const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
5144
+ let cropZoneBbox;
5145
+ if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
5146
+ await this.ensureDeviceProxy(effectiveDeviceId);
5147
+ const proxy = this.deviceProxies.get(effectiveDeviceId);
5148
+ if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
5149
+ }
4988
5150
  const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
4989
5151
  traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
4990
5152
  plane: input.plane
4991
- }, nativeFaceCropProvider);
5153
+ }, nativeFaceCropProvider, cropZoneBbox);
4992
5154
  if (isRuntime) {
4993
5155
  if (trace && this.eventBus) this.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineTrace, {
4994
5156
  type: "device",