@camstack/addon-pipeline 1.2.291 → 1.2.293

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 (37) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-DcTy-nJl.mjs → default-detection-model-DCQlW9fx.mjs} +2 -1
  4. package/dist/{default-detection-model-jjdmKKr3.js → default-detection-model-DTKHLLcC.js} +2 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-Bn6BQW8q.mjs → dist-C0Fr4hM5.mjs} +415 -21
  8. package/dist/{dist-D7FonEUB.js → dist-Ce8s7XqV.js} +415 -21
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-D3C1krEi.js → node-DpOrymbW.js} +1 -1
  12. package/dist/{node-BN0Na3HN.mjs → node-Xm7amG2e.mjs} +1 -1
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-CeZ8ms80.mjs → process-memory-BN5qYGsD.mjs} +1 -1
  16. package/dist/{process-memory-DI4jrCtX.js → process-memory-CBRjuhyg.js} +1 -1
  17. package/dist/recorder/index.js +2 -2
  18. package/dist/recorder/index.mjs +2 -2
  19. package/dist/{segment-demux-js-COooJ-t0.js → segment-demux-js-BhMlS_Ow.js} +1 -1
  20. package/dist/{segment-demux-js-CNPzlcyO.mjs → segment-demux-js-Bhwpj5Wl.mjs} +1 -1
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BrD7TK3W.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DKd7oYba.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-6IHzlLJ_.mjs +26 -0
  24. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DhImOdbA.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DoyA71_q.mjs} +1 -1
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-CbSGtrCj.mjs → hostInit-CCMIHjff.mjs} +2 -2
  28. package/dist/stream-broker/index.js +3 -3
  29. package/dist/stream-broker/index.mjs +3 -3
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +1 -1
  32. package/python/inference_pool.py +33 -10
  33. package/python/postprocessors/scrfd.py +21 -3
  34. package/python/postprocessors/test_scrfd.py +151 -0
  35. package/python/test_inference_pool_ov_ppp_scrfd.py +264 -0
  36. package/python/test_inference_pool_scrfd_normalization.py +142 -0
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-pSFdAaR0.mjs +0 -26
@@ -1522,24 +1522,40 @@ def _to_model_tensor(arr: "np.ndarray", channels_last: bool) -> "np.ndarray":
1522
1522
  _IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
1523
1523
  _IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
1524
1524
 
1525
+ # InsightFace's SCRFD input contract: upstream feeds `(pixel - 127.5) / 128`
1526
+ # ([-0.996, 0.996]), not the pool's historical plain `/255` ([0,1]). Expressed
1527
+ # against the pool's already-`/255` HWC array (`arr`, not raw pixels):
1528
+ # (arr*255 - 127.5) / 128 == (arr - _SCRFD_MEAN) / _SCRFD_STD
1529
+ # with mean/std pre-divided by 255, the same shape of transform as the
1530
+ # ImageNet branch below. See `postprocessors/scrfd.py` for the upstream
1531
+ # citation and the measured effect (188 → 196 GT faces found,
1532
+ # 2026-09-26 model-replacement spike, §2.4-2).
1533
+ _SCRFD_MEAN = 127.5 / 255.0
1534
+ _SCRFD_STD = 128.0 / 255.0
1535
+
1525
1536
 
1526
1537
  def _apply_normalization(arr: "np.ndarray", config: dict) -> "np.ndarray":
1527
1538
  """Apply per-model input normalization to an HWC RGB float array in [0,1].
1528
1539
 
1529
- Only ``inputNormalization == 'imagenet'`` changes anything: it subtracts the
1530
- ImageNet per-channel mean and divides by the std (RGB), matching the
1531
- EfficientNet-Lite0 / MobileNetV3 animal + vehicle classifiers whose
1532
- ``labels.json`` declares ``normalize: imagenet``. Absent / ``'none'`` /
1533
- ``'zero-one'`` keep the plain ``/255`` array byte-identical to the historical
1534
- unconditional path (detectors, CLIP/ArcFace, and the AIY bird classifier
1535
- which bakes its own scale). Runs on the HWC array BEFORE the optional
1536
- NHWC/NCHW transpose in :func:`_to_model_tensor`, so it is layout-agnostic
1537
- (channel axis is always last here). Pure numpy.
1540
+ ``inputNormalization == 'imagenet'`` subtracts the ImageNet per-channel
1541
+ mean and divides by the std (RGB), matching the EfficientNet-Lite0 /
1542
+ MobileNetV3 animal + vehicle classifiers whose ``labels.json`` declares
1543
+ ``normalize: imagenet``. ``'scrfd'`` applies InsightFace's SCRFD contract
1544
+ (`_SCRFD_MEAN`/`_SCRFD_STD` above) — the `scrfd-2.5g` catalog entry.
1545
+ Absent / ``'none'`` / ``'zero-one'`` keep the plain ``/255`` array
1546
+ byte-identical to the historical unconditional path (most detectors,
1547
+ CLIP/ArcFace, and the AIY bird classifier which bakes its own scale). Runs
1548
+ on the HWC array BEFORE the optional NHWC/NCHW transpose in
1549
+ :func:`_to_model_tensor`, so it is layout-agnostic (channel axis is always
1550
+ last here). Pure numpy.
1538
1551
  """
