@trillboards/edge-sdk 0.2.106 → 0.2.109

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.
@@ -143,22 +143,44 @@ def main() -> int:
143
143
  out_details = interp.get_output_details()
144
144
  shape = tuple(int(v) for v in in_detail["shape"])
145
145
 
146
- # Both runtimes take NCHW here — the LiteRT export of an ultralytics detect
147
- # model is NOT NHWC. Transposing "because LiteRT is channels-last" silently
148
- # feeds a rotated image and every residual then looks like export noise.
149
- if len(shape) != 4 or shape[1] != 3:
150
- raise ParityError(f"expected an NCHW [1,3,H,W] input, got {shape}")
151
- imgsz = shape[2]
146
+ # READ THE INPUT LAYOUT OFF THE GRAPH, the same rule this file already applies
147
+ # to outputs. It used to assert NCHW and refuse anything else, on the reasoning
148
+ # that "the LiteRT export of an ultralytics detect model is NOT NHWC". That is
149
+ # true of the toolchain the 21 committed twins were exported with and it is NOT
150
+ # a property of LiteRT: `onnx2tf` transposes to channels-last by design, and an
151
+ # export made with ultralytics 8.3.145 declares [1,640,640,3]. The assertion
152
+ # fired on a real, correct twin (gh_fall_tonlong, 2026-08-26).
153
+ #
154
+ # The warning it carried is still right and is why this branches on the DECLARED
155
+ # shape rather than on a flag: transposing "because LiteRT is channels-last"
156
+ # feeds a rotated image to a model that wanted NCHW, and every residual then
157
+ # looks like export noise. Both runtimes must receive the SAME IMAGE, each in
158
+ # the layout its own graph declares — never the same buffer.
159
+ if len(shape) != 4:
160
+ raise ParityError(f"expected a 4-D [1,3,H,W] or [1,H,W,3] input, got {shape}")
161
+ if shape[1] == 3 and shape[3] != 3:
162
+ litert_layout, imgsz = "nchw", shape[2]
163
+ elif shape[3] == 3 and shape[1] != 3:
164
+ litert_layout, imgsz = "nhwc", shape[1]
165
+ elif shape[1] == 3 and shape[3] == 3:
166
+ # [1,3,H,3] is ambiguous and no real detector input is 3 px wide.
167
+ raise ParityError(f"input {shape} is ambiguous between NCHW and NHWC")
168
+ else:
169
+ raise ParityError(f"no channel axis of size 3 in {shape}")
152
170
 
153
171
  if args.image:
154
172
  from PIL import Image
155
173
 
156
174
  pixels = np.asarray(Image.open(args.image).convert("RGB").resize((imgsz, imgsz)))
157
- x = (pixels.astype(np.float32) / 255.0).transpose(2, 0, 1)[None]
175
+ nchw = (pixels.astype(np.float32) / 255.0).transpose(2, 0, 1)[None]
158
176
  else:
159
- x = np.random.default_rng(args.seed).random(shape).astype(np.float32)
177
+ nchw = np.random.default_rng(args.seed).random((1, 3, imgsz, imgsz)).astype(np.float32)
178
+
179
+ # ONNX always gets NCHW; LiteRT gets whatever it declared, from the same pixels.
180
+ x = nchw
181
+ x_litert = nchw if litert_layout == "nchw" else nchw.transpose(0, 2, 3, 1).copy()
160
182
 
161
- interp.set_tensor(in_detail["index"], x)
183
+ interp.set_tensor(in_detail["index"], x_litert)
162
184
  interp.invoke()
163
185
  litert_out = interp.get_tensor(out_details[0]["index"])
164
186
 
@@ -173,11 +195,13 @@ def main() -> int:
173
195
  result["reason"] = why
174
196
  result["onnx"] = Path(args.onnx).name
175
197
  result["litert"] = Path(args.litert).name
198
+ result["litert_input_layout"] = litert_layout
176
199
 
177
200
  if args.json:
178
201
  print(json.dumps(result, indent=2))
179
202
  else:
180
- print(f"{'PASS' if ok else 'FAIL'} {result['onnx']} layout={layout} {why}")
203
+ print(f"{'PASS' if ok else 'FAIL'} {result['onnx']} layout={layout} "
204
+ f"litert_in={litert_layout} {why}")
181
205
  print(f" box x{result['box_scale_applied']:g}: {result['box_residual_px']:.4g} px"
182
206
  f" (unscaled {result['box_residual_unscaled']:.4g})")
