@camstack/addon-pipeline 1.1.64 → 1.1.65

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 +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +157 -17
  4. package/dist/detection-pipeline/index.mjs +157 -17
  5. package/dist/{dist-BzVZ2I4P.mjs → dist-D6tHM9GA.mjs} +0 -55
  6. package/dist/{dist-B2jTt7Lq.js → dist-WW3gH8hF.js} +0 -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 +2 -2
  10. package/dist/pipeline-runner/index.mjs +2 -2
  11. package/dist/recorder/index.js +1 -1
  12. package/dist/recorder/index.mjs +1 -1
  13. package/dist/{step-definitions-DXkY5Zor.js → step-definitions-CXZpwwT0.js} +12 -1
  14. package/dist/{step-definitions-B07M9qhj.mjs → step-definitions-Dbsm64_I.mjs} +12 -1
  15. package/dist/stream-broker/_stub.js +2 -2
  16. 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-CbU_HuOC.mjs} +2 -2
  17. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bAh96WNb.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CxZvj8X1.mjs} +1 -1
  18. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BzNJyeNE.mjs +26 -0
  19. package/dist/stream-broker/{hostInit-BY2oQAEX.mjs → hostInit-QaDEOY0Z.mjs} +2 -2
  20. package/dist/stream-broker/index.js +51 -3
  21. package/dist/stream-broker/index.mjs +51 -3
  22. package/dist/stream-broker/remoteEntry.js +1 -1
  23. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C03fx7Sp.js → MaskShapeCanvas-DI4BY7W2-C7H0kVAH.js} +1 -1
  24. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BTxzn1rQ.js → MotionZonesSettings-NcxxQN8r-CHfEFlEF.js} +1 -1
  25. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-GKO8O7rC.js → PrivacyMaskSettings-APgPLF7p-Bc-trVxX.js} +1 -1
  26. package/embed-dist/assets/{index-QdssILAA.js → index-eZGm_itM.js} +9 -9
  27. package/embed-dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/python/postprocessors/scrfd.py +4 -1
  30. package/python/postprocessors/test_yolo_nms.py +45 -0
  31. package/python/postprocessors/yolo.py +4 -1
  32. package/python/postprocessors/yolo_seg.py +4 -1
  33. 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
@@ -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-WW3gH8hF.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-D6tHM9GA.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-WW3gH8hF.js");
7
+ const require_step_definitions = require("../step-definitions-CXZpwwT0.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,7 @@ 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;
954
955
  if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
955
956
  if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
956
957
  if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
@@ -1258,6 +1259,31 @@ var RUNTIME_TO_FORMAT = {
1258
1259
  onnxruntime: "onnx",
1259
1260
  edgetpu: "tflite"
1260
1261
  };
1262
+ /** The step whose detections are re-filtered downstream by the executor's
1263
+ * per-macro `minConfidence*` sliders (`matchesMacroFilter`). Its pool-side
1264
+ * floor must stay BELOW those sliders or it silently pre-empts them. */
1265
+ var OBJECT_DETECTION_STEP_ID = "object-detection";
1266
+ /**
1267
+ * Pool-side confidence FLOOR for the object-detection step (#4 foot-gun fix).
1268
+ *
1269
+ * The pool used to receive `def.defaultConfidence` (0.5) as its floor and drop
1270
+ * every box < 0.5 BEFORE the executor's per-macro `minConfidence*` sliders run —
1271
+ * so a slider set below 0.5 was a silent no-op. Lower the pool floor to a small
1272
+ * constant so the operator-visible per-macro sliders are the authoritative
1273
+ * filter across their full 0–1 range. The per-macro slider DEFAULTS stay at 0.5
1274
+ * (see `step-definitions.ts` getConfigSchema), so default UI behaviour is
1275
+ * unchanged; only sub-0.5 sliders start having an effect. Non-object-detection
1276
+ * steps keep their own `def.defaultConfidence` floor (no per-macro re-filter). */
1277
+ var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
1278
+ /**
1279
+ * Resolve the pool-side confidence FLOOR for a step (#4). The object-detection
1280
+ * step gets the small {@link OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR} so its
1281
+ * per-macro `minConfidence*` sliders (applied downstream by the executor) are
1282
+ * the authoritative filter; every other step keeps its own `defaultConfidence`.
1283
+ */
1284
+ function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
1285
+ return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
1286
+ }
1261
1287
  var EngineFactory = class {
1262
1288
  pool = null;
1263
1289
  poolManager = null;
@@ -1441,7 +1467,8 @@ var EngineFactory = class {
1441
1467
  inputChannels,
1442
1468
  preprocessMode: modelEntry.preprocessMode ?? "letterbox",
1443
1469
  postprocessor: modelEntry.postprocessor ?? def.postprocessor,
1444
- confidence: def.defaultConfidence,
1470
+ confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
1471
+ nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
1445
1472
  labels,
1446
1473
  charset: def.charset,
1447
1474
  numClasses: labels?.length,
@@ -1560,6 +1587,9 @@ var EngineProvisioner = class {
1560
1587
  };
1561
1588
  //#endregion
1562
1589
  //#region src/detection-pipeline/postprocess/dispatch.ts
1590
+ /** Detector NMS IoU fallback — the historical hardcoded literal, used when the
1591
+ * step carries no `nmsIouThreshold` setting (unset == today). */
1592
+ var DEFAULT_NMS_IOU = .45;
1563
1593
  var VALID_KINDS = new Set([
1564
1594
  "detections",
1565
1595
  "classifications",
@@ -1587,7 +1617,7 @@ function tryStructured(output) {
1587
1617
  if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
1588
1618
  return output.structured;
1589
1619
  }
1590
- function postprocessYolo(output, stepDef) {
1620
+ function postprocessYolo(output, stepDef, nmsIouThreshold) {
1591
1621
  const structured = tryStructured(output);
1592
1622
  if (structured) return structured;
1593
1623
  const tensor = output.tensor;
@@ -1636,7 +1666,7 @@ function postprocessYolo(output, stepDef) {
1636
1666
  }
1637
1667
  return {
1638
1668
  kind: "detections",
1639
- detections: simpleNms(dets, .45)
1669
+ detections: simpleNms(dets, nmsIouThreshold)
1640
1670
  };
1641
1671
  }
1642
1672
  function postprocessSsd(output, _stepDef) {
@@ -1644,7 +1674,7 @@ function postprocessSsd(output, _stepDef) {
1644
1674
  if (structured) return structured;
1645
1675
  throw new Error("SSD postprocessing runs in the Python inference pool (ssd.py); no TypeScript raw-tensor path.");
1646
1676
  }
1647
- function postprocessScrfd(output, stepDef) {
1677
+ function postprocessScrfd(output, stepDef, nmsIouThreshold) {
1648
1678
  const structured = tryStructured(output);
1649
1679
  if (structured) return structured;
1650
1680
  if (!output.tensors) throw new Error("SCRFD postprocessor: no tensors in engine output");
@@ -1722,7 +1752,7 @@ function postprocessScrfd(output, stepDef) {
1722
1752
  score: c.score,
1723
1753
  bbox: c.bbox,
1724
1754
  ...c.landmarks ? { landmarks: c.landmarks } : {}
1725
- })), .45)
1755
+ })), nmsIouThreshold)
1726
1756
  };
