@camstack/addon-pipeline 1.2.6 → 1.2.8

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 (41) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +2 -3
  3. package/dist/{chunk-D6vf50IK.js → chunk-emK7D4bc.js} +7 -0
  4. package/dist/detection-pipeline/index.js +104 -8
  5. package/dist/detection-pipeline/index.mjs +103 -8
  6. package/dist/{dist-Bh0H8jM6.js → dist-DNjy_sr4.js} +1374 -925
  7. package/dist/{dist-Bw_V5L0r.mjs → dist-owmpaeIY.mjs} +1379 -924
  8. package/dist/fmp4-keyframes-BzTPoPVn.js +6676 -0
  9. package/dist/fmp4-keyframes-qOgJBjMO.mjs +6669 -0
  10. package/dist/{sheet-geometry-DcpuGTCm.mjs → hub-hostname-DAJXlOgV.js} +6 -39
  11. package/dist/{sheet-geometry-BR-ip0CP.js → hub-hostname-cCknRYKj.mjs} +1 -56
  12. package/dist/{model-download-service-Cp9f4dk6-CwUb5v3Y.mjs → model-download-service-Cp9f4dk6-WO-foD7W.mjs} +26 -1
  13. package/dist/{model-download-service-Cp9f4dk6-Dv-oFwjN.js → model-download-service-Cp9f4dk6-fsdDExML.js} +1 -1
  14. package/dist/motion-wasm/index.js +1 -1
  15. package/dist/motion-wasm/index.mjs +1 -1
  16. package/dist/pipeline-runner/index.js +51 -132
  17. package/dist/pipeline-runner/index.mjs +49 -130
  18. package/dist/recorder/index.js +695 -974
  19. package/dist/recorder/index.mjs +694 -972
  20. package/dist/session-decode/decode-worker-child.js +9 -5
  21. package/dist/session-decode/decode-worker-child.mjs +8 -4
  22. package/dist/{step-definitions-xxdpJX9-.js → step-definitions-C2bNlww3.js} +1 -1
  23. package/dist/{step-definitions-aY4D9Loy.mjs → step-definitions-CkvctwuS.mjs} +1 -1
  24. package/dist/stream-broker/_stub.js +2 -2
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DX-Ef8CP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-XdI0RgvI.mjs} +2 -2
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DSfbOjdi.mjs +26 -0
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BdxY9zld.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js--zEZtV2N.mjs} +1 -1
  28. package/dist/stream-broker/{hostInit-DUL71Vjq.mjs → hostInit-DsMIdlwP.mjs} +2 -2
  29. package/dist/stream-broker/index.js +680 -15
  30. package/dist/stream-broker/index.mjs +679 -15
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CGWEeRkz.js → MaskShapeCanvas-DI4BY7W2-CftzR7Pj.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-4YbTa3Wp.js → MotionZonesSettings-NcxxQN8r-DPgVIdEn.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DL_tAs9-.js → PrivacyMaskSettings-APgPLF7p-D0HqiXHW.js} +1 -1
  35. package/embed-dist/assets/{index-CvBwcYl_.js → index-DRtbV_en.js} +17 -15
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/python/postprocessors/ctc.py +14 -5
  39. package/python/postprocessors/test_ctc.py +35 -0
  40. package/dist/chunk-BdkLduGY.mjs +0 -5
  41. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-gchR4WlX.mjs +0 -26