1539
- if config.get("inputNormalization") != "imagenet":
1552
+ mode = config.get("inputNormalization")
1553
+ if mode not in ("imagenet", "scrfd"):
1540
1554
  return arr
1541
1555
  if arr.ndim != 3 or arr.shape[-1] != 3:
1542
1556
  return arr
1557
+ if mode == "scrfd":
1558
+ return (arr - _SCRFD_MEAN) / _SCRFD_STD
1543
1559
  return (arr - _IMAGENET_MEAN) / _IMAGENET_STD
1544
1560
 
1545
1561
 
@@ -1700,6 +1716,13 @@ def _build_ov_ppp_model(core: Any, path: str, config: dict) -> "tuple[Any, dict]
1700
1716
  steps.scale([255.0, 255.0, 255.0])
1701
1717
  steps.mean([float(v) for v in _IMAGENET_MEAN])
1702
1718
  steps.scale([float(v) for v in _IMAGENET_STD])
1719
+ elif plan["normalization"] == "scrfd":
1720
+ # ((x/255) - _SCRFD_MEAN) / _SCRFD_STD — InsightFace's SCRFD contract,
1721
+ # folded the same way as the ImageNet branch above (see
1722
+ # `_apply_normalization`/`_SCRFD_MEAN`/`_SCRFD_STD`).
1723
+ steps.scale([255.0, 255.0, 255.0])
1724
+ steps.mean([_SCRFD_MEAN, _SCRFD_MEAN, _SCRFD_MEAN])
1725
+ steps.scale([_SCRFD_STD, _SCRFD_STD, _SCRFD_STD])
1703
1726
  else:
1704
1727
  steps.scale([255.0, 255.0, 255.0])
1705
1728
  built = ppp.build()
@@ -1,7 +1,12 @@
1
1
  """SCRFD face detection postprocessor.
2
2
 
3
3
  Multi-stride anchor-based face detector (strides 8, 16, 32).
4
- Ported from packages/addon-vision/src/shared/postprocess/scrfd.ts.
4
+ Originally ported from packages/addon-vision/src/shared/postprocess/scrfd.ts,
5
+ whose Node.js raw-tensor onnxruntime path was retired in favour of doing all
6
+ postprocessing here, in the Python inference pool — that TS file, and the
7
+ addon-vision package it lived in, no longer exist (see the header comment in
8
+ packages/addon-pipeline/src/detection-pipeline/postprocess/dispatch.ts). This
9
+ module is now the sole SCRFD decoder in the repo.
5
10
 
6
11
  Output: {"kind": "detections", "detections": [{"class": "face", "score", "bbox": [x1,y1,x2,y2], "landmarks?"}]}
7
12
  """
@@ -13,13 +18,26 @@ NUM_ANCHORS_PER_STRIDE = 2
13
18
 
14
19
 
15
20
  def _generate_anchors(stride: int, input_size: int) -> list[tuple[float, float]]:
16
- """Generate anchor centers for a given stride."""
21
+ """Generate anchor centers for a given stride.
22
+
23
+ Anchor centers are `(x * stride, y * stride)` — NO half-cell `+0.5` offset.
24
+ This matches upstream InsightFace's reference decoder exactly
25
+ (`insightface/model_zoo/scrfd.py`: `anchor_centers = np.stack([anchor_centers_x,
26
+ anchor_centers_y], axis=-1).astype(np.float32)`, built from a raw
27
+ `np.mgrid[:h, :w]` and then `anchor_centers * stride` — no `+0.5` anywhere
28
+ in that path). The previous `(x + 0.5) * stride` formula shifted every box
29
+ and landmark down-and-right by half a stride (4/8/16 px at 640), which is a
30
+ large fraction of a face after the letterbox upscale from a person crop.
31
+ Fixing this moved NME (COCO eyes+nose vs GT) 0.352 → 0.115 and AuraFace
32
+ self-consistency cosine 0.79 → 0.91 (2026-09-26 model-replacement spike,
33
+ docs/superpowers/specs/2026-09-26-replacement-models-spike.md §2.4-1).
34
+ """
17
35
  feat_size = int(np.ceil(input_size / stride))
18
36
  anchors = []
19
37
  for y in range(feat_size):