1727
1757
  }
1728
1758
  /** Match ONNX output tensors to SCRFD strides by expected anchor count. */
@@ -1927,7 +1957,7 @@ function postprocessSaliency(output, _stepDef) {
1927
1957
  maskHeight: side
1928
1958
  };
1929
1959
  }
1930
- function postprocessYoloSeg(output, stepDef) {
1960
+ function postprocessYoloSeg(output, stepDef, nmsIouThreshold) {
1931
1961
  const structured = tryStructured(output);
1932
1962
  if (structured) return structured;
1933
1963
  const tensors = output.tensors;
@@ -2052,7 +2082,7 @@ function postprocessYoloSeg(output, stepDef) {
2052
2082
  class: d.class,
2053
2083
  score: d.score,
2054
2084
  bbox: d.bbox
2055
- })), .45);
2085
+ })), nmsIouThreshold);
2056
2086
  const keptSet = new Set(keptBoxes.map((d) => `${d.bbox[0]}_${d.bbox[1]}_${d.bbox[2]}_${d.bbox[3]}_${d.score}`));
2057
2087
  return {
2058
2088
  kind: "detections",
@@ -2085,10 +2115,11 @@ var DISPATCH = {
2085
2115
  * Both paths pass through normalizeOutput() to ensure consistent format
2086
2116
  * (e.g., classification top-K, alternates).
2087
2117
  */
2088
- function dispatchPostprocess(output, stepDef) {
2118
+ function dispatchPostprocess(output, stepDef, settings) {
2089
2119
  const fn = DISPATCH[stepDef.postprocessor];
2090
2120
  if (!fn) throw new Error(`Unknown postprocessor type: "${stepDef.postprocessor}"`);
2091
- return normalizeOutput(fn(output, stepDef));
2121
+ const raw = settings?.["nmsIouThreshold"];
2122
+ return normalizeOutput(fn(output, stepDef, typeof raw === "number" && raw > 0 ? raw : DEFAULT_NMS_IOU));
2092
2123
  }
2093
2124
  /**
2094
2125
  * Normalize any StepOutput to a consistent format.
@@ -2889,6 +2920,26 @@ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
2889
2920
  function isEnrichmentOutput(output) {
2890
2921
  return output.kind === "classifications" || output.kind === "embedding" || output.kind === "text";
2891
2922
  }
2923
+ /**
2924
+ * Re-project a `crop-zone` root's detections from crop-relative coordinates
2925
+ * back to full-frame image space by the crop's origin — the root analogue of
2926
+ * the crop-roi child re-projection (`transformBboxToImageSpace`). Landmarks (if
2927
+ * any) are shifted by the same origin. Immutable: returns a fresh output.
2928
+ */
2929
+ function reprojectCropZoneDetections(output, cropOrigin) {
2930
+ const [ox, oy] = cropOrigin;
2931
+ return {
2932
+ kind: "detections",
2933
+ detections: output.detections.map((det) => ({
2934
+ ...det,
2935
+ bbox: transformBboxToImageSpace(det.bbox, cropOrigin),
2936
+ ...det.landmarks ? { landmarks: det.landmarks.map((l) => ({
2937
+ x: l.x + ox,
2938
+ y: l.y + oy
2939
+ })) } : {}
2940
+ }))
2941
+ };
2942
+ }
2892
2943
  var PipelineExecutor = class {
2893
2944
  opts;
2894
2945
  constructor(opts) {
@@ -2942,7 +2993,7 @@ var PipelineExecutor = class {
2942
2993
  *
2943
2994
  * @returns FrameResult + optional trace
2944
2995
  */
2945
- async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
2996
+ async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider, cropZoneBbox) {
2946
2997
  const startMs = Date.now();
2947
2998
  const verbosity = runOpts?.traceVerbosity ?? "off";
2948
2999
  const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
@@ -2962,9 +3013,11 @@ var PipelineExecutor = class {
2962
3013
  };
2963
3014
  for (const rootStep of tree.roots) {
2964
3015
  if (debug) console.log(`[executor] rootStep=${rootStep.stepId} settings=${JSON.stringify(rootStep.settings ?? {})}`);
3016
+ const cropPrep = rootStep.definition.extractMode === "crop-zone" && cropZoneBbox !== void 0 ? await this.prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
2965
3017
  const rootStart = Date.now();
2966
- const rootOutput = await this.executeStep(rootStep, rootInput, imageWidth, imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
3018
+ 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
3019
  const rootMs = Date.now() - rootStart;
3020
+ const rootOutput = cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
2968
3021
  if (rootOutput.kind !== "detections") {
2969
3022
  if (isEnrichmentOutput(rootOutput)) {
2970
3023
  const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
@@ -3059,6 +3112,39 @@ var PipelineExecutor = class {
3059
3112
  ]
3060
3113
  }, rootStep, idGen, rootStepLatencyMs);
3061
3114
  }
3115
+ /**
3116
+ * Crop the full frame to a `crop-zone` root's absolute-pixel zone bbox and
3117
+ * return the cropped JPEG input + the exact origin used, so detections can be
3118
+ * re-projected. Returns null (⇒ full-frame fallback) when the bbox is
3119
+ * degenerate or the crop/encode fails — a package frame is never dropped for
3120
+ * a crop failure. The origin is computed to match `cropJpeg`'s clamp+round
3121
+ * exactly so the re-projection is pixel-accurate.
3122
+ */
3123
+ async prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) {
3124
+ if (isBboxDegenerate(cropZoneBbox)) return null;
3125
+ try {
3126
+ const crop = await cropJpeg(await fullFrameJpegProvider(), cropZoneBbox, imageWidth, imageHeight);
3127
+ const ox = Math.max(0, Math.round(cropZoneBbox[0]));
3128
+ const oy = Math.max(0, Math.round(cropZoneBbox[1]));
3129
+ return {
3130
+ input: {
3131
+ kind: "jpeg",
3132
+ data: crop.jpeg
3133
+ },
3134
+ width: crop.width,
3135
+ height: crop.height,
3136
+ origin: [
3137
+ ox,
3138
+ oy,
3139
+ ox + crop.width,
3140
+ oy + crop.height
3141
+ ]
3142
+ };
3143
+ } catch (err) {
3144
+ this.throttledWarn(`crop-zone-prep:${deviceId}`, 3e4, "crop-zone frame preparation failed — running package-detection full-frame", { error: require_dist.errMsg(err) }, deviceId);
3145
+ return null;
3146
+ }
3147
+ }
3062
3148
  async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg) {
3063
3149
  const preprocessStart = Date.now();
3064
3150
  const preprocessMs = Date.now() - preprocessStart;
@@ -3101,7 +3187,7 @@ var PipelineExecutor = class {
3101
3187
  if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3102
3188
  if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3103
3189
  const postprocessStart = Date.now();
3104
- const output = dispatchPostprocess(engineOutput, step.definition);
3190
+ const output = dispatchPostprocess(engineOutput, step.definition, step.settings);
3105
3191
  const postprocessMs = Date.now() - postprocessStart;
3106
3192
  stepTimings.push({
3107
3193
  source: step.stepId,
@@ -3248,6 +3334,54 @@ function capitalize(s) {
3248
3334
  return s.charAt(0).toUpperCase() + s.slice(1);
3249
3335
  }
3250
3336
  //#endregion
3337
+ //#region src/detection-pipeline/pipeline/crop-zone.ts
3338
+ /**
3339
+ * Resolve the absolute-pixel crop bbox for a `crop-zone` root detector: the
3340
+ * bounding box of the UNION of every polygon referenced by the device's
3341
+ * ENABLED `package`-stage zone rules. Zone polygons are normalized (0..1); the
3342
+ * union is clamped to [0,1] then scaled to the frame's pixel dimensions.
3343
+ *
3344
+ * Returns `null` when there is nothing usable to crop to — no enabled rules, no
3345
+ * referenced geometry, or a degenerate result — in which case the caller falls
3346
+ * back to full-frame inference (behaviour identical to before the crop-zone
3347
+ * plumbing).
3348
+ */
3349
+ function resolvePackageCropBbox(zones, packageRules, frameWidth, frameHeight) {
3350
+ if (frameWidth <= 0 || frameHeight <= 0) return null;
3351
+ const enabledRules = packageRules.filter((r) => r.enabled !== false);
3352
+ if (enabledRules.length === 0) return null;
3353
+ const zoneIds = /* @__PURE__ */ new Set();
3354
+ for (const rule of enabledRules) for (const id of rule.zoneIds) zoneIds.add(id);
3355
+ if (zoneIds.size === 0) return null;
3356
+ const zoneById = new Map(zones.map((z) => [z.id, z]));
3357
+ let minX = Number.POSITIVE_INFINITY;
3358
+ let minY = Number.POSITIVE_INFINITY;
3359
+ let maxX = Number.NEGATIVE_INFINITY;
3360
+ let maxY = Number.NEGATIVE_INFINITY;
3361
+ let sawPoint = false;
3362
+ for (const id of zoneIds) {
3363
+ const zone = zoneById.get(id);
3364
+ if (!zone || zone.polygon.length === 0) continue;
3365
+ for (const point of zone.polygon) {
3366
+ sawPoint = true;
3367
+ if (point.x < minX) minX = point.x;
3368
+ if (point.y < minY) minY = point.y;
3369
+ if (point.x > maxX) maxX = point.x;
3370
+ if (point.y > maxY) maxY = point.y;
3371
+ }
3372
+ }
3373
+ if (!sawPoint) return null;
3374
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
3375
+ const bbox = [
3376
+ clamp01(minX) * frameWidth,
3377
+ clamp01(minY) * frameHeight,
3378
+ clamp01(maxX) * frameWidth,
3379
+ clamp01(maxY) * frameHeight
3380
+ ];
3381
+ if (isBboxDegenerate(bbox)) return null;
3382
+ return bbox;
3383
+ }
3384
+ //#endregion
3251
3385
  //#region src/detection-pipeline/pipeline/tree-builder.ts
3252
3386
  /**
3253
3387
  * Build an executable tree from user config.
@@ -4985,10 +5119,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4985
5119
  const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
4986
5120
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
4987
5121
  const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
5122
+ let cropZoneBbox;
5123
+ if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
5124
+ await this.ensureDeviceProxy(effectiveDeviceId);
5125
+ const proxy = this.deviceProxies.get(effectiveDeviceId);
5126
+ if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
5127
+ }
4988
5128
  const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
4989
5129
  traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
4990
5130
  plane: input.plane
4991
- }, nativeFaceCropProvider);
5131
+ }, nativeFaceCropProvider, cropZoneBbox);
4992
5132
  if (isRuntime) {
4993
5133
  if (trace && this.eventBus) this.eventBus.emit(require_dist.createEvent(require_dist.EventCategory.PipelineTrace, {
4994
5134
  type: "device",
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, m as YAMNET_TO_MACRO, t as APPLE_SA_TO_MACRO } from "../dist-BzVZ2I4P.mjs";
3
- import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-B07M9qhj.mjs";
2
+ import { $ as sleep, A as pipelineExecutorCapability, B as errMsg, C as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, S as detectionPipelineCapability, V as BaseAddon, X as parseJsonUnknown, b as defaultDeviceFor$1, dt as EventCategory, k as nodePin, m as YAMNET_TO_MACRO, t as APPLE_SA_TO_MACRO } from "../dist-D6tHM9GA.mjs";
3
+ import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-Dbsm64_I.mjs";
4
4
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
5
5
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
6
6
  import { spawn } from "node:child_process";
@@ -308,7 +308,7 @@ function projectNodeEngine(store, nodeId) {
308
308
  //#endregion
309
309
  //#region src/detection-pipeline/default-detection-model.ts
310
310
  /** The object-detection step id — the only slot this resolver applies to. */
311
- var OBJECT_DETECTION_STEP_ID = "object-detection";
311
+ var OBJECT_DETECTION_STEP_ID$1 = "object-detection";
312
312
  /**
313
313
  * Balanced default object-detection model per accelerator class (measured on
314
314
  * COCO val2017). `'cpu'` maps to `null` → the caller substitutes the step's
@@ -368,7 +368,7 @@ function classifyAccelerator(backend, device) {
368
368
  function resolveDefaultDetectionModel(backend, device, format, getStepDef = getStepDefinition) {
369
369
  let def;
370
370
  try {
371
- def = getStepDef(OBJECT_DETECTION_STEP_ID);
371
+ def = getStepDef(OBJECT_DETECTION_STEP_ID$1);
372
372
  } catch {
373
373
  return "yolo26n";
374
374
  }
@@ -943,6 +943,7 @@ function serializeModelConfig(config) {
943
943
  postprocessor: config.postprocessor,
944
944
  confidence: config.confidence
945
945
  };
946
+ if (config.nmsIouThreshold !== void 0) result["nmsIouThreshold"] = config.nmsIouThreshold;
946
947
  if (config.inputWidth !== void 0) result["inputWidth"] = config.inputWidth;
947
948
  if (config.inputHeight !== void 0) result["inputHeight"] = config.inputHeight;
948
949
  if (config.inputChannels !== void 0) result["inputChannels"] = config.inputChannels;
@@ -1250,6 +1251,31 @@ var RUNTIME_TO_FORMAT = {
1250
1251
  onnxruntime: "onnx",
1251
1252
  edgetpu: "tflite"
1252
1253
  };
1254
+ /** The step whose detections are re-filtered downstream by the executor's
1255
+ * per-macro `minConfidence*` sliders (`matchesMacroFilter`). Its pool-side
1256
+ * floor must stay BELOW those sliders or it silently pre-empts them. */
1257
+ var OBJECT_DETECTION_STEP_ID = "object-detection";
1258
+ /**
1259
+ * Pool-side confidence FLOOR for the object-detection step (#4 foot-gun fix).
1260
+ *
1261
+ * The pool used to receive `def.defaultConfidence` (0.5) as its floor and drop
1262
+ * every box < 0.5 BEFORE the executor's per-macro `minConfidence*` sliders run —
1263
+ * so a slider set below 0.5 was a silent no-op. Lower the pool floor to a small
1264
+ * constant so the operator-visible per-macro sliders are the authoritative
1265
+ * filter across their full 0–1 range. The per-macro slider DEFAULTS stay at 0.5
1266
+ * (see `step-definitions.ts` getConfigSchema), so default UI behaviour is
1267
+ * unchanged; only sub-0.5 sliders start having an effect. Non-object-detection
1268
+ * steps keep their own `def.defaultConfidence` floor (no per-macro re-filter). */
1269
+ var OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR = .05;
1270
+ /**
1271
+ * Resolve the pool-side confidence FLOOR for a step (#4). The object-detection
1272
+ * step gets the small {@link OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR} so its
1273
+ * per-macro `minConfidence*` sliders (applied downstream by the executor) are
1274
+ * the authoritative filter; every other step keeps its own `defaultConfidence`.
1275
+ */
1276
+ function resolvePoolConfidenceFloor(stepId, defaultConfidence) {
1277
+ return stepId === OBJECT_DETECTION_STEP_ID ? OBJECT_DETECTION_POOL_CONFIDENCE_FLOOR : defaultConfidence;
1278
+ }
1253
1279
  var EngineFactory = class {
1254
1280
  pool = null;
1255
1281
  poolManager = null;
@@ -1433,7 +1459,8 @@ var EngineFactory = class {
1433
1459
  inputChannels,
1434
1460
  preprocessMode: modelEntry.preprocessMode ?? "letterbox",
1435
1461
  postprocessor: modelEntry.postprocessor ?? def.postprocessor,
1436
- confidence: def.defaultConfidence,
1462
+ confidence: resolvePoolConfidenceFloor(stepId, def.defaultConfidence),
1463
+ nmsIouThreshold: this.opts.nmsIouThreshold ?? .45,
1437
1464
  labels,
1438
1465
  charset: def.charset,
1439
1466
  numClasses: labels?.length,
@@ -1552,6 +1579,9 @@ var EngineProvisioner = class {
1552
1579
  };
1553
1580
  //#endregion
1554
1581
  //#region src/detection-pipeline/postprocess/dispatch.ts
1582
+ /** Detector NMS IoU fallback — the historical hardcoded literal, used when the
1583
+ * step carries no `nmsIouThreshold` setting (unset == today). */
1584
+ var DEFAULT_NMS_IOU = .45;
1555
1585
  var VALID_KINDS = new Set([
1556
1586
  "detections",
1557
1587
  "classifications",
@@ -1579,7 +1609,7 @@ function tryStructured(output) {
1579
1609
  if (!isStepOutput(output.structured)) throw new Error(`Python pool returned structured output with unexpected kind: ${JSON.stringify(output.structured["kind"])}`);
1580
1610
  return output.structured;
1581
1611
  }
1582
- function postprocessYolo(output, stepDef) {
1612
+ function postprocessYolo(output, stepDef, nmsIouThreshold) {
1583
1613
  const structured = tryStructured(output);
1584
1614
  if (structured) return structured;
1585
1615
  const tensor = output.tensor;
@@ -1628,7 +1658,7 @@ function postprocessYolo(output, stepDef) {
1628
1658
  }
1629
1659
  return {
1630
1660
  kind: "detections",
1631
- detections: simpleNms(dets, .45)
1661
+ detections: simpleNms(dets, nmsIouThreshold)
1632
1662
  };
1633
1663
  }
1634
1664
  function postprocessSsd(output, _stepDef) {
@@ -1636,7 +1666,7 @@ function postprocessSsd(output, _stepDef) {
1636
1666
  if (structured) return structured;
1637
1667
  throw new Error("SSD postprocessing runs in the Python inference pool (ssd.py); no TypeScript raw-tensor path.");
1638
1668
  }
1639
- function postprocessScrfd(output, stepDef) {
1669
+ function postprocessScrfd(output, stepDef, nmsIouThreshold) {
1640
1670
  const structured = tryStructured(output);
1641
1671
  if (structured) return structured;
1642
1672
  if (!output.tensors) throw new Error("SCRFD postprocessor: no tensors in engine output");
@@ -1714,7 +1744,7 @@ function postprocessScrfd(output, stepDef) {
1714
1744
  score: c.score,
1715
1745
  bbox: c.bbox,
1716
1746
  ...c.landmarks ? { landmarks: c.landmarks } : {}
1717
- })), .45)
1747
+ })), nmsIouThreshold)
1718
1748
  };
1719
1749
  }
1720
1750
  /** Match ONNX output tensors to SCRFD strides by expected anchor count. */
@@ -1919,7 +1949,7 @@ function postprocessSaliency(output, _stepDef) {
1919
1949
  maskHeight: side
1920
1950
  };
1921
1951
  }
1922
- function postprocessYoloSeg(output, stepDef) {
1952
+ function postprocessYoloSeg(output, stepDef, nmsIouThreshold) {
1923
1953
  const structured = tryStructured(output);
1924
1954
  if (structured) return structured;
1925
1955
  const tensors = output.tensors;
@@ -2044,7 +2074,7 @@ function postprocessYoloSeg(output, stepDef) {
2044
2074
  class: d.class,
2045
2075
  score: d.score,
2046
2076
  bbox: d.bbox
2047
- })), .45);
2077
+ })), nmsIouThreshold);
2048
2078
  const keptSet = new Set(keptBoxes.map((d) => `${d.bbox[0]}_${d.bbox[1]}_${d.bbox[2]}_${d.bbox[3]}_${d.score}`));
2049
2079
  return {
2050
2080
  kind: "detections",
@@ -2077,10 +2107,11 @@ var DISPATCH = {
2077
2107
  * Both paths pass through normalizeOutput() to ensure consistent format
2078
2108
  * (e.g., classification top-K, alternates).
2079
2109
  */
2080
- function dispatchPostprocess(output, stepDef) {
2110
+ function dispatchPostprocess(output, stepDef, settings) {
2081
2111
  const fn = DISPATCH[stepDef.postprocessor];
2082
2112
  if (!fn) throw new Error(`Unknown postprocessor type: "${stepDef.postprocessor}"`);
2083
- return normalizeOutput(fn(output, stepDef));
2113
+ const raw = settings?.["nmsIouThreshold"];
2114
+ return normalizeOutput(fn(output, stepDef, typeof raw === "number" && raw > 0 ? raw : DEFAULT_NMS_IOU));
2084
2115
  }
2085
2116
  /**
2086
2117
  * Normalize any StepOutput to a consistent format.
@@ -2881,6 +2912,26 @@ var NATIVE_FACE_CROP_MAX_WIDTH = 320;
2881
2912
  function isEnrichmentOutput(output) {
2882
2913
  return output.kind === "classifications" || output.kind === "embedding" || output.kind === "text";
2883
2914
  }
2915
+ /**
2916
+ * Re-project a `crop-zone` root's detections from crop-relative coordinates
2917
+ * back to full-frame image space by the crop's origin — the root analogue of
2918
+ * the crop-roi child re-projection (`transformBboxToImageSpace`). Landmarks (if
2919
+ * any) are shifted by the same origin. Immutable: returns a fresh output.
2920
+ */
2921
+ function reprojectCropZoneDetections(output, cropOrigin) {
2922
+ const [ox, oy] = cropOrigin;
2923
+ return {
2924
+ kind: "detections",
2925
+ detections: output.detections.map((det) => ({
2926
+ ...det,
2927
+ bbox: transformBboxToImageSpace(det.bbox, cropOrigin),
2928
+ ...det.landmarks ? { landmarks: det.landmarks.map((l) => ({
2929
+ x: l.x + ox,
2930
+ y: l.y + oy
2931
+ })) } : {}
2932
+ }))
2933
+ };
2934
+ }
2884
2935
  var PipelineExecutor = class {
2885
2936
  opts;
2886
2937
  constructor(opts) {
@@ -2934,7 +2985,7 @@ var PipelineExecutor = class {
2934
2985
  *
2935
2986
  * @returns FrameResult + optional trace
2936
2987
  */
2937
- async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider) {
2988
+ async run(tree, rootInput, fullFrameJpegProvider, imageWidth, imageHeight, deviceId, runOpts, nativeFaceCropProvider, cropZoneBbox) {
2938
2989
  const startMs = Date.now();
2939
2990
  const verbosity = runOpts?.traceVerbosity ?? "off";
2940
2991
  const traceBuilder = new ExecutionTraceBuilder(verbosity, deviceId, imageWidth, imageHeight, this.opts.engineRuntime);
@@ -2954,9 +3005,11 @@ var PipelineExecutor = class {
2954
3005
  };
2955
3006
  for (const rootStep of tree.roots) {
2956
3007
  if (debug) console.log(`[executor] rootStep=${rootStep.stepId} settings=${JSON.stringify(rootStep.settings ?? {})}`);
3008
+ const cropPrep = rootStep.definition.extractMode === "crop-zone" && cropZoneBbox !== void 0 ? await this.prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
2957
3009
  const rootStart = Date.now();
2958
- const rootOutput = await this.executeStep(rootStep, rootInput, imageWidth, imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
3010
+ const rawRootOutput = await this.executeStep(rootStep, cropPrep?.input ?? rootInput, cropPrep?.width ?? imageWidth, cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
2959
3011
  const rootMs = Date.now() - rootStart;
3012
+ const rootOutput = cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
2960
3013
  if (rootOutput.kind !== "detections") {
2961
3014
  if (isEnrichmentOutput(rootOutput)) {
2962
3015
  const mutable = this.synthesizeRootDetection(rootOutput, rootStep, idGen, rootMs, imageWidth, imageHeight);
@@ -3051,6 +3104,39 @@ var PipelineExecutor = class {
3051
3104
  ]
3052
3105
  }, rootStep, idGen, rootStepLatencyMs);
3053
3106
  }
3107
+ /**
3108
+ * Crop the full frame to a `crop-zone` root's absolute-pixel zone bbox and
3109
+ * return the cropped JPEG input + the exact origin used, so detections can be
3110
+ * re-projected. Returns null (⇒ full-frame fallback) when the bbox is
3111
+ * degenerate or the crop/encode fails — a package frame is never dropped for
3112
+ * a crop failure. The origin is computed to match `cropJpeg`'s clamp+round
3113
+ * exactly so the re-projection is pixel-accurate.
3114
+ */
3115
+ async prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) {
3116
+ if (isBboxDegenerate(cropZoneBbox)) return null;
3117
+ try {
3118
+ const crop = await cropJpeg(await fullFrameJpegProvider(), cropZoneBbox, imageWidth, imageHeight);
3119
+ const ox = Math.max(0, Math.round(cropZoneBbox[0]));
3120
+ const oy = Math.max(0, Math.round(cropZoneBbox[1]));
3121
+ return {
3122
+ input: {
3123
+ kind: "jpeg",
3124
+ data: crop.jpeg
3125
+ },
3126
+ width: crop.width,
3127
+ height: crop.height,
3128
+ origin: [
3129
+ ox,
3130
+ oy,
3131
+ ox + crop.width,
3132
+ oy + crop.height
3133
+ ]
3134
+ };
3135
+ } catch (err) {
3136
+ this.throttledWarn(`crop-zone-prep:${deviceId}`, 3e4, "crop-zone frame preparation failed — running package-detection full-frame", { error: errMsg(err) }, deviceId);
3137
+ return null;
3138
+ }
3139
+ }
3054
3140
  async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg) {
3055
3141
  const preprocessStart = Date.now();
3056
3142
  const preprocessMs = Date.now() - preprocessStart;
@@ -3093,7 +3179,7 @@ var PipelineExecutor = class {
3093
3179
  if (typeof structured.predictMs === "number") poolAgg.predictMs += structured.predictMs;
3094
3180
  if (typeof structured.batchSize === "number" && structured.batchSize > poolAgg.batchSize) poolAgg.batchSize = structured.batchSize;
3095
3181
  const postprocessStart = Date.now();
3096
- const output = dispatchPostprocess(engineOutput, step.definition);
3182
+ const output = dispatchPostprocess(engineOutput, step.definition, step.settings);
3097
3183
  const postprocessMs = Date.now() - postprocessStart;
3098
3184
  stepTimings.push({
3099
3185
  source: step.stepId,
@@ -3240,6 +3326,54 @@ function capitalize(s) {
3240
3326
  return s.charAt(0).toUpperCase() + s.slice(1);
3241
3327
  }
3242
3328
  //#endregion
3329
+ //#region src/detection-pipeline/pipeline/crop-zone.ts
3330
+ /**
3331
+ * Resolve the absolute-pixel crop bbox for a `crop-zone` root detector: the
3332
+ * bounding box of the UNION of every polygon referenced by the device's
3333
+ * ENABLED `package`-stage zone rules. Zone polygons are normalized (0..1); the
3334
+ * union is clamped to [0,1] then scaled to the frame's pixel dimensions.
3335
+ *
3336
+ * Returns `null` when there is nothing usable to crop to — no enabled rules, no
3337
+ * referenced geometry, or a degenerate result — in which case the caller falls
3338
+ * back to full-frame inference (behaviour identical to before the crop-zone
3339
+ * plumbing).
3340
+ */
3341
+ function resolvePackageCropBbox(zones, packageRules, frameWidth, frameHeight) {
3342
+ if (frameWidth <= 0 || frameHeight <= 0) return null;
3343
+ const enabledRules = packageRules.filter((r) => r.enabled !== false);
3344
+ if (enabledRules.length === 0) return null;
3345
+ const zoneIds = /* @__PURE__ */ new Set();
3346
+ for (const rule of enabledRules) for (const id of rule.zoneIds) zoneIds.add(id);
3347
+ if (zoneIds.size === 0) return null;
3348
+ const zoneById = new Map(zones.map((z) => [z.id, z]));
3349
+ let minX = Number.POSITIVE_INFINITY;
3350
+ let minY = Number.POSITIVE_INFINITY;
3351
+ let maxX = Number.NEGATIVE_INFINITY;
3352
+ let maxY = Number.NEGATIVE_INFINITY;
3353
+ let sawPoint = false;
3354
+ for (const id of zoneIds) {
3355
+ const zone = zoneById.get(id);
3356
+ if (!zone || zone.polygon.length === 0) continue;
3357
+ for (const point of zone.polygon) {
3358
+ sawPoint = true;
3359
+ if (point.x < minX) minX = point.x;
3360
+ if (point.y < minY) minY = point.y;
3361
+ if (point.x > maxX) maxX = point.x;
3362
+ if (point.y > maxY) maxY = point.y;
3363
+ }
3364
+ }
3365
+ if (!sawPoint) return null;
3366
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
3367
+ const bbox = [
3368
+ clamp01(minX) * frameWidth,
3369
+ clamp01(minY) * frameHeight,
3370
+ clamp01(maxX) * frameWidth,
3371
+ clamp01(maxY) * frameHeight
3372
+ ];
3373
+ if (isBboxDegenerate(bbox)) return null;
3374
+ return bbox;
3375
+ }
3376
+ //#endregion
3243
3377
  //#region src/detection-pipeline/pipeline/tree-builder.ts
3244
3378
  /**
3245
3379
  * Build an executable tree from user config.
@@ -4977,10 +5111,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
4977
5111
  const deviceOverrides = effectiveDeviceId > 0 ? await this.readDeviceStore(effectiveDeviceId) : {};
4978
5112
  const effectiveTree = Object.keys(deviceOverrides).length > 0 ? applyDeviceOverridesToTree(tree, "object-detection", deviceOverrides) : tree;
4979
5113
  const nativeFaceCropProvider = this.buildNativeFaceCropProvider(input.frameHandle);
5114
+ let cropZoneBbox;
5115
+ if (isRuntime && effectiveDeviceId > 0 && effectiveTree.roots.some((r) => r.definition.extractMode === "crop-zone")) {
5116
+ await this.ensureDeviceProxy(effectiveDeviceId);
5117
+ const proxy = this.deviceProxies.get(effectiveDeviceId);
5118
+ if (proxy) cropZoneBbox = resolvePackageCropBbox(proxy.state.zones.value?.zones ?? [], proxy.state.zoneRules.value?.package ?? [], imageWidth, imageHeight) ?? void 0;
5119
+ }
4980
5120
  const { result, trace } = await executor.run(effectiveTree, rootInput, jpegProvider, imageWidth, imageHeight, effectiveDeviceId, {
4981
5121
  traceVerbosity: isRuntime ? this.eventBus ? "summary" : "off" : "full",
4982
5122
  plane: input.plane
4983
- }, nativeFaceCropProvider);
5123
+ }, nativeFaceCropProvider, cropZoneBbox);
4984
5124
  if (isRuntime) {
4985
5125
  if (trace && this.eventBus) this.eventBus.emit(createEvent(EventCategory.PipelineTrace, {
4986
5126
  type: "device",