@@ -2,10 +2,10 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-Bh0H8jM6.js");
5
+ const require_chunk = require("../chunk-emK7D4bc.js");
6
+ const require_dist = require("../dist-DNjy_sr4.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
- const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
8
+ const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
9
9
  let node_fs = require("node:fs");
10
10
  node_fs = require_chunk.__toESM(node_fs);
11
11
  let node_path = require("node:path");
@@ -1,7 +1,6 @@
1
- import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { D as mapAudioLabelToMacro, H as errMsg, J as hydrateSchema, U as BaseAddon, _ as audioAnalysisCapability, d as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, v as audioAnalyzerCapability } from "../dist-Bw_V5L0r.mjs";
1
+ import { i as downloadFile, l as __require } from "../model-download-service-Cp9f4dk6-WO-foD7W.mjs";
2
+ import { D as mapAudioLabelToMacro, H as BaseAddon, V as errMsg, _ as audioAnalysisCapability, d as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, q as hydrateSchema, v as audioAnalyzerCapability } from "../dist-owmpaeIY.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
- import { i as downloadFile } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
5
4
  import * as fs from "node:fs";
6
5
  import * as path$1 from "node:path";
7
6
  //#region src/audio-analyzer/audio-pipeline.ts
@@ -5,6 +5,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
8
9
  var __copyProps = (to, from, except, desc) => {
9
10
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
11
  key = keys[i];
@@ -20,6 +21,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
20
21
  enumerable: true
21
22
  }) : target, mod));
22
23
  //#endregion