183
207
  print(f" cls : {result['cls_residual']:.4g}")
@@ -0,0 +1,120 @@
1
+ """A regrade that writes `UNRESOLVED` must reproduce it, not harden it.
2
+
3
+ `regrade_control_selection.py --apply` writes its verdict back into the
4
+ adjudication it read. That makes the tool its own input, and the branch that
5
+ emits `UNRESOLVED PENDING REMEASURE` was originally gated on
6
+ `prior == "PUBLISH"` — so a second run saw `prior == "UNRESOLVED …"`, took the
7
+ gate as false, and replaced a careful "we do not know" with a definitive
8
+ `NO MODEL CLEARS BAR`, on exactly the same evidence the first run had refused to
9
+ conclude from. Nothing downstream would have caught it: the `positive-control-grade`
10
+ guard audits PUBLISH rows only, and both states are non-PUBLISH.
11
+
12
+ The other half is the mirror error. A prior that was ALREADY a measured
13
+ `NO MODEL CLEARS BAR` came out of a run that scored more controls than this tool
14
+ holds for an elided row, so softening it to `UNRESOLVED` would be the tool
15
+ asserting its own blindness over a measurement.
16
+
17
+ STDLIB ONLY — the detector CI lane runs `python3 -m unittest discover` with no
18
+ pip install, and `regrade_control_selection.py` imports nothing but the stdlib
19
+ for that reason.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import pathlib
25
+ import sys
26
+ import unittest
27
+
28
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "bench"))
29
+
30
+ from regrade_control_selection import ( # noqa: E402
31
+ PRESERVE_ON_BLOCKING,
32
+ UNRESOLVED,
33
+ rateable_controls,
34
+ regrade_capability,
35
+ )
36
+
37
+ CTLQ = {
38
+ "big": {"grade": "good"},
39
+ "tiny": {"grade": "good_small_n"},
40
+ "junk": {"grade": "DISCARDED"},
41
+ }
42
+ BAR = {"recall_retention": 0.5, "off_target_mean": 0.1}
43
+
44
+
45
+ def _cap(prior, *, rateable_rate=0.4, with_elided=True):
46
+ """One capability whose rateable control FAILS and whose 2nd row is elided."""
47
+ cands = [{
48
+ "model": "m1", "class": "fire", "quiet_t": 0.25,
49
+ "off_target_mean": 0.0, "n_valid_negatives": 100,
50
+ "by_bucket": {}, "best_control": "big", "control_n": 9,
51
+ "control_rate_025": rateable_rate, "control_rate_at_quiet_t": rateable_rate,
52
+ "per_control": {
53
+ "big": {"n": 9, "rate_025": rateable_rate, "rate_at_quiet_t": rateable_rate},
54
+ "tiny": {"n": 5, "rate_025": 0.9, "rate_at_quiet_t": 0.9},
55
+ },
56
+ }]
57
+ if with_elided:
58
+ cands.append({
59
+ "model": "m2", "class": "fire", "quiet_t": 0.25,
60
+ "off_target_mean": 0.0, "n_valid_negatives": 100,
61
+ "by_bucket": {"_elided": "summary row"},
62
+ "best_control": "tiny", "control_n": 5,
63
+ "control_rate_025": 0.9, "control_rate_at_quiet_t": 0.9,
64
+ "per_control": {},
65
+ })
66
+ return {"capability": "fire",
67
+ "decision": {"decision": prior, "control": "tiny", "control_n": 5},
68
+ "candidates": cands}
69
+
70
+
71
+ class TestRegradeControlSelection(unittest.TestCase):
72
+
73
+ def test_liveness_only_and_unusable_grades_cannot_rate_a_class(self):
74
+ per = {"big": {"n": 9}, "tiny": {"n": 5}, "junk": {"n": 26}}
75
+ self.assertEqual(sorted(rateable_controls(per, CTLQ)), ["big"])
76
+
77
+ def test_a_publish_on_a_liveness_only_set_is_withdrawn_not_failed(self):
78
+ out = regrade_capability(_cap("PUBLISH"), CTLQ, BAR)
79
+ self.assertEqual(out["regraded_decision"], UNRESOLVED)
80
+ self.assertTrue(out["candidates_unknown"], "the elided row must be named")
81
+ self.assertTrue(out["changed"])
82
+
83
+ def test_rerunning_over_its_own_output_reproduces_unresolved(self):
84
+ first = regrade_capability(_cap("PUBLISH"), CTLQ, BAR)
85
+ second = regrade_capability(_cap(first["regraded_decision"]), CTLQ, BAR)
86
+ self.assertEqual(
87
+ second["regraded_decision"], UNRESOLVED,
88
+ "a second --apply hardened an unknown into a definitive verdict")
89
+ self.assertFalse(second["changed"], "a settled UNRESOLVED must not read as changed")
90
+
91
+ def test_a_measured_failure_is_left_alone(self):
92
+ out = regrade_capability(_cap("NO MODEL CLEARS BAR"), CTLQ, BAR)
93
+ self.assertEqual(out["regraded_decision"], "NO MODEL CLEARS BAR")
94
+ self.assertFalse(out["changed"])
95
+
96
+ def test_with_no_elided_row_the_verdict_is_definitive(self):
97
+ out = regrade_capability(_cap("PUBLISH", with_elided=False), CTLQ, BAR)
98
+ self.assertEqual(out["regraded_decision"], "NO MODEL CLEARS BAR")
99
+ self.assertEqual(out["status"], "proven")
100
+
101
+ def test_a_candidate_that_clears_on_the_rateable_set_still_publishes(self):
102
+ out = regrade_capability(_cap("PUBLISH", rateable_rate=0.8), CTLQ, BAR)
103
+ self.assertEqual(out["regraded_decision"], "PUBLISH")
104
+ self.assertEqual(out["winner"]["control"], "big")
105
+ self.assertEqual(out["winner"]["control_n"], 9)
106
+
107
+ def test_the_preserve_set_names_both_states_and_nothing_else(self):
108
+ self.assertEqual(sorted(PRESERVE_ON_BLOCKING), sorted(["PUBLISH", UNRESOLVED]))
109
+
110
+ def test_branches_resolved_before_a_control_is_read_are_untouched(self):
111
+ for prior in ("NO CANDIDATE", "NOT SCREENABLE HERE"):
112
+ out = regrade_capability(
113
+ {"capability": "x", "decision": {"decision": prior}, "candidates": []},
114
+ CTLQ, BAR)
115
+ self.assertEqual(out["regraded_decision"], prior)
116
+ self.assertEqual(out["status"], "not_affected")
117
+
118
+
119
+ if __name__ == "__main__":
120
+ unittest.main()
@@ -0,0 +1,83 @@
1
+ """An ungraded control must stop the run, not quietly become eligible to decide.
2
+
3
+ `liveness_only` and `unusable` are both built by MEMBERSHIP of a grade set, so a
4
+ control with no grade at all falls through both and is treated as able to rate a
5
+ class — the flattering direction. The committed-artifact guard catches it
6
+ downstream, but by then a wrong number exists and may have been quoted.
7
+
8
+ The same asymmetry is why `--control-quality` defaults to the COMPLETE sorted set
9
+ rather than to one file: the grades live in dated files (the 2026-08-25 record is
10
+ pinned by the score cards that cite it), and a run reading a subset would give the
11
+ sets graded in the omitted file exactly that benefit of the doubt.
12
+
13
+ STDLIB ONLY — this lane runs `python3 -m unittest discover` with no pip install,
14
+ and `venue_adjudication.py` imports numpy at module scope, so it is loaded here
15
+ with `importlib` under a guard and the whole module is SKIPPED if numpy is absent
16
+ rather than erroring the lane during discovery.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib.util
22
+ import os
23
+ import pathlib
24
+ import unittest
25
+
26
+ BENCH = pathlib.Path(__file__).resolve().parent.parent / "bench"
27
+ MODULE_PATH = BENCH / "venue_adjudication.py"
28
+
29
+
30
+ def _load():
31
+ spec = importlib.util.spec_from_file_location("venue_adjudication_under_test", MODULE_PATH)
32
+ module = importlib.util.module_from_spec(spec)
33
+ spec.loader.exec_module(module)
34
+ return module
35
+
36
+
37
+ try:
38
+ VA = _load()
39
+ except ImportError: # numpy absent on this runner
40
+ VA = None
41
+
42
+
43
+ @unittest.skipIf(VA is None, "numpy is not installed on this runner")
44
+ class TestControlGradeCompleteness(unittest.TestCase):
45
+
46
+ def test_an_ungraded_control_that_is_used_refuses_the_run(self):
47
+ with self.assertRaises(SystemExit) as caught:
48
+ VA.assert_every_control_is_graded(
49
+ ["fire_cctv", "fire_firesense"], {"fire_firesense": {"grade": "good"}})
50
+ self.assertIn("fire_cctv", str(caught.exception))
51
+
52
+ def test_a_fully_graded_set_passes(self):
53
+ VA.assert_every_control_is_graded(
54
+ ["fire_cctv", "fire_firesense"],
55
+ {"fire_cctv": {"grade": "good"}, "fire_firesense": {"grade": "good"}})
56
+
57
+ def test_a_set_no_class_references_is_not_required_to_be_graded(self):
58
+ # `pos_sets` carries every scored set, including off-target ones that no
59
+ # CLASS_CONTROL entry names. Those cannot become `best_control`, so
60
+ # demanding a grade for them would refuse runs for no reason.
61
+ VA.assert_every_control_is_graded(["some_off_target_set"], {})
62
+
63
+ def test_discovery_finds_every_dated_grade_file(self):
64
+ found = [os.path.basename(p) for p in VA.discover_control_quality()]
65
+ self.assertTrue(found, "no positive-control-quality-*.json discovered")
66
+ self.assertEqual(found, sorted(found), "discovery must be sorted so later files win")
67
+ for name in found:
68
+ self.assertTrue(name.startswith("positive-control-quality-"))
69
+ self.assertTrue(name.endswith(".json"))
70
+ # The two the repo carries today. A third is fine; fewer is not.
71
+ self.assertIn("positive-control-quality-2026-08-25.json", found)
72
+ self.assertIn("positive-control-quality-2026-08-27.json", found)
73
+
74
+ def test_discovery_of_a_missing_directory_is_empty_not_an_exception(self):
75
+ self.assertEqual(VA.discover_control_quality("/nonexistent/grade/dir"), [])
76
+
77
+ def test_the_grade_lists_stay_disjoint(self):
78
+ # A grade in both lists would make the refusal order-dependent.
79
+ self.assertFalse(set(VA.LIVENESS_ONLY_GRADES) & set(VA.UNUSABLE_GRADES))
80
+
81
+
82
+ if __name__ == "__main__":
83
+ unittest.main()
@@ -523,7 +523,58 @@ PPE_KILLUMINATI_19CLS_V8S = ModelVocabulary(
523
523
  #: this artifact as the fixture that exercises `_verify_class_axis_width`, where
524
524
  #: `4 + 7 = 11` matches its real `[1, 11, 8400]` head and `4 + 6 = 10` would
525
525
  #: reject it.
526
- BUILTIN_VOCABULARIES: "tuple[ModelVocabulary, ...]" = (RFDETR_MEDIUM, GH_FORKLIFT_COMBINE, PPE_VYRA_14CLS_V8M, PPE_CONSTRUCTION_17CLS_V8N, PPE_KILLUMINATI_19CLS_V8S,)
526
+ #: Person Down Detector — published 2026-08-27 by
527
+ #: scripts/sensing/publish-sensing-model.js. The class list is the artifact's own
528
+ #: (graph metadata key 'names'); the footprint is MEASURED, 817.3594 MiB after a
529
+ #: probe inference via rusage-maxrss, not an estimate from the file size.
530
+ #: Coordinate space MEASURED, not assumed: max|box channel| = 1.5467 over 94 real frames; / imgsz 640 = 0.0024. pixels at >= 0.2, normalized at <= 0.02, refused in between.
531
+ FALL_1CLS_11X_CLASS_NAMES: "MappingProxyType[int, str]" = MappingProxyType({
532
+ 0: "fall detected",
533
+ })
534
+
535
+ FALL_1CLS_11X = ModelVocabulary(
536
+ model_name="fall_1cls_11x",
537
+ slot_names=FALL_1CLS_11X_CLASS_NAMES,
538
+ logit_slots=1,
539
+ approx_ram_bytes=857063451,
540
+ onnx_variant=VariantContract(
541
+ output_layout="anchor_4_plus_nc",
542
+ box_units="normalized",
543
+ box_scale_to_pixels=640.0,
544
+ ),
545
+ )
546
+
547
+ #: Heavy Machinery Detector (venue-adjudicated) — published 2026-08-27 by
548
+ #: scripts/sensing/publish-sensing-model.js. The class list is the artifact's own
549
+ #: (graph metadata key 'names'); the footprint is MEASURED, 593.3125 MiB after a
550
+ #: probe inference via rusage-maxrss, not an estimate from the file size.
551
+ #: Coordinate space MEASURED, not assumed: max|box channel| = 1825.6333 over 120 real frames; / imgsz 640 = 2.8526. pixels at >= 0.2, normalized at <= 0.02, refused in between.
552
+ GH_PPE_VINAYAK_L_CLASS_NAMES: "MappingProxyType[int, str]" = MappingProxyType({
553
+ 0: "hardhat",
554
+ 1: "mask",
555
+ 2: "no hardhat",
556
+ 3: "no mask",
557
+ 4: "no safety vest",
558
+ 5: "person",
559
+ 6: "safety cone",
560
+ 7: "safety vest",
561
+ 8: "machinery",
562
+ 9: "vehicle",
563
+ })
564
+
565
+ GH_PPE_VINAYAK_L = ModelVocabulary(
566
+ model_name="gh_ppe_vinayak_l",
567
+ slot_names=GH_PPE_VINAYAK_L_CLASS_NAMES,
568
+ logit_slots=10,
569
+ approx_ram_bytes=622133248,
570
+ onnx_variant=VariantContract(
571
+ output_layout="anchor_4_plus_nc",
572
+ box_units="pixels",
573
+ box_scale_to_pixels=1.0,
574
+ ),
575
+ )
576
+
577
+ BUILTIN_VOCABULARIES: "tuple[ModelVocabulary, ...]" = (RFDETR_MEDIUM, GH_FORKLIFT_COMBINE, PPE_VYRA_14CLS_V8M, PPE_CONSTRUCTION_17CLS_V8N, PPE_KILLUMINATI_19CLS_V8S, FALL_1CLS_11X, GH_PPE_VINAYAK_L,)
527
578
 
528
579
 
529
580
  class ClassIndex:
@@ -617,6 +668,8 @@ class ClassIndex:
617
668
 
618
669
 
619
670
  __all__ = [
671
+ "GH_PPE_VINAYAK_L",
672
+ "FALL_1CLS_11X",
620
673
  "PPE_KILLUMINATI_19CLS_V8S",
621
674
  "PPE_CONSTRUCTION_17CLS_V8N",
622
675
  "PPE_VYRA_14CLS_V8M",
package/dist/EdgeAgent.js CHANGED
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
44
44
  const CommandRouter_1 = require("./CommandRouter");
45
45
  const config_1 = require("./config");
46
46
  const dataDir_1 = require("./dataDir");
47
- const SDK_VERSION = '0.2.106';
47
+ const SDK_VERSION = '0.2.109';
48
48
  class EdgeAgent extends events_1.EventEmitter {
49
49
  constructor(config, deps) {
50
50
  super();
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
31
31
  skipped: number;
32
32
  };
33
33
  }
34
- declare const SDK_VERSION = "0.2.106";
34
+ declare const SDK_VERSION = "0.2.109";
35
35
  declare function checkNodeVersion(): CheckResult;
36
36
  declare function checkPlatform(): CheckResult;
37
37
  declare function checkMemory(): CheckResult;
package/dist/diagnose.js CHANGED
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
63
63
  // ---------------------------------------------------------------------------
64
64
  // SDK version — mirrors package.json; kept in sync by CI
65
65
  // ---------------------------------------------------------------------------
66
- const SDK_VERSION = '0.2.106';
66
+ const SDK_VERSION = '0.2.109';
67
67
  exports._SDK_VERSION = SDK_VERSION;
68
68
  // ---------------------------------------------------------------------------
69
69
  // ANSI helpers (no external deps)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trillboards/edge-sdk",
3
- "version": "0.2.106",
3
+ "version": "0.2.109",
4
4
  "description": "Composable cross-platform Edge AI SDK for audience sensing on DOOH/CTV devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",