@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.
- package/deploy/detector/bench/litert-twin-parity.json +27 -1
- package/deploy/detector/bench/regrade_control_selection.py +371 -0
- package/deploy/detector/bench/venue_adjudication.py +959 -0
- package/deploy/detector/bench/verify_litert_twin_parity.py +34 -10
- package/deploy/detector/tests/test_regrade_control_selection.py +120 -0
- package/deploy/detector/tests/test_venue_adjudication_grades.py +83 -0
- package/deploy/detector/trillboards_detector/labels.py +54 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/package.json +1 -1
|
@@ -20,7 +20,18 @@
|
|
|
20
20
|
"its scores agree to 7.2e-08, but row-wise the two disagree and lexsorting by box does",
|
|
21
21
|
"not collapse it. The model returns ZERO detections above 0.25 on 40 real production",
|
|
22
22
|
"frames, so there is no above-threshold sample to compare and row-level parity is",
|
|
23
|
-
"UNVERIFIED. It is not published. Settle it on a corpus the model actually fires on."
|
|
23
|
+
"UNVERIFIED. It is not published. Settle it on a corpus the model actually fires on.",
|
|
24
|
+
"",
|
|
25
|
+
"2026-08-26: the 22nd twin, gh_fall_tonlong_v2, is the first whose LiteRT input is",
|
|
26
|
+
"NHWC [1,640,640,3]. The line above saying an ultralytics detect export is NOT NHWC",
|
|
27
|
+
"described the TOOLCHAIN the first 21 were exported with, not LiteRT: onnx2tf",
|
|
28
|
+
"transposes to channels-last by design and ultralytics 8.3.145 leaves it that way.",
|
|
29
|
+
"verify_litert_twin_parity.py used to ASSERT NCHW and refused a correct twin; it now",
|
|
30
|
+
"reads the input layout off the graph -- the same rule it already applied to outputs",
|
|
31
|
+
"-- and feeds each runtime the SAME IMAGE in the layout that runtime declares. The",
|
|
32
|
+
"warning that motivated the assertion still stands: transposing because 'LiteRT is",
|
|
33
|
+
"channels-last' feeds a rotated image to a model that wanted NCHW, and every residual",
|
|
34
|
+
"then reads as export noise."
|
|
24
35
|
],
|
|
25
36
|
"measured_at": "2026-08-25",
|
|
26
37
|
"tolerances": {
|
|
@@ -308,6 +319,21 @@
|
|
|
308
319
|
"reason": "ok",
|
|
309
320
|
"onnx": "weapon_gunknife_11n.onnx",
|
|
310
321
|
"litert": "weapon_gunknife_11n.tflite"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"layout": "anchor_4_plus_nc",
|
|
325
|
+
"imgsz": 640,
|
|
326
|
+
"box_scale_applied": 640.0,
|
|
327
|
+
"box_residual_px": 0.0007171630859375,
|
|
328
|
+
"box_residual_unscaled": 646.9068156480789,
|
|
329
|
+
"cls_residual": 1.3665612286217765e-07,
|
|
330
|
+
"pass": true,
|
|
331
|
+
"reason": "ok",
|
|
332
|
+
"onnx": "gh_fall_tonlong_v2.onnx",
|
|
333
|
+
"litert": "gh_fall_tonlong_v2.tflite",
|
|
334
|
+
"litert_input_layout": "nhwc",
|
|
335
|
+
"measured_at": "2026-08-26",
|
|
336
|
+
"note": "The 22nd twin, added 2026-08-26, and the FIRST whose LiteRT input is NHWC. gh_fall_tonlong had no parity measurement at all until now: upstream ships `Violence` at slot 0 and `violence` at slot 6, normalize_class_name folds them to one name and ModelVocabulary refuses at construction, so neither format could be probed. Both were re-exported from ok.pt (sha256 1e12d89198e28d7c650ba86886c511fecd9f70b9d7f8bf985e76704690e58dd9) with the slots disambiguated by INDEX -- violence_slot0 / violence_slot6 -- which preserves upstream truth and asserts no semantics we have not measured."
|
|
311
337
|
}
|
|
312
338
|
]
|
|
313
339
|
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Re-apply the venue adjudication's own bar with the control-selection fix.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS
|
|
5
|
+
---------------
|
|
6
|
+
`venue_adjudication.py` chose the control a verdict rests on with
|
|
7
|
+
`argmax(rate_025)` over every set listed for the class. That is a selector with no
|
|
8
|
+
access to the contact-sheet grade, so it reliably picks the set that flatters the
|
|
9
|
+
model. On 2026-08-26 it picked `fire_generic` -- n=5, graded `good_small_n`, whose
|
|
10
|
+
own note reads "n=5 so only a coarse liveness signal" -- over `fire_cctv`, n=39,
|
|
11
|
+
graded `good`, "realistic scale for a venue camera", scored in the same run against
|
|
12
|
+
the same artifact at the same threshold. `fire` and `smoke` were awarded PUBLISH on
|
|
13
|
+
the 5-frame set.
|
|
14
|
+
|
|
15
|
+
The tool is fixed. Re-running it end to end needs the per-frame score matrices
|
|
16
|
+
(`<alias>.npz` + `combo.json`) from the eval box, which are not committed. This
|
|
17
|
+
re-applies the SAME bar to the numbers the artifact already carries, so the
|
|
18
|
+
corrected verdict is derivable from what is in the repo -- and, crucially, marks
|
|
19
|
+
each capability `proven` or `needs_remeasure` rather than pretending the elided
|
|
20
|
+
rows are known.
|
|
21
|
+
|
|
22
|
+
WHAT IT CANNOT DO, AND SAYS SO
|
|
23
|
+
------------------------------
|
|
24
|
+
`venue_adjudication.py` elides `per_control` for candidates ranked 5+ that carry no
|
|
25
|
+
shipped bytes. For those rows only the WINNING set's rate survives. Where the
|
|
26
|
+
winning set was liveness-only, that candidate's rateable-control rate is unknown
|
|
27
|
+
and the capability is reported `needs_remeasure` with the candidate named. An
|
|
28
|
+
unknown is not a pass and it is not a fail; it is the reason to re-run.
|
|
29
|
+
|
|
30
|
+
python3 regrade_control_selection.py \
|
|
31
|
+
--adjudication trillboard-api/scripts/ai/eval-baselines/venue-adjudication-2026-08-26.json \
|
|
32
|
+
--control-quality trillboard-api/scripts/ai/eval-baselines/positive-control-quality-2026-08-25.json \
|
|
33
|
+
--out trillboard-api/scripts/ai/eval-baselines/venue-adjudication-control-regrade-2026-08-26.json
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
|
|
41
|
+
#: Grades whose own note says the set cannot rate a class. Kept in step with
|
|
42
|
+
#: `venue_adjudication.py::LIVENESS_ONLY_GRADES`; the test pins them equal.
|
|
43
|
+
LIVENESS_ONLY_GRADES = ("good_small_n", "weak")
|
|
44
|
+
UNUSABLE_GRADES = ("DISCARDED", "POOR", "CONTAMINATED")
|
|
45
|
+
|
|
46
|
+
#: What a capability becomes when the PUBLISH is provably withdrawn but what
|
|
47
|
+
#: replaces it is not. NOT a verdict a model may ship on, and NOT a failure: the
|
|
48
|
+
#: elided rows could still clear the bar, and writing "NO MODEL CLEARS BAR" over
|
|
49
|
+
#: them would be the same mistake in the other direction -- a definitive claim
|
|
50
|
+
#: standing on evidence that was never committed.
|
|
51
|
+
UNRESOLVED = "UNRESOLVED PENDING REMEASURE"
|
|
52
|
+
|
|
53
|
+
#: Priors that an unknown may hold open. A PUBLISH because it is exactly what this
|
|
54
|
+
#: tool exists to withdraw; `UNRESOLVED` because this tool writes it, and a run
|
|
55
|
+
#: over its own output must not harden "not known" into "known bad".
|
|
56
|
+
PRESERVE_ON_BLOCKING = ("PUBLISH", UNRESOLVED)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def rateable_controls(per_control: dict, ctlq: dict) -> dict:
|
|
60
|
+
"""The controls that may carry a verdict: graded, usable, not liveness-only."""
|
|
61
|
+
out = {}
|
|
62
|
+
for name, row in (per_control or {}).items():
|
|
63
|
+
grade = (ctlq.get(name) or {}).get("grade")
|
|
64
|
+
if grade in LIVENESS_ONLY_GRADES or grade in UNUSABLE_GRADES:
|
|
65
|
+
continue
|
|
66
|
+
out[name] = dict(row, grade=grade)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def regrade_capability(cap: dict, ctlq: dict, bar: dict) -> dict:
|
|
71
|
+
"""Re-apply the bar to one capability using only rateable controls."""
|
|
72
|
+
recall_bar = bar["recall_retention"]
|
|
73
|
+
off_bar = bar["off_target_mean"]
|
|
74
|
+
prior = cap["decision"].get("decision")
|
|
75
|
+
|
|
76
|
+
# `decide()` resolves two branches BEFORE it ever looks at a control, and both
|
|
77
|
+
# are properties of the negative set. Re-deriving them here would only
|
|
78
|
+
# reproduce them from less information, and getting them subtly wrong is how a
|
|
79
|
+
# regrade starts reporting movement it did not cause -- the first run of this
|
|
80
|
+
# tool "moved" eight capabilities that have no control involved at all.
|
|
81
|
+
if not cap["candidates"] or prior in ("NO CANDIDATE", "NOT SCREENABLE HERE"):
|
|
82
|
+
return {
|
|
83
|
+
"capability": cap["capability"],
|
|
84
|
+
"prior_decision": prior,
|
|
85
|
+
"regraded_decision": prior,
|
|
86
|
+
"changed": False,
|
|
87
|
+
"status": "not_affected",
|
|
88
|
+
"why": "resolved before any control is read: %s" % (prior,),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
rows, unknown = [], []
|
|
92
|
+
for cand in cap["candidates"]:
|
|
93
|
+
elided = "_elided" in (cand.get("by_bucket") or {})
|
|
94
|
+
rate = rateable_controls(cand.get("per_control"), ctlq)
|
|
95
|
+
if not rate:
|
|
96
|
+
# No per-control detail survived the elision. If the set this row was
|
|
97
|
+
# decided on is itself rateable, the surviving number IS the rateable
|
|
98
|
+
# number and the row is knowable; otherwise it is not.
|
|
99
|
+
best = cand.get("best_control")
|
|
100
|
+
bg = (ctlq.get(best) or {}).get("grade") if best else None
|
|
101
|
+
if best and bg not in LIVENESS_ONLY_GRADES and bg not in UNUSABLE_GRADES:
|
|
102
|
+
rows.append({
|
|
103
|
+
"model": cand["model"], "class": cand["class"],
|
|
104
|
+
"control": best, "control_n": cand.get("control_n"),
|
|
105
|
+
"grade": bg,
|
|
106
|
+
"rate_at_quiet_t": cand.get("control_rate_at_quiet_t"),
|
|
107
|
+
"rate_025": cand.get("control_rate_025"),
|
|
108
|
+
"off_target_mean": cand.get("off_target_mean"),
|
|
109
|
+
"quiet_t": cand.get("quiet_t"),
|
|
110
|
+
"n_valid_negatives": cand.get("n_valid_negatives"),
|
|
111
|
+
"source": "winning set was already rateable",
|
|
112
|
+
})
|
|
113
|
+
else:
|
|
114
|
+
unknown.append({
|
|
115
|
+
"model": cand["model"], "class": cand["class"],
|
|
116
|
+
"decided_on": best, "grade": bg,
|
|
117
|
+
"why_unknown": ("per_control elided for this rank and the set it was "
|
|
118
|
+
"decided on cannot rate a class, so its rateable-control "
|
|
119
|
+
"rate was never committed"),
|
|
120
|
+
"elided": elided,
|
|
121
|
+
})
|
|
122
|
+
continue
|
|
123
|
+
# argmax(rate_025) among the rateable, exactly as the fixed tool does.
|
|
124
|
+
name = max(rate, key=lambda k: (rate[k]["rate_025"] or 0))
|
|
125
|
+
row = rate[name]
|
|
126
|
+
rows.append({
|
|
127
|
+
"model": cand["model"], "class": cand["class"],
|
|
128
|
+
"control": name, "control_n": row["n"], "grade": row["grade"],
|
|
129
|
+
"rate_at_quiet_t": row["rate_at_quiet_t"], "rate_025": row["rate_025"],
|
|
130
|
+
"off_target_mean": cand.get("off_target_mean"),
|
|
131
|
+
"quiet_t": cand.get("quiet_t"),
|
|
132
|
+
"n_valid_negatives": cand.get("n_valid_negatives"),
|
|
133
|
+
"also_scored": {k: {"n": v["n"], "grade": (ctlq.get(k) or {}).get("grade"),
|
|
134
|
+
"rate_at_quiet_t": v["rate_at_quiet_t"]}
|
|
135
|
+
for k, v in (cand.get("per_control") or {}).items() if k != name},
|
|
136
|
+
"source": "recomputed from committed per_control",
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
live = [r for r in rows if (r["rate_025"] or 0) > 0]
|
|
140
|
+
retained = [r for r in live if (r["rate_at_quiet_t"] or 0) >= recall_bar]
|
|
141
|
+
ok = [r for r in retained
|
|
142
|
+
if r["off_target_mean"] is not None and r["off_target_mean"] <= off_bar]
|
|
143
|
+
|
|
144
|
+
if ok:
|
|
145
|
+
# The artifact's own ordering is preserved: candidates arrive sorted.
|
|
146
|
+
win = ok[0]
|
|
147
|
+
verdict = "PUBLISH"
|
|
148
|
+
why = ("clears the bar on %s (n=%d, grade %s): keeps %.1f%% at conf %s"
|
|
149
|
+
% (win["control"], win["control_n"], win["grade"],
|
|
150
|
+
100 * (win["rate_at_quiet_t"] or 0), win["quiet_t"]))
|
|
151
|
+
elif live:
|
|
152
|
+
best = max(live, key=lambda r: (r["rate_at_quiet_t"] or 0))
|
|
153
|
+
verdict = "NO MODEL CLEARS BAR"
|
|
154
|
+
why = ("no candidate keeps >=%.0f%% of a control that can rate a class. Best is "
|
|
155
|
+
"%s / '%s' at %.1f%% of %s (n=%d, grade %s)."
|
|
156
|
+
% (100 * recall_bar, best["model"], best["class"],
|
|
157
|
+
100 * (best["rate_at_quiet_t"] or 0), best["control"],
|
|
158
|
+
best["control_n"], best["grade"]))
|
|
159
|
+
win = None
|
|
160
|
+
else:
|
|
161
|
+
verdict, why, win = "UNINFORMATIVE", "no candidate fires on a rateable control", None
|
|
162
|
+
|
|
163
|
+
# An unknown row can only ever ADD a passing candidate, so it can overturn a
|
|
164
|
+
# failing verdict and can never overturn a PUBLISH.
|
|
165
|
+
blocking = unknown if verdict != "PUBLISH" else []
|
|
166
|
+
|
|
167
|
+
# ...and a verdict an unknown could overturn must not be stated as if it could
|
|
168
|
+
# not. Withdrawing the PUBLISH is CERTAIN -- it rested on a set that cannot
|
|
169
|
+
# rate a class, and no re-measurement changes that. Replacing it with a
|
|
170
|
+
# definitive failure is NOT certain, because an elided candidate could still
|
|
171
|
+
# clear the bar. Two different claims; only the first is proved here, and
|
|
172
|
+
# asserting the second would be the same error in the other direction.
|
|
173
|
+
#
|
|
174
|
+
# Gated on the PRIOR. Where the prior was already a MEASURED failure, the
|
|
175
|
+
# original run reached it with MORE controls and better data than this tool
|
|
176
|
+
# has for the elided rows, so overwriting it with `UNRESOLVED` would be this
|
|
177
|
+
# tool asserting its own blindness over a measurement. Those rows still carry
|
|
178
|
+
# `status: needs_remeasure`; their verdict is left alone.
|
|
179
|
+
#
|
|
180
|
+
# `UNRESOLVED` is in the set because THIS TOOL WRITES IT. Without it, running
|
|
181
|
+
# `--apply` twice would see `prior == UNRESOLVED`, skip this branch, and
|
|
182
|
+
# overwrite its own careful "not known" with a definitive `NO MODEL CLEARS
|
|
183
|
+
# BAR` -- the exact claim the first run refused to make, on the same
|
|
184
|
+
# evidence. The guard would not catch it either: it audits PUBLISH rows only.
|
|
185
|
+
# `--selftest` pins the idempotence.
|
|
186
|
+
if blocking and verdict == "NO MODEL CLEARS BAR" and prior in PRESERVE_ON_BLOCKING:
|
|
187
|
+
why = ("the prior verdict is WITHDRAWN -- it rested on a control that cannot rate a "
|
|
188
|
+
"class, which no re-measurement changes. What REPLACES it is not yet known: "
|
|
189
|
+
+ why
|
|
190
|
+
+ (" %d candidate(s) had their rateable-control rate elided and could still "
|
|
191
|
+
"clear the bar (%s). Re-run venue_adjudication.py against the score "
|
|
192
|
+
"matrices to settle it."
|
|
193
|
+
% (len(blocking), ", ".join("%s/%s" % (b["model"], b["class"])
|
|
194
|
+
for b in blocking))))
|
|
195
|
+
verdict = UNRESOLVED
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"capability": cap["capability"],
|
|
199
|
+
"prior_decision": prior,
|
|
200
|
+
"prior_control": cap["decision"].get("control"),
|
|
201
|
+
"prior_control_n": cap["decision"].get("control_n"),
|
|
202
|
+
"prior_control_grade": (ctlq.get(cap["decision"].get("control")) or {}).get("grade"),
|
|
203
|
+
"prior_events_lost": (cap["decision"].get("gate") or {}).get("events_lost"),
|
|
204
|
+
"regraded_decision": verdict,
|
|
205
|
+
"changed": bool(prior and prior != verdict),
|
|
206
|
+
"status": "needs_remeasure" if blocking else "proven",
|
|
207
|
+
"why": why,
|
|
208
|
+
"winner": win,
|
|
209
|
+
"candidates_regraded": len(rows),
|
|
210
|
+
"candidates_unknown": blocking,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _selftest() -> None:
|
|
215
|
+
"""Pin the two things a reader cannot see by reading the ladder.
|
|
216
|
+
|
|
217
|
+
1. IDEMPOTENCE. `--apply` writes `UNRESOLVED` into the artifact it reads. A
|
|
218
|
+
second run must reproduce it, not harden it into a failure.
|
|
219
|
+
2. A MEASURED failure is left alone. A prior that was already
|
|
220
|
+
`NO MODEL CLEARS BAR` came from a run with more controls than this tool
|
|
221
|
+
holds for an elided row, and must not be softened to `UNRESOLVED` either.
|
|
222
|
+
"""
|
|
223
|
+
ctlq = {"big": {"grade": "good"}, "tiny": {"grade": "good_small_n"}}
|
|
224
|
+
bar = {"recall_retention": 0.5, "off_target_mean": 0.1}
|
|
225
|
+
|
|
226
|
+
def cap(prior):
|
|
227
|
+
return {
|
|
228
|
+
"capability": "fire",
|
|
229
|
+
"decision": {"decision": prior, "control": "tiny", "control_n": 5},
|
|
230
|
+
"candidates": [
|
|
231
|
+
{ # a candidate that fails on the rateable set
|
|
232
|
+
"model": "m1", "class": "fire", "quiet_t": 0.25,
|
|
233
|
+
"off_target_mean": 0.0, "n_valid_negatives": 100,
|
|
234
|
+
"by_bucket": {}, "best_control": "big", "control_n": 9,
|
|
235
|
+
"control_rate_025": 0.4, "control_rate_at_quiet_t": 0.4,
|
|
236
|
+
"per_control": {"big": {"n": 9, "rate_025": 0.4, "rate_at_quiet_t": 0.4},
|
|
237
|
+
"tiny": {"n": 5, "rate_025": 0.9, "rate_at_quiet_t": 0.9}},
|
|
238
|
+
},
|
|
239
|
+
{ # ...and one whose rateable rate was elided
|
|
240
|
+
"model": "m2", "class": "fire", "quiet_t": 0.25,
|
|
241
|
+
"off_target_mean": 0.0, "n_valid_negatives": 100,
|
|
242
|
+
"by_bucket": {"_elided": "summary row"},
|
|
243
|
+
"best_control": "tiny", "control_n": 5,
|
|
244
|
+
"control_rate_025": 0.9, "control_rate_at_quiet_t": 0.9,
|
|
245
|
+
"per_control": {},
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
first = regrade_capability(cap("PUBLISH"), ctlq, bar)
|
|
251
|
+
assert first["regraded_decision"] == UNRESOLVED, first["regraded_decision"]
|
|
252
|
+
assert first["candidates_unknown"], "the elided row must be reported"
|
|
253
|
+
|
|
254
|
+
second = regrade_capability(cap(first["regraded_decision"]), ctlq, bar)
|
|
255
|
+
assert second["regraded_decision"] == UNRESOLVED, (
|
|
256
|
+
"re-running over this tool's own output hardened an unknown into %r"
|
|
257
|
+
% second["regraded_decision"])
|
|
258
|
+
assert second["changed"] is False, "a settled UNRESOLVED must not read as changed"
|
|
259
|
+
|
|
260
|
+
measured = regrade_capability(cap("NO MODEL CLEARS BAR"), ctlq, bar)
|
|
261
|
+
assert measured["regraded_decision"] == "NO MODEL CLEARS BAR", measured["regraded_decision"]
|
|
262
|
+
|
|
263
|
+
print("selftest OK: idempotent on UNRESOLVED; a measured failure is left alone")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main() -> None:
|
|
267
|
+
ap = argparse.ArgumentParser()
|
|
268
|
+
if "--selftest" in __import__("sys").argv[1:]:
|
|
269
|
+
_selftest()
|
|
270
|
+
return
|
|
271
|
+
ap.add_argument("--adjudication", required=True)
|
|
272
|
+
ap.add_argument("--control-quality", required=True)
|
|
273
|
+
ap.add_argument("--out", required=True)
|
|
274
|
+
ap.add_argument("--apply", action="store_true",
|
|
275
|
+
help="also write the corrected verdict back into --adjudication. The "
|
|
276
|
+
"adjudication is the record; leaving a verdict in it that its own "
|
|
277
|
+
"numbers refute is a second source of truth, not a safer one.")
|
|
278
|
+
args = ap.parse_args()
|
|
279
|
+
|
|
280
|
+
adj = json.load(open(args.adjudication))
|
|
281
|
+
ctlq = json.load(open(args.control_quality))
|
|
282
|
+
bar = adj["bar"]
|
|
283
|
+
|
|
284
|
+
rows = [regrade_capability(c, ctlq, bar) for c in adj["capabilities"]]
|
|
285
|
+
changed = [r for r in rows if r["changed"]]
|
|
286
|
+
|
|
287
|
+
# The regrade artifact is a record OF A TRANSITION. Once `--apply` has run, the
|
|
288
|
+
# adjudication no longer holds the verdict that moved, so a second run reads
|
|
289
|
+
# nothing to change and would write an empty `changed_list` over the record of
|
|
290
|
+
# what did. The adjudication itself is idempotent (proved by `--selftest` and
|
|
291
|
+
# by re-running `--apply`, which leaves it byte-identical); this file is not,
|
|
292
|
+
# and erasing it silently would leave the corrected artifact with nothing
|
|
293
|
+
# beside it saying what it was corrected FROM.
|
|
294
|
+
if not changed and os.path.exists(args.out):
|
|
295
|
+
try:
|
|
296
|
+
prev = json.load(open(args.out))
|
|
297
|
+
except (OSError, ValueError):
|
|
298
|
+
prev = {}
|
|
299
|
+
if (prev.get("summary") or {}).get("changed"):
|
|
300
|
+
raise SystemExit(
|
|
301
|
+
"refusing to overwrite %s: it records %d capability transition(s) and this "
|
|
302
|
+
"run found none, because the adjudication it read has ALREADY been corrected. "
|
|
303
|
+
"That is the expected result of re-running, not a reason to erase the record. "
|
|
304
|
+
"Point --out somewhere else, or re-run against an uncorrected adjudication."
|
|
305
|
+
% (args.out, (prev["summary"] or {})["changed"]))
|
|
306
|
+
|
|
307
|
+
json.dump({
|
|
308
|
+
"schema": "trillboards.venue-adjudication-control-regrade/1",
|
|
309
|
+
"regrades": adj["schema"] + " @ " + adj["negative_set"],
|
|
310
|
+
"_what": "the SAME bar, re-applied with the control-selection fix: a verdict may "
|
|
311
|
+
"only rest on a control whose contact-sheet grade can rate a class.",
|
|
312
|
+
"_not_a_re_run": "the per-frame scores are unchanged and are NOT re-derived here. "
|
|
313
|
+
"This re-reads the numbers the adjudication already committed. A "
|
|
314
|
+
"capability marked `needs_remeasure` has a candidate whose "
|
|
315
|
+
"rateable-control rate was elided and must come from a re-run.",
|
|
316
|
+
"liveness_only_grades": list(LIVENESS_ONLY_GRADES),
|
|
317
|
+
"unusable_grades": list(UNUSABLE_GRADES),
|
|
318
|
+
"bar": bar,
|
|
319
|
+
"summary": {
|
|
320
|
+
"capabilities": len(rows),
|
|
321
|
+
"changed": len(changed),
|
|
322
|
+
"changed_list": [{"capability": r["capability"],
|
|
323
|
+
"was": r["prior_decision"], "now": r["regraded_decision"],
|
|
324
|
+
"status": r["status"]} for r in changed],
|
|
325
|
+
},
|
|
326
|
+
"capabilities": rows,
|
|
327
|
+
}, open(args.out, "w"), indent=1)
|
|
328
|
+
|
|
329
|
+
if args.apply and changed:
|
|
330
|
+
by_cap = {r["capability"]: r for r in changed}
|
|
331
|
+
for cap in adj["capabilities"]:
|
|
332
|
+
r = by_cap.get(cap["capability"])
|
|
333
|
+
if not r:
|
|
334
|
+
continue
|
|
335
|
+
was = dict(cap["decision"])
|
|
336
|
+
cap["decision"] = {
|
|
337
|
+
"decision": r["regraded_decision"],
|
|
338
|
+
"why": r["why"],
|
|
339
|
+
# The gate is a promise computed from a recall this verdict no
|
|
340
|
+
# longer accepts. Keeping it would leave an events-lost figure in
|
|
341
|
+
# the file with nothing behind it.
|
|
342
|
+
"gate": None,
|
|
343
|
+
"corrected": {
|
|
344
|
+
"on": "2026-08-26",
|
|
345
|
+
"by": "deploy/detector/bench/regrade_control_selection.py --apply",
|
|
346
|
+
"was": was,
|
|
347
|
+
"ships": False,
|
|
348
|
+
"why_corrected": (
|
|
349
|
+
"the verdict rested on `%s` (n=%s, grade %s), a set whose own "
|
|
350
|
+
"contact-sheet note says it can only show liveness. Re-applying "
|
|
351
|
+
"this run's OWN bar to the controls that can rate a class "
|
|
352
|
+
"overturns it." % (was.get("control"), was.get("control_n"),
|
|
353
|
+
r.get("prior_control_grade"))),
|
|
354
|
+
"needs_remeasure": r["candidates_unknown"],
|
|
355
|
+
"regrade_artifact": args.out,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
cap["changed"] = bool(cap.get("prior_decision_taxi_negatives")
|
|
359
|
+
and cap["prior_decision_taxi_negatives"]
|
|
360
|
+
!= r["regraded_decision"])
|
|
361
|
+
json.dump(adj, open(args.adjudication, "w"), indent=1)
|
|
362
|
+
|
|
363
|
+
print(json.dumps({"capabilities": len(rows), "changed": len(changed),
|
|
364
|
+
"applied": bool(args.apply and changed),
|
|
365
|
+
"changed_list": [(r["capability"], r["prior_decision"],
|
|
366
|
+
r["regraded_decision"], r["status"])
|
|
367
|
+
for r in changed]}, indent=1))
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
main()
|