24
+ Object.defineProperty(exports, "__commonJSMin", {
25
+ enumerable: true,
26
+ get: function() {
27
+ return __commonJSMin;
28
+ }
29
+ });
23
30
  Object.defineProperty(exports, "__toESM", {
24
31
  enumerable: true,
25
32
  get: function() {
@@ -2,11 +2,11 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_chunk = require("../chunk-D6vf50IK.js");
6
- const require_dist = require("../dist-Bh0H8jM6.js");
7
- const require_step_definitions = require("../step-definitions-xxdpJX9-.js");
5
+ const require_chunk = require("../chunk-emK7D4bc.js");
6
+ const require_dist = require("../dist-DNjy_sr4.js");
7
+ const require_step_definitions = require("../step-definitions-C2bNlww3.js");
8
8
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
9
- const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
9
+ const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
10
10
  let node_child_process = require("node:child_process");
11
11
  let sharp = require("sharp");
12
12
  sharp = require_chunk.__toESM(sharp);
@@ -2556,7 +2556,13 @@ async function buildAlignedFaceCrop(args) {
2556
2556
  if (native && native.width >= 2 && native.height >= 2 && native.bytes.length >= native.width * native.height * 3) return {
2557
2557
  ...await alignFaceCropFromRgb(native, region, args.landmarksImageSpace, { outSize: args.outSize }),
2558
2558
  source: "native",
2559
- nativeFaceShortSidePx: nativeFaceShortSide(args.faceBbox, region, native)
2559
+ nativeFaceShortSidePx: nativeFaceShortSide(args.faceBbox, region, native),
2560
+ nativeDiag: {
2561
+ nativeCropW: native.width,
2562
+ nativeCropH: native.height,
2563
+ regionW: region.right - region.left,
2564
+ regionH: region.bottom - region.top
2565
+ }
2560
2566
  };
2561
2567
  } catch {}
2562
2568
  return {
@@ -2686,6 +2692,51 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
2686
2692
  height: native.height
2687
2693
  };
2688
2694
  }
2695
+ /**
2696
+ * Detection score at/above which a near-full-frame box is trusted as a REAL
2697
+ * close subject rather than a phantom. Below this bar a full-frame box is
2698
+ * dropped; at/above it survives (a full-frame crop of a genuine close subject
2699
+ * beats a phantom occupancy chip). The observed cam-615 phantom scored 0.52,
2700
+ * comfortably under this 0.70 bar; a genuine object right under the lens
2701
+ * (doorbell close-ups) commonly scores 0.65-0.85 — 0.70 keeps the legit band
2702
+ * above the bar while holding a wide margin over observed phantoms (~0.52).
2703
+ * Score-aware like the media guard (whose thumbnail-only bar is 0.5; this one
2704
+ * DROPS detections outright, so it sits between that and a hard cut).
2705
+ */
2706
+ var FULL_FRAME_MAX_CONFIDENCE = .7;
2707
+ /**
2708
+ * True when `bbox` is a near-full-frame LOW-confidence phantom that should be
2709
+ * DROPPED from the frame's detections (overlay / tracker / events / motion-wake).
2710
+ *
2711
+ * Rule (drop ⇔ returns true):
2712
+ * (areaRatio ≥ {@link FULL_FRAME_AREA_RATIO}
2713
+ * OR width ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameWidth
2714
+ * OR height ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameHeight)
2715
+ * AND score < `maxConfidence`
2716
+ *
2717
+ * A high-confidence detection (`score ≥ maxConfidence`) is trusted as a real
2718
+ * subject and always survives. Degenerate frame dimensions (≤0) carry no info
2719
+ * to reject on → not a phantom.
2720
+ *
2721
+ * @param bbox absolute-pixel `[x1, y1, x2, y2]` box in source-frame coords.
2722
+ * @param frameWidth source frame width in px.
2723
+ * @param frameHeight source frame height in px.
2724
+ * @param score detector confidence for this detection (0..1).
2725
+ * @param maxConfidence confidence bar below which a full-frame box is a phantom
2726
+ * (defaults to {@link FULL_FRAME_MAX_CONFIDENCE}; overridable by the caller so
2727
+ * the bar can be threaded from detection settings without new cap methods).
2728
+ */
2729
+ function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE) {
2730
+ if (frameWidth <= 0 || frameHeight <= 0) return false;
2731
+ if (score >= maxConfidence) return false;
2732
+ const [x1, y1, x2, y2] = bbox;
2733
+ const w = x2 - x1;
2734
+ const h = y2 - y1;
2735
+ if (w * h / (frameWidth * frameHeight) >= .9) return true;
2736
+ if (w >= frameWidth * .98) return true;
2737
+ if (h >= frameHeight * .98) return true;
2738
+ return false;
2739
+ }
2689
2740
  //#endregion
2690
2741
  //#region src/detection-pipeline/pipeline/plate-deskew.ts
2691
2742
  /**
@@ -3375,6 +3426,17 @@ var MIN_FACE_DETECTION_FLOOR_PX = 6;
3375
3426
  */
3376
3427
  var NATIVE_FACE_CROP_MAX_WIDTH = 320;
3377
3428
  /**
3429
+ * Minimum short side (px) of the ACTUAL crop fed to the plate-OCR (`ctc`) leaf
3430
+ * — the plate analogue of the face path's min-face-size gate. Below ~20px the
3431
+ * CTC logit map collapses toward the blank and the surviving timesteps emit
3432
+ * symbol junk ("[miv4o)", "6ou.2" — live-observed 2026-07-22). Measured on the
3433
+ * NATIVE crop when the native ROI fetch succeeded (its dimensions ARE the
3434
+ * plate's real native size — `nativeChildCrop`'s 1920 cap never binds for a
3435
+ * plate), else on the fallback detection-space tile — which IS the input OCR
3436
+ * would read, so the same floor applies honestly in both cases.
3437
+ */
3438
+ var MIN_PLATE_OCR_SHORT_SIDE_PX = 20;
3439
+ /**
3378
3440
  * StepOutput kinds that synthesize a full-frame detection when they run at
3379
3441
  * the tree ROOT (see `synthesizeRootDetection`) — classifier/embedding/OCR
3380
3442
  * steps promoted to root by `pipelineRunner.runDetailSubtree` (task 6b), e.g.
@@ -3438,14 +3500,15 @@ var PipelineExecutor = class {
3438
3500
  * native crop, the NATIVE face size so an operator sees the size that actually
3439
3501
  * gated. Throttled so it isn't per-frame spam.
3440
3502
  */
3441
- warnFaceTooSmall(child, detShortSidePx, minFaceSizePx, nativeFaceShortSidePx, deviceId) {
3503
+ warnFaceTooSmall(child, detShortSidePx, minFaceSizePx, nativeFaceShortSidePx, deviceId, diag) {
3442
3504
  this.throttledWarn(`face-small:${child.stepId}:${deviceId}`, 3e4, "face detected but too small to embed — skipping", {
3443
3505
  step: child.stepId,
3444
3506
  model: child.modelId,
3445
3507
  faceShortSidePx: Math.round(detShortSidePx),
3446
3508
  minFaceSizePx,
3447
3509
  gatedOn: nativeFaceShortSidePx === void 0 ? "detection" : "native",
3448
- ...nativeFaceShortSidePx === void 0 ? {} : { nativeFaceShortSidePx: Math.round(nativeFaceShortSidePx) }
3510
+ ...nativeFaceShortSidePx === void 0 ? {} : { nativeFaceShortSidePx: Math.round(nativeFaceShortSidePx) },
3511
+ ...diag
3449
3512
  }, deviceId);
3450
3513
  }
3451
3514
  /**
@@ -3520,6 +3583,11 @@ var PipelineExecutor = class {
3520
3583
  if (debug) console.log(`[executor] drop det macro=${mutable.macroClass} score=${mutable.score} (class or confidence filter)`);
3521
3584
  continue;
3522
3585
  }
3586
+ const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
3587
+ if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0)) {
3588
+ if (debug) console.log(`[executor] drop det macro=${mutable.macroClass} score=${mutable.score} bbox=${JSON.stringify(mutable.bbox)} (full-frame phantom guard)`);
3589
+ continue;
3590
+ }
3523
3591
  try {
3524
3592
  await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
3525
3593
  } catch (err) {
@@ -3719,10 +3787,22 @@ var PipelineExecutor = class {
3719
3787
  } : {}
3720
3788
  });
3721
3789
  const gateShortSide = aligned.nativeFaceShortSidePx ?? detShortSide;
3790
+ const faceDiag = {
3791
+ tileW: imageWidth,
3792
+ tileH: imageHeight,
3793
+ ...aligned.nativeDiag
3794
+ };
3722
3795
  if (gateShortSide < minFaceSize) {
3723
- this.warnFaceTooSmall(child, detShortSide, minFaceSize, aligned.source === "native" ? gateShortSide : void 0, deviceId);
3796
+ this.warnFaceTooSmall(child, detShortSide, minFaceSize, aligned.source === "native" ? gateShortSide : void 0, deviceId, faceDiag);
3724
3797
  continue;
3725
3798
  }
3799
+ this.throttledWarn(`face-embed-diag:${deviceId}`, 3e4, "face embed accepted (scale diagnostic)", {
3800
+ step: child.stepId,
3801
+ detShortSidePx: Math.round(detShortSide),
3802
+ gateShortSidePx: Math.round(gateShortSide),
3803
+ source: aligned.source,
3804
+ ...faceDiag
3805
+ }, deviceId);
3726
3806
  cropJpegBuf = aligned.jpeg;
3727
3807
  cropW = aligned.width;
3728
3808
  cropH = aligned.height;
@@ -3750,6 +3830,22 @@ var PipelineExecutor = class {
3750
3830
  cropW = crop.width;
3751
3831
  cropH = crop.height;
3752
3832
  }
3833
+ if (child.definition.postprocessor === "ctc") {
3834
+ const plateShortSidePx = Math.min(cropW, cropH);
3835
+ if (plateShortSidePx < MIN_PLATE_OCR_SHORT_SIDE_PX) {
3836
+ this.opts.logger?.debug("plate ROI too small for OCR — skipping", {
3837
+ tags: { deviceId },
3838
+ meta: {
3839
+ step: child.stepId,
3840
+ model: child.modelId,
3841
+ plateShortSidePx: Math.round(plateShortSidePx),
3842
+ minPlateShortSidePx: MIN_PLATE_OCR_SHORT_SIDE_PX,
3843
+ source: nativeChild ? "native" : "detection-tile"
3844
+ }
3845
+ });
3846
+ continue;
3847
+ }
3848
+ }
3753
3849
  if (child.definition.postprocessor === "ctc" && child.settings?.["rectify"] !== false) try {
3754
3850
  const rectified = await rectifyPlateJpeg(cropJpegBuf);
3755
3851
  if (rectified) {
@@ -1,8 +1,7 @@
1
- import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as supportedRuntimes$1, C as detectionPipelineCapability, H as errMsg, J as hydrateSchema, L as runtimeDevices$1, M as pipelineExecutorCapability, Q as parseJsonUnknown, T as evaluateZoneRules, U as BaseAddon, dt as string, ft as union, h as YAMNET_TO_MACRO, j as nodePin, lt as object, pt as EventCategory, q as createEvent, rt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, tt as sleep, w as enumerateInferenceDevices, x as defaultDeviceFor$1 } from "../dist-Bw_V5L0r.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-aY4D9Loy.mjs";
1
+ import { a as ensureModel, l as __require, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-WO-foD7W.mjs";
2
+ import { C as detectionPipelineCapability, H as BaseAddon, I as runtimeDevices$1, K as createEvent, M as pipelineExecutorCapability, T as evaluateZoneRules, V as errMsg, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as nodePin, nt as array, q as hydrateSchema, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as enumerateInferenceDevices, x as defaultDeviceFor$1, z as supportedRuntimes$1 } from "../dist-owmpaeIY.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-CkvctwuS.mjs";
4
4
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
5
- import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
6
5
  import { spawn } from "node:child_process";
7
6
  import sharp from "sharp";
8
7
  import * as fs from "node:fs";
@@ -2548,7 +2547,13 @@ async function buildAlignedFaceCrop(args) {
2548
2547
  if (native && native.width >= 2 && native.height >= 2 && native.bytes.length >= native.width * native.height * 3) return {
2549
2548
  ...await alignFaceCropFromRgb(native, region, args.landmarksImageSpace, { outSize: args.outSize }),
2550
2549
  source: "native",
2551
- nativeFaceShortSidePx: nativeFaceShortSide(args.faceBbox, region, native)
2550
+ nativeFaceShortSidePx: nativeFaceShortSide(args.faceBbox, region, native),
2551
+ nativeDiag: {
2552
+ nativeCropW: native.width,
2553
+ nativeCropH: native.height,
2554
+ regionW: region.right - region.left,
2555
+ regionH: region.bottom - region.top
2556
+ }
2552
2557
  };
2553
2558
  } catch {}
2554
2559
  return {
@@ -2678,6 +2683,51 @@ async function nativeChildCrop(provider, bbox, imageWidth, imageHeight, maxWidth
2678
2683
  height: native.height
2679
2684
  };
2680
2685
  }
2686
+ /**
2687
+ * Detection score at/above which a near-full-frame box is trusted as a REAL
2688
+ * close subject rather than a phantom. Below this bar a full-frame box is
2689
+ * dropped; at/above it survives (a full-frame crop of a genuine close subject
2690
+ * beats a phantom occupancy chip). The observed cam-615 phantom scored 0.52,
2691
+ * comfortably under this 0.70 bar; a genuine object right under the lens
2692
+ * (doorbell close-ups) commonly scores 0.65-0.85 — 0.70 keeps the legit band
2693
+ * above the bar while holding a wide margin over observed phantoms (~0.52).
2694
+ * Score-aware like the media guard (whose thumbnail-only bar is 0.5; this one
2695
+ * DROPS detections outright, so it sits between that and a hard cut).
2696
+ */
2697
+ var FULL_FRAME_MAX_CONFIDENCE = .7;
2698
+ /**
2699
+ * True when `bbox` is a near-full-frame LOW-confidence phantom that should be
2700
+ * DROPPED from the frame's detections (overlay / tracker / events / motion-wake).
2701
+ *
2702
+ * Rule (drop ⇔ returns true):
2703
+ * (areaRatio ≥ {@link FULL_FRAME_AREA_RATIO}
2704
+ * OR width ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameWidth
2705
+ * OR height ≥ {@link FULL_FRAME_DIMENSION_RATIO} × frameHeight)
2706
+ * AND score < `maxConfidence`
2707
+ *
2708
+ * A high-confidence detection (`score ≥ maxConfidence`) is trusted as a real
2709
+ * subject and always survives. Degenerate frame dimensions (≤0) carry no info
2710
+ * to reject on → not a phantom.
2711
+ *
2712
+ * @param bbox absolute-pixel `[x1, y1, x2, y2]` box in source-frame coords.
2713
+ * @param frameWidth source frame width in px.
2714
+ * @param frameHeight source frame height in px.
2715
+ * @param score detector confidence for this detection (0..1).
2716
+ * @param maxConfidence confidence bar below which a full-frame box is a phantom
2717
+ * (defaults to {@link FULL_FRAME_MAX_CONFIDENCE}; overridable by the caller so
2718
+ * the bar can be threaded from detection settings without new cap methods).
2719
+ */
2720
+ function isFullFramePhantomDetection(bbox, frameWidth, frameHeight, score, maxConfidence = FULL_FRAME_MAX_CONFIDENCE) {
2721
+ if (frameWidth <= 0 || frameHeight <= 0) return false;
2722
+ if (score >= maxConfidence) return false;
2723
+ const [x1, y1, x2, y2] = bbox;
2724
+ const w = x2 - x1;
2725
+ const h = y2 - y1;
2726
+ if (w * h / (frameWidth * frameHeight) >= .9) return true;
2727
+ if (w >= frameWidth * .98) return true;
2728
+ if (h >= frameHeight * .98) return true;
2729
+ return false;
2730
+ }
2681
2731
  //#endregion
2682
2732
  //#region src/detection-pipeline/pipeline/plate-deskew.ts
2683
2733
  /**
@@ -3367,6 +3417,17 @@ var MIN_FACE_DETECTION_FLOOR_PX = 6;
3367
3417
  */
3368
3418
  var NATIVE_FACE_CROP_MAX_WIDTH = 320;
3369
3419
  /**
3420
+ * Minimum short side (px) of the ACTUAL crop fed to the plate-OCR (`ctc`) leaf
3421
+ * — the plate analogue of the face path's min-face-size gate. Below ~20px the
3422
+ * CTC logit map collapses toward the blank and the surviving timesteps emit
3423
+ * symbol junk ("[miv4o)", "6ou.2" — live-observed 2026-07-22). Measured on the
3424
+ * NATIVE crop when the native ROI fetch succeeded (its dimensions ARE the
3425
+ * plate's real native size — `nativeChildCrop`'s 1920 cap never binds for a
3426
+ * plate), else on the fallback detection-space tile — which IS the input OCR
3427
+ * would read, so the same floor applies honestly in both cases.
3428
+ */
3429
+ var MIN_PLATE_OCR_SHORT_SIDE_PX = 20;
3430
+ /**
3370
3431
  * StepOutput kinds that synthesize a full-frame detection when they run at
3371
3432
  * the tree ROOT (see `synthesizeRootDetection`) — classifier/embedding/OCR
3372
3433
  * steps promoted to root by `pipelineRunner.runDetailSubtree` (task 6b), e.g.
@@ -3430,14 +3491,15 @@ var PipelineExecutor = class {
3430
3491
  * native crop, the NATIVE face size so an operator sees the size that actually
3431
3492
  * gated. Throttled so it isn't per-frame spam.
3432
3493
  */
3433
- warnFaceTooSmall(child, detShortSidePx, minFaceSizePx, nativeFaceShortSidePx, deviceId) {
3494
+ warnFaceTooSmall(child, detShortSidePx, minFaceSizePx, nativeFaceShortSidePx, deviceId, diag) {
3434
3495
  this.throttledWarn(`face-small:${child.stepId}:${deviceId}`, 3e4, "face detected but too small to embed — skipping", {
3435
3496
  step: child.stepId,
3436
3497
  model: child.modelId,
3437
3498
  faceShortSidePx: Math.round(detShortSidePx),
3438
3499
  minFaceSizePx,
3439
3500
  gatedOn: nativeFaceShortSidePx === void 0 ? "detection" : "native",
3440
- ...nativeFaceShortSidePx === void 0 ? {} : { nativeFaceShortSidePx: Math.round(nativeFaceShortSidePx) }
3501
+ ...nativeFaceShortSidePx === void 0 ? {} : { nativeFaceShortSidePx: Math.round(nativeFaceShortSidePx) },
3502
+ ...diag
3441
3503
  }, deviceId);
3442
3504
  }
3443
3505
  /**
@@ -3512,6 +3574,11 @@ var PipelineExecutor = class {
3512
3574
  if (debug) console.log(`[executor] drop det macro=${mutable.macroClass} score=${mutable.score} (class or confidence filter)`);
3513
3575
  continue;
3514
3576
  }
3577
+ const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
3578
+ if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0)) {
3579
+ if (debug) console.log(`[executor] drop det macro=${mutable.macroClass} score=${mutable.score} bbox=${JSON.stringify(mutable.bbox)} (full-frame phantom guard)`);
3580
+ continue;
3581
+ }
3515
3582
  try {
3516
3583
  await this.executeChildren(rootStep.children, mutable, fullFrameJpegProvider, imageWidth, imageHeight, traceBuilder, stepTimings, ctx, poolAgg, runOpts?.plane, deviceId, nativeCropProvider);
3517
3584
  } catch (err) {
@@ -3711,10 +3778,22 @@ var PipelineExecutor = class {
3711
3778
  } : {}
3712
3779
  });
3713
3780
  const gateShortSide = aligned.nativeFaceShortSidePx ?? detShortSide;
3781
+ const faceDiag = {
3782
+ tileW: imageWidth,
3783
+ tileH: imageHeight,
3784
+ ...aligned.nativeDiag
3785
+ };
3714
3786
  if (gateShortSide < minFaceSize) {
3715
- this.warnFaceTooSmall(child, detShortSide, minFaceSize, aligned.source === "native" ? gateShortSide : void 0, deviceId);
3787
+ this.warnFaceTooSmall(child, detShortSide, minFaceSize, aligned.source === "native" ? gateShortSide : void 0, deviceId, faceDiag);
3716
3788
  continue;
3717
3789
  }
3790
+ this.throttledWarn(`face-embed-diag:${deviceId}`, 3e4, "face embed accepted (scale diagnostic)", {
3791
+ step: child.stepId,
3792
+ detShortSidePx: Math.round(detShortSide),
3793
+ gateShortSidePx: Math.round(gateShortSide),
3794
+ source: aligned.source,
3795
+ ...faceDiag
3796
+ }, deviceId);
3718
3797
  cropJpegBuf = aligned.jpeg;
3719
3798
  cropW = aligned.width;
3720
3799
  cropH = aligned.height;
@@ -3742,6 +3821,22 @@ var PipelineExecutor = class {
3742
3821
  cropW = crop.width;
3743
3822
  cropH = crop.height;
3744
3823
  }
3824
+ if (child.definition.postprocessor === "ctc") {
3825
+ const plateShortSidePx = Math.min(cropW, cropH);
3826
+ if (plateShortSidePx < MIN_PLATE_OCR_SHORT_SIDE_PX) {
3827
+ this.opts.logger?.debug("plate ROI too small for OCR — skipping", {
3828
+ tags: { deviceId },
3829
+ meta: {
3830
+ step: child.stepId,
3831
+ model: child.modelId,
3832
+ plateShortSidePx: Math.round(plateShortSidePx),
3833
+ minPlateShortSidePx: MIN_PLATE_OCR_SHORT_SIDE_PX,
3834
+ source: nativeChild ? "native" : "detection-tile"
3835
+ }
3836
+ });
3837
+ continue;
3838
+ }
3839
+ }
3745
3840
  if (child.definition.postprocessor === "ctc" && child.settings?.["rectify"] !== false) try {
3746
3841
  const rectified = await rectifyPlateJpeg(cropJpegBuf);
3747
3842
  if (rectified) {