20
38
  for x in range(feat_size):
21
39
  for _ in range(NUM_ANCHORS_PER_STRIDE):
22
- anchors.append(((x + 0.5) * stride, (y + 0.5) * stride))
40
+ anchors.append((x * stride, y * stride))
23
41
  return anchors
24
42
 
25
43
 
@@ -0,0 +1,151 @@
1
+ """Pins SCRFD's anchor-center placement to InsightFace's upstream convention.
2
+
3
+ The production defect (2026-09-26 model-replacement spike,
4
+ docs/superpowers/specs/2026-09-26-replacement-models-spike.md §2.4-1): anchor
5
+ centers were computed as `(x + 0.5) * stride` (a "half-cell" YOLO-style
6
+ offset). Upstream InsightFace (`insightface/model_zoo/scrfd.py`) uses
7
+ `x * stride` — no half-cell. The offset shifted every box and landmark
8
+ down-and-right by half a stride (4/8/16 px at 640), degrading alignment and
9
+ therefore face-recognition accuracy on every install. Fixing it moved NME
10
+ (COCO eyes+nose GT) 0.352 → 0.115.
11
+
12
+ `unittest.TestCase`, not pytest-style: `scripts/test-python-pool.sh` classifies
13
+ test modules by content and runs the `unittest.TestCase` ones on the bare
14
+ interpreter (no pytest) — the path CI actually exercises
15
+ (`.github/workflows/ci.yml` `Test the inference pool (python)` step). Real
16
+ numpy when present; only `np.ceil` is stubbed on `ImportError` so `scrfd.py`
17
+ still imports and `_generate_anchors` (no ndarray ops, just scalar `np.ceil`)
18
+ still runs correctly — the anchor-formula regression is caught either way. The
19
+ fuller `postprocess_scrfd` round trip needs real ndarray behaviour a stub
20
+ cannot reasonably fake, so it is skipped (not silently passed) without real
21
+ numpy.
22
+ """
23
+ import math
24
+ import sys
25
+ import types
26
+ import unittest
27
+
28
+ try: # real numpy when the sandbox has it
29
+ import numpy as _numpy_probe # noqa: F401
30
+ except ImportError:
31
+ _numpy_probe = None
32
+
33
+ # `import numpy` can SUCCEED against a MINIMAL STUB another test module in
34
+ # this same `python3 -m unittest` process already installed (this file's own
35
+ # `postprocessors/` subprocess currently runs alone, but this is the same
36
+ # defensive check `test_inference_pool_scrfd_normalization.py` needs — a bare
37
+ # "did the import raise" check is not reliable once more than one stubbing
38
+ # test module shares a process). `ndarray` is real-numpy-only.
39
+ _HAS_REAL_NUMPY = _numpy_probe is not None and hasattr(_numpy_probe, "ndarray")
40
+ if _numpy_probe is None:
41
+ _np = types.ModuleType("numpy")
42
+ _np.ceil = math.ceil # the only numpy call `_generate_anchors` makes
43
+ sys.modules["numpy"] = _np
44
+
45
+ from scrfd import _generate_anchors, postprocess_scrfd # noqa: E402
46
+
47
+
48
+ class AnchorCentersHaveNoHalfCellOffsetTest(unittest.TestCase):
49
+ """Numpy-independent: `_generate_anchors` only calls `np.ceil` on a
50
+ scalar, so this runs — and catches the regression — with or without real
51
+ numpy installed."""
52
+
53
+ def test_first_anchor_sits_at_the_pixel_origin(self):
54
+ # stride=8, input_size=16 → feat_size=2 → grid x,y in {0,1}. The FIRST
55
+ # anchor center (grid cell 0,0) must sit at the pixel origin (0,0),
56
+ # not at the half-cell (4,4) the old `(x+0.5)*stride` formula
57
+ # produced.
58
+ anchors = _generate_anchors(stride=8, input_size=16)
59
+ self.assertEqual(anchors[0], (0.0, 0.0))
60
+ self.assertEqual(anchors[1], (0.0, 0.0)) # NUM_ANCHORS_PER_STRIDE=2
61
+
62
+ def test_anchor_centers_match_upstream_grid_exactly(self):
63
+ stride, input_size = 16, 32 # feat_size = 2
64
+ anchors = _generate_anchors(stride, input_size)
65
+ # Upstream: anchor_centers = mgrid(feat, feat) * stride — plain
66
+ # multiples of stride, row-major (y outer, x inner), 2 anchors/cell.
67
+ expected = []
68
+ for y in range(2):
69
+ for x in range(2):
70
+ expected.append((x * stride, y * stride))
71
+ expected.append((x * stride, y * stride))
72
+ self.assertEqual(anchors, expected)
73
+ # The rejected half-cell formula would have placed the first center
74
+ # at (8.0, 8.0), not (0.0, 0.0) — the exact regression this test
75
+ # would catch if `_generate_anchors` reverted to `(x + 0.5) * stride`.
76
+ self.assertNotEqual(anchors[0], (stride / 2, stride / 2))
77
+
78
+
79
+ def _single_anchor_output(np_mod, stride: int, input_size: int, box_offsets, kps_offsets=None):
80
+ """Build a raw SCRFD-shaped output with exactly ONE live anchor (grid cell
81
+ 0,0, first of the two anchors), all others below threshold. Needs real
82
+ ndarray behaviour, so only called from the real-numpy-gated tests below."""
83
+ feat = int(np_mod.ceil(input_size / stride))
84
+ n = feat * feat * 2
85
+ scores = np_mod.zeros((n, 1), dtype=np_mod.float32)
86
+ scores[0, 0] = 0.99
87
+ bboxes = np_mod.zeros((n, 4), dtype=np_mod.float32)
88
+ bboxes[0] = box_offsets
89
+ out = {f"score_{stride}": scores, f"bbox_{stride}": bboxes}
90
+ if kps_offsets is not None:
91
+ kps = np_mod.zeros((n, 10), dtype=np_mod.float32)
92
+ kps[0] = kps_offsets
93
+ out[f"kps_{stride}"] = kps
94
+ return out
95
+
96
+
97
+ @unittest.skipUnless(_HAS_REAL_NUMPY, "postprocess_scrfd round trip needs real ndarray ops")
98
+ class PostprocessScrfdRoundTripTest(unittest.TestCase):
99
+ def setUp(self):
100
+ import numpy as np
101
+
102
+ self.np = np
103
+
104
+ def test_box_position_uses_unshifted_anchor_center(self):
105
+ # Grid cell (0,0) at stride 8 → anchor center (0,0) after the fix. A
106
+ # symmetric box offset of 5 stride-units around the anchor center
107
+ # lands the box at [0, 0, 40, 40] pre-clamp on the un-clamped side.
108
+ stride, input_size = 8, 640
109
+ raw = _single_anchor_output(self.np, stride, input_size, box_offsets=[5.0, 5.0, 5.0, 5.0])
110
+ out = postprocess_scrfd(
111
+ raw,
112
+ {"confidence": 0.5, "inputSize": input_size, "strides": [stride]},
113
+ orig_w=1000,
114
+ orig_h=1000,
115
+ scale=1.0,
116
+ pad=(0, 0),
117
+ )
118
+ dets = out["detections"]
119
+ self.assertEqual(len(dets), 1)
120
+ _x1, _y1, x2, y2 = dets[0]["bbox"]
121
+ self.assertEqual(x2, 40.0) # 0 + 5*8
122
+ self.assertEqual(y2, 40.0)
123
+ # With the OLD (x+0.5)*stride formula the anchor center would have
124
+ # been (4, 4) and x2/y2 would be 44.0, not 40.0 — the regression
125
+ # guard.
126
+ self.assertNotEqual(x2, 44.0)
127
+
128
+ def test_landmarks_use_unshifted_anchor_center(self):
129
+ stride, input_size = 16, 640
130
+ kps = [1.0, 1.0] * 5 # 5 landmarks, each offset (1,1) stride-units
131
+ raw = _single_anchor_output(
132
+ self.np, stride, input_size, box_offsets=[1.0, 1.0, 1.0, 1.0], kps_offsets=kps
133
+ )
134
+ out = postprocess_scrfd(
135
+ raw,
136
+ {"confidence": 0.5, "inputSize": input_size, "strides": [stride]},
137
+ orig_w=1000,
138
+ orig_h=1000,
139
+ scale=1.0,
140
+ pad=(0, 0),
141
+ )
142
+ dets = out["detections"]
143
+ self.assertEqual(len(dets), 1)
144
+ for p in dets[0]["landmarks"]:
145
+ # anchor (0,0) + 1*stride = 16, not 8 (the half-cell) + 16 = 24.
146
+ self.assertEqual(p["x"], 16.0)
147
+ self.assertEqual(p["y"], 16.0)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ unittest.main()
@@ -0,0 +1,264 @@
1
+ """Pins the OpenVINO PPP fold's `'scrfd'` branch (`_build_ov_ppp_model`,
2
+ `inference_pool.py` ~1719) — the code path that folds InsightFace's SCRFD
3
+ input contract (`(x-127.5)/128`) into a compiled OpenVINO graph, mirrored
4
+ against `_apply_normalization`'s plain-Python path (covered separately by
5
+ `test_inference_pool_scrfd_normalization.py`).
6
+
7
+ `_ov_ppp_plan` (tested elsewhere) only proves the CONFIG VALUE `'scrfd'`
8
+ reaches a plan dict — it does not touch `_build_ov_ppp_model`'s actual
9
+ `elif plan["normalization"] == "scrfd":` branch, so renaming or deleting that
10
+ branch (falling through to the `else` — plain `/255`, no shift) would leave
11
+ every other test green. This file drives `_build_ov_ppp_model` itself and
12
+ asserts the exact preprocessing STEPS it records for `'scrfd'`.
13
+
14
+ No real OpenVINO install needed: `openvino` / `openvino.preprocess` are
15
+ FAKE modules that record every `PrePostProcessor` call instead of running one,
16
+ built fresh in `setUp` and removed in `tearDown` so nothing leaks into a
17
+ sibling test run in the same `python3 -m unittest` process (this repo's own
18
+ warning: "a blind stub POISONS the whole session"). If real `openvino` is
19
+ importable, this test still uses its own fakes (swapped in for the duration
20
+ of the test only) rather than depending on optional real behaviour — so it
21
+ runs the same way whether or not OpenVINO is installed.
22
+
23
+ `unittest.TestCase`, not pytest-style — runs on the bare interpreter, no
24
+ pytest needed (`scripts/test-python-pool.sh`, the path CI exercises).
25
+ """
26
+ import sys
27
+ import types
28
+ import unittest
29
+
30
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
31
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
32
+ except ImportError:
33
+ _np = types.ModuleType("numpy")
34
+ _np.array = lambda seq, dtype=None: list(seq) # noqa: E731
35
+ _np.float32 = "float32"
36
+ sys.modules["numpy"] = _np
37
+ try: # real PIL when present, for the same reason
38
+ import PIL.Image # noqa: F401
39
+ except ImportError:
40
+ _pil = types.ModuleType("PIL")
41
+ _pil_image = types.ModuleType("PIL.Image")
42
+ _pil.Image = _pil_image
43
+ sys.modules["PIL"] = _pil
44
+ sys.modules["PIL.Image"] = _pil_image
45
+ if "postprocessors" not in sys.modules:
46
+ _pp = types.ModuleType("postprocessors")
47
+ _pp.POSTPROCESSORS = {}
48
+ sys.modules["postprocessors"] = _pp
49
+
50
+ import inference_pool # noqa: E402
51
+ from inference_pool import _SCRFD_MEAN, _SCRFD_STD, _build_ov_ppp_model # noqa: E402
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Fakes standing in for the real `openvino` / `openvino.preprocess` API
56
+ # surface `_build_ov_ppp_model` calls. Each records the calls made on it.
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ class _FakeSteps:
61
+ """Stands in for OpenVINO's `PreProcessSteps` — every call is recorded,
62
+ in order, so the test can assert the EXACT scale/mean sequence."""
63
+
64
+ def __init__(self, calls: list) -> None:
65
+ self._calls = calls
66
+
67
+ def convert_element_type(self, t):
68
+ self._calls.append(("convert_element_type", t))
69
+ return self
70
+
71
+ def scale(self, v):
72
+ self._calls.append(("scale", tuple(v)))
73
+ return self
74
+
75
+ def mean(self, v):
76
+ self._calls.append(("mean", tuple(v)))
77
+ return self
78
+
79
+
80
+ class _FakeTensorInfo:
81
+ def set_element_type(self, t):
82
+ return self
83
+
84
+ def set_layout(self, layout):
85
+ return self
86
+
87
+
88
+ class _FakeModelInfo:
89
+ def set_layout(self, layout):
90
+ return self
91
+
92
+
93
+ class _FakeInputInfo:
94
+ def __init__(self, calls: list) -> None:
95
+ self._steps = _FakeSteps(calls)
96
+
97
+ def tensor(self):
98
+ return _FakeTensorInfo()
99
+
100
+ def model(self):
101
+ return _FakeModelInfo()
102
+
103
+ def preprocess(self):
104
+ return self._steps
105
+
106
+
107
+ class _FakePrePostProcessor:
108
+ """Records every preprocessing step applied, keyed by the model it was
109
+ built for — `_build_ov_ppp_model` builds exactly one per call."""
110
+
111
+ last_calls: "list | None" = None
112
+
113
+ def __init__(self, model) -> None:
114
+ self._calls: list = []
115
+ _FakePrePostProcessor.last_calls = self._calls
116
+ self._input = _FakeInputInfo(self._calls)
117
+
118
+ def input(self):
119
+ return self._input
120
+
121
+ def build(self):
122
+ return "BUILT_MODEL"
123
+
124
+
125
+ class _FakeLayout:
126
+ def __init__(self, name: str) -> None:
127
+ self.name = name
128
+
129
+
130
+ class _FakeType:
131
+ u8 = "u8"
132
+ f32 = "f32"
133
+
134
+
135
+ class _FakeDim:
136
+ """Mimics an OpenVINO static Dimension (see `test_inference_pool_ov_ppp.py`)."""
137
+
138
+ def __init__(self, length: int) -> None:
139
+ self._length = length
140
+ self.is_static = True
141
+
142
+ def __int__(self) -> int:
143
+ raise TypeError("Dimension is not directly int()-able")
144
+
145
+ def get_length(self) -> int:
146
+ return self._length
147
+
148
+
149
+ class _FakeInput:
150
+ def __init__(self, dims) -> None:
151
+ self._dims = dims
152
+
153
+ def get_partial_shape(self):
154
+ return self._dims
155
+
156
+
157
+ class _FakeModel:
158
+ def __init__(self, dims) -> None:
159
+ self._input = _FakeInput(dims)
160
+
161
+ def input(self):
162
+ return self._input
163
+
164
+
165
+ class _FakeCore:
166
+ def __init__(self, dims) -> None:
167
+ self._dims = dims
168
+
169
+ def read_model(self, path):
170
+ return _FakeModel(self._dims)
171
+
172
+
173
+ # A standard 640x640 NCHW 3-channel static-shape detector input — the shape
174
+ # `_ov_ppp_plan` requires to fold preprocessing at all.
175
+ _NCHW_640_DIMS = [_FakeDim(1), _FakeDim(3), _FakeDim(640), _FakeDim(640)]
176
+
177
+
178
+ class BuildOvPppModelScrfdBranchTest(unittest.TestCase):
179
+ def setUp(self):
180
+ # Swap in the fakes for the duration of this test ONLY, whether or
181
+ # not real openvino/openvino.preprocess were already in sys.modules —
182
+ # restored in tearDown so nothing leaks into a sibling test run in
183
+ # the same `python3 -m unittest` process.
184
+ self._saved_openvino = sys.modules.get("openvino")
185
+ self._saved_openvino_preprocess = sys.modules.get("openvino.preprocess")
186
+
187
+ fake_openvino = types.ModuleType("openvino")
188
+ fake_openvino.Layout = _FakeLayout
189
+ fake_openvino.Type = _FakeType
190
+ fake_preprocess = types.ModuleType("openvino.preprocess")
191
+ fake_preprocess.PrePostProcessor = _FakePrePostProcessor
192
+ fake_openvino.preprocess = fake_preprocess
193
+
194
+ sys.modules["openvino"] = fake_openvino
195
+ sys.modules["openvino.preprocess"] = fake_preprocess
196
+
197
+ def tearDown(self):
198
+ for name, saved in (
199
+ ("openvino", self._saved_openvino),
200
+ ("openvino.preprocess", self._saved_openvino_preprocess),
201
+ ):
202
+ if saved is None:
203
+ sys.modules.pop(name, None)
204
+ else:
205
+ sys.modules[name] = saved
206
+
207
+ def test_scrfd_branch_applies_the_scrfd_mean_and_std(self):
208
+ core = _FakeCore(_NCHW_640_DIMS)
209
+ config = {
210
+ "inputSize": 640,
211
+ "inputWidth": 640,
212
+ "inputHeight": 640,
213
+ "inputChannels": 3,
214
+ "preprocessMode": "letterbox",
215
+ "inputNormalization": "scrfd",
216
+ }
217
+ built, plan = _build_ov_ppp_model(core, "dummy-path", config)
218
+ self.assertEqual(built, "BUILT_MODEL")
219
+ self.assertEqual(plan["normalization"], "scrfd")
220
+
221
+ calls = _FakePrePostProcessor.last_calls
222
+ # convert_element_type, then scale(255) → mean(_SCRFD_MEAN) →
223
+ # scale(_SCRFD_STD) — exactly 4 recorded steps, in this order. If the
224
+ # 'scrfd' branch is removed (falling to the bare `else: scale(255)`),
225
+ # this drops to 2 steps and the assertion below fails.
226
+ self.assertEqual(
227
+ calls,
228
+ [
229
+ ("convert_element_type", "f32"),
230
+ ("scale", (255.0, 255.0, 255.0)),
231
+ ("mean", (_SCRFD_MEAN, _SCRFD_MEAN, _SCRFD_MEAN)),
232
+ ("scale", (_SCRFD_STD, _SCRFD_STD, _SCRFD_STD)),
233
+ ],
234
+ )
235
+
236
+ def test_scrfd_branch_is_distinct_from_imagenet_and_none(self):
237
+ core = _FakeCore(_NCHW_640_DIMS)
238
+
239
+ def steps_for(normalization):
240
+ config = {
241
+ "inputSize": 640,
242
+ "inputWidth": 640,
243
+ "inputHeight": 640,
244
+ "inputChannels": 3,
245
+ "preprocessMode": "letterbox",
246
+ "inputNormalization": normalization,
247
+ }
248
+ _build_ov_ppp_model(core, "dummy-path", config)
249
+ return _FakePrePostProcessor.last_calls
250
+
251
+ scrfd_calls = steps_for("scrfd")
252
+ imagenet_calls = steps_for("imagenet")
253
+ none_calls = steps_for(None)
254
+
255
+ self.assertEqual(len(scrfd_calls), 4)
256
+ self.assertEqual(len(imagenet_calls), 4)
257
+ self.assertEqual(len(none_calls), 2) # convert_element_type + scale(255) only
258
+ self.assertNotEqual(scrfd_calls, imagenet_calls)
259
+ # The imagenet branch uses the real _IMAGENET_MEAN/_STD, not SCRFD's.
260
+ self.assertNotIn(("mean", (_SCRFD_MEAN, _SCRFD_MEAN, _SCRFD_MEAN)), imagenet_calls)
261
+
262
+
263
+ if __name__ == "__main__":
264
+ unittest.main()
@@ -0,0 +1,142 @@
1
+ """Pins the SCRFD input-normalization contract (`inputNormalization: 'scrfd'`).
2
+
3
+ The production defect (2026-09-26 model-replacement spike,
4
+ docs/superpowers/specs/2026-09-26-replacement-models-spike.md §2.4-2): the
5
+ pool fed SCRFD the historical plain `/255` ([0,1]) tensor, but upstream
6
+ InsightFace expects `(pixel - 127.5) / 128` ([-0.996, 0.996]). The re-hosted
7
+ ONNX/OpenVINO/CoreML artifacts are unmodified upstream graphs (first op is a
8
+ Conv — nothing absorbs the shift), so this has to be threaded through the
9
+ runtime preprocessor rather than baked into the graph (contrast with
10
+ AuraFace/CLIP, whose graphs bake their own normalization).
11
+
12
+ This one contract, `_apply_normalization`, is the SAME function the ONNX path
13
+ and the CoreML `multiArrayType` path both call — verified 2026-09-26 that the
14
+ hosted `camstack-scrfd-2.5g.mlpackage`'s input is `multiArrayType`, not
15
+ `imageType`, so it goes through this same function. The OpenVINO PPP fast
16
+ path (`_build_ov_ppp_model`) folds the identical scale/mean/scale sequence
17
+ into the compiled graph instead (covered separately, without needing a real
18
+ OpenVINO install, by `test_inference_pool_ov_ppp_scrfd.py`), and is mutually
19
+ exclusive with `_apply_normalization` (`_preprocess` returns early on the PPP
20
+ fast path, `inference_pool.py` ~1828) — so there is no double application on
21
+ any backend.
22
+
23
+ `unittest.TestCase`, not pytest-style: `scripts/test-python-pool.sh` runs the
24
+ `unittest.TestCase` modules on the bare interpreter (no pytest) — the path CI
25
+ actually exercises. Heavy deps (numpy/PIL/postprocessors) are stubbed on
26
+ `ImportError`, the same pattern as `test_inference_pool_ov_ppp.py`, so the
27
+ module always imports; the tests that need real ndarray arithmetic are
28
+ skipped (not silently passed) without real numpy, while the ones that don't
29
+ (the constants pin, and `_ov_ppp_plan`'s pure config threading) always run.
30
+
31
+ Run: python3 -m unittest test_inference_pool_scrfd_normalization -v
32
+ """
33
+ import sys
34
+ import types
35
+ import unittest
36
+
37
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
38
+ import numpy as _numpy_probe # noqa: F401 # session for every sibling that needs it
39
+ except ImportError:
40
+ _numpy_probe = None
41
+
42
+ # `import numpy` can SUCCEED against a MINIMAL STUB another test module in
43
+ # this same `python3 -m unittest` process already installed into
44
+ # `sys.modules['numpy']` (`test_inference_pool_ov_ppp.py` and siblings do the
45
+ # same guard) — a bare "did the import raise" check would then wrongly treat
46
+ # that stub as real numpy and crash on the first `arr.ndim` access. `ndarray`
47
+ # is real-numpy-only (no stub in this file tree defines it), so check for it
48
+ # instead of trusting import success alone.
49
+ _HAS_REAL_NUMPY = _numpy_probe is not None and hasattr(_numpy_probe, "ndarray")
50
+ if _numpy_probe is None:
51
+ _np = types.ModuleType("numpy")
52
+ # `inference_pool` builds `_IMAGENET_MEAN = np.array(...)` at import time.
53
+ _np.array = lambda seq, dtype=None: list(seq) # noqa: E731
54
+ _np.float32 = "float32"
55
+ sys.modules["numpy"] = _np
56
+ try: # real PIL when present, for the same reason
57
+ import PIL.Image # noqa: F401
58
+ except ImportError:
59
+ _pil = types.ModuleType("PIL")
60
+ _pil_image = types.ModuleType("PIL.Image")
61
+ _pil.Image = _pil_image
62
+ sys.modules["PIL"] = _pil
63
+ sys.modules["PIL.Image"] = _pil_image
64
+ if "postprocessors" not in sys.modules:
65
+ _pp = types.ModuleType("postprocessors")
66
+ _pp.POSTPROCESSORS = {}
67
+ sys.modules["postprocessors"] = _pp
68
+
69
+ import inference_pool # noqa: E402
70
+ from inference_pool import ( # noqa: E402
71
+ _SCRFD_MEAN,
72
+ _SCRFD_STD,
73
+ _apply_normalization,
74
+ _ov_ppp_plan,
75
+ )
76
+
77
+
78
+ class ScrfdNormalizationConstantsTest(unittest.TestCase):
79
+ """Plain floats, no ndarray ops — always runs, with or without numpy."""
80
+
81
+ def test_constants_match_upstream_formula(self):
82
+ # _SCRFD_MEAN/_SCRFD_STD are the pool's own pre-divided-by-255
83
+ # constants; cross-check them against the raw-pixel upstream formula
84
+ # `(pixel - 127.5) / 128` directly, so a future edit to either
85
+ # constant is caught here rather than only in the end-to-end face
86
+ # count.
87
+ self.assertAlmostEqual(_SCRFD_MEAN, 127.5 / 255.0)
88
+ self.assertAlmostEqual(_SCRFD_STD, 128.0 / 255.0)
89
+
90
+
91
+ class OvPppPlanThreadsScrfdTest(unittest.TestCase):
92
+ """`_ov_ppp_plan` is pure Python (no numpy) — always runs."""
93
+
94
+ def test_scrfd_normalization_reaches_the_plan(self):
95
+ plan = _ov_ppp_plan(
96
+ dims=[1, 3, 640, 640],
97
+ input_channels=3,
98
+ preprocess_mode="letterbox",
99
+ normalization="scrfd",
100
+ enabled=True,
101
+ )
102
+ self.assertIsNotNone(plan)
103
+ self.assertEqual(plan["normalization"], "scrfd")
104
+
105
+
106
+ @unittest.skipUnless(_HAS_REAL_NUMPY, "_apply_normalization needs real ndarray ops")
107
+ class ApplyNormalizationScrfdTest(unittest.TestCase):
108
+ def setUp(self):
109
+ import numpy as np
110
+
111
+ self.np = np
112
+
113
+ def _flat_hwc(self, value: float):
114
+ """A 1x1 HWC array holding a single already-`/255` pixel value."""
115
+ return self.np.array([[[value, value, value]]], dtype=self.np.float32)
116
+
117
+ def test_scrfd_mode_matches_upstream_formula_at_pixel_extremes(self):
118
+ # Upstream, on raw pixels: (pixel - 127.5) / 128.
119
+ # pixel=0 → -0.99609375 ; pixel=255 → 0.99609375 ; pixel=127.5 → 0.0
120
+ for pixel, expected in [(0.0, -0.99609375), (255.0, 0.99609375), (127.5, 0.0)]:
121
+ arr = self._flat_hwc(pixel / 255.0)
122
+ out = _apply_normalization(arr, {"inputNormalization": "scrfd"})
123
+ self.assertEqual(out.shape, arr.shape)
124
+ self.assertAlmostEqual(float(out[0, 0, 0]), expected, places=6)
125
+
126
+ def test_absent_and_none_and_zero_one_are_unchanged(self):
127
+ arr = self._flat_hwc(0.6)
128
+ for config in ({}, {"inputNormalization": "none"}, {"inputNormalization": "zero-one"}):
129
+ out = _apply_normalization(arr, config)
130
+ self.assertTrue(self.np.array_equal(out, arr))
131
+
132
+ def test_imagenet_mode_is_unaffected_by_the_scrfd_branch(self):
133
+ # Regression guard: adding the 'scrfd' branch must not perturb the
134
+ # existing 'imagenet' branch (animal/vehicle classifiers).
135
+ arr = self._flat_hwc(0.5)
136
+ out = _apply_normalization(arr, {"inputNormalization": "imagenet"})
137
+ expected = (arr - inference_pool._IMAGENET_MEAN) / inference_pool._IMAGENET_STD
138
+ self.assertTrue(self.np.allclose(out, expected))
139
+
140
+
141
+ if __name__ == "__main__":
142
+ unittest.main()