inferencekit-cli 0.1.0__py3-none-any.whl

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 (104) hide show
  1. inferencekit/__init__.py +9 -0
  2. inferencekit/__main__.py +5 -0
  3. inferencekit/analysis/__init__.py +1 -0
  4. inferencekit/analysis/ncu_csv.py +284 -0
  5. inferencekit/analysis/nsys_sqlite.py +518 -0
  6. inferencekit/canonical.py +104 -0
  7. inferencekit/checkpoint.py +396 -0
  8. inferencekit/cli.py +274 -0
  9. inferencekit/cuda/bw_peak.cu +252 -0
  10. inferencekit/cuda/torch_bw.py +60 -0
  11. inferencekit/engines/__init__.py +7 -0
  12. inferencekit/engines/adapters.py +190 -0
  13. inferencekit/engines/clip/__init__.py +13 -0
  14. inferencekit/engines/clip/comfyui.py +749 -0
  15. inferencekit/engines/clip/diffusers_ltx.py +601 -0
  16. inferencekit/engines/clip/noise.py +46 -0
  17. inferencekit/engines/clip/official_ltx.py +316 -0
  18. inferencekit/engines/clip/server.py +542 -0
  19. inferencekit/engines/clip/stepdump.py +182 -0
  20. inferencekit/engines/gates.py +94 -0
  21. inferencekit/engines/sglang_worker.py +86 -0
  22. inferencekit/engines/vllm_worker.py +129 -0
  23. inferencekit/evidence.py +427 -0
  24. inferencekit/gpuinfo.py +93 -0
  25. inferencekit/hardware.py +135 -0
  26. inferencekit/mcp_server.py +178 -0
  27. inferencekit/ops/__init__.py +30 -0
  28. inferencekit/ops/bench.py +184 -0
  29. inferencekit/ops/bench_audio.py +384 -0
  30. inferencekit/ops/bench_clip.py +819 -0
  31. inferencekit/ops/bench_image.py +393 -0
  32. inferencekit/ops/bench_rounds.py +297 -0
  33. inferencekit/ops/bench_serve.py +866 -0
  34. inferencekit/ops/ceiling.py +135 -0
  35. inferencekit/ops/compare.py +378 -0
  36. inferencekit/ops/correctness.py +153 -0
  37. inferencekit/ops/doctor.py +486 -0
  38. inferencekit/ops/gates.py +1772 -0
  39. inferencekit/ops/gates_clip.py +930 -0
  40. inferencekit/ops/intake.py +1321 -0
  41. inferencekit/ops/ledger.py +749 -0
  42. inferencekit/ops/manifest.py +309 -0
  43. inferencekit/ops/ncu.py +183 -0
  44. inferencekit/ops/nsys.py +224 -0
  45. inferencekit/ops/prompts.py +316 -0
  46. inferencekit/ops/quality.py +2161 -0
  47. inferencekit/ops/quality_clip.py +690 -0
  48. inferencekit/ops/quality_clip_steps.py +341 -0
  49. inferencekit/ops/report.py +317 -0
  50. inferencekit/ops/review.py +119 -0
  51. inferencekit/ops/rocprof.py +89 -0
  52. inferencekit/ops/roofline.py +392 -0
  53. inferencekit/ops/store.py +61 -0
  54. inferencekit/proc.py +172 -0
  55. inferencekit/prompts/ATTRIBUTION.md +56 -0
  56. inferencekit/prompts/__init__.py +167 -0
  57. inferencekit/prompts/build.py +518 -0
  58. inferencekit/prompts/dev-load.jsonl.gz +0 -0
  59. inferencekit/prompts/dev-load.manifest.json +212 -0
  60. inferencekit/prompts/dev.jsonl.gz +0 -0
  61. inferencekit/prompts/dev.manifest.json +214 -0
  62. inferencekit/prompts/exclusions.json +54 -0
  63. inferencekit/quality/__init__.py +23 -0
  64. inferencekit/quality/client.py +66 -0
  65. inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
  66. inferencekit/quality/fake.py +329 -0
  67. inferencekit/quality/gsm8k.py +224 -0
  68. inferencekit/quality/logprobs.py +106 -0
  69. inferencekit/quality/metrics.py +235 -0
  70. inferencekit/quality/niah.py +63 -0
  71. inferencekit/quality/parquet.py +296 -0
  72. inferencekit/quality/sampling.py +262 -0
  73. inferencekit/quality/step_replay.py +217 -0
  74. inferencekit/quality/video_metrics.py +304 -0
  75. inferencekit/registry.py +206 -0
  76. inferencekit/review/__init__.py +6 -0
  77. inferencekit/review/diff.py +163 -0
  78. inferencekit/review/media.py +181 -0
  79. inferencekit/review/page.py +920 -0
  80. inferencekit/review/rundir.py +844 -0
  81. inferencekit/roofline_headers.py +504 -0
  82. inferencekit/schemas/evidence.v1.json +130 -0
  83. inferencekit/schemas/evidence.v2.json +499 -0
  84. inferencekit/schemas/intake.v1.json +276 -0
  85. inferencekit/schemas/run-manifest.v1.json +118 -0
  86. inferencekit/serve/__init__.py +8 -0
  87. inferencekit/serve/audio_client.py +330 -0
  88. inferencekit/serve/client.py +446 -0
  89. inferencekit/serve/clip_client.py +351 -0
  90. inferencekit/serve/clip_rounds.py +429 -0
  91. inferencekit/serve/image_client.py +373 -0
  92. inferencekit/serve/loadgen.py +136 -0
  93. inferencekit/serve/metrics.py +203 -0
  94. inferencekit/serve/profiles.py +105 -0
  95. inferencekit/serve/prompts_io.py +125 -0
  96. inferencekit/serve/rounds.py +1111 -0
  97. inferencekit/serve/servers.py +897 -0
  98. inferencekit/stats.py +249 -0
  99. inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
  100. inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
  101. inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
  102. inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
  103. inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  104. inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
@@ -0,0 +1,930 @@
1
+ """gates clip: anti-cheat gates for a video clip speed claim.
2
+
3
+ A clip time is only worth publishing if the engine made the clip it was asked for. These gates probe a
4
+ live clip engine (the clip HTTP contract, or ComfyUI through its adapter: serve/clip_client.py) and,
5
+ optionally, check a ``bench clip`` record's timed clips. Each gate ends PASS, FAIL, UNPROVEN or NOT RUN
6
+ with the probes and numbers that decided it:
7
+
8
+ steps_honoured the engine's own step counter (effective.steps_run) equals the steps asked on every
9
+ probe; a clip at N steps differs from the same request at N/2 steps; and N steps take
10
+ measurably longer than N/2 (median e2e ratio >= 1.10 in >= 2/3 of the pairs; FAIL below
11
+ 1.03: the step count changes nothing). An engine that runs a fixed fraction of whatever
12
+ it is asked scales the same way; quality clip (outputs vs the reference at the same
13
+ steps) catches that, not this gate.
14
+ cfg_honoured the same request with another guidance scale (1.0, or +3 when the clip set's is <= 1.5)
15
+ gives a different clip. The time ratio is recorded, not judged (an engine may batch the
16
+ two guidance branches).
17
+ resolution_frames every clip is decoded (PyAV in the video venv with --python, else ffprobe): the decoded
18
+ frames' width and height, the decoded frame count, the fps from the decoded timestamps
19
+ and, when audio was asked for, the decoded audio duration (within 0.1 s + one frame of
20
+ num_frames / fps) must match the request. What the container header claims is recorded
21
+ beside it but never trusted.
22
+ seed_honoured a different seed (same prompt) gives a different clip, and the same seed repeated stays
23
+ within the A/A floor: FAIL when the different seed is no farther from the clip than the
24
+ repeat is, or when the repeat is more than half as far (the seed is not reproduced).
25
+ prompt_sensitivity a one-word change in the prompt (same seed) gives a different clip.
26
+ no_cache replaying a request is not > 10% faster than its first time (median over pairs and in
27
+ >= 2/3 of the pairs, as gates run's caching gate); different requests never get
28
+ byte-identical clips; no probe reports a stage served from a cache. With
29
+ --bench-evidence, the same holds for every timed clip of that bench clip run.
30
+
31
+ --bench-evidence: the record must verify (its id matches its content, its requests.jsonl matches the
32
+ sha256 it lists). Its timed clips are found in its own store directory, in those of the records it
33
+ extends (``data.outputs_from``: bench clip --extend and bench rounds' clip mode keep each round's clips
34
+ with the record that timed it) and in the run directory (``outputs/<label>/r<round>/<id>.<fmt>``), and
35
+ each file must have the sha256 its row records: one that does not is not the clip that was timed (FAIL);
36
+ a timed clip with no file anywhere leaves resolution_frames UNPROVEN. ``--max-decode N`` decodes a random
37
+ N of them, drawn from the probe seed.
38
+
39
+ "Different clip" is judged on decoded pixels when --python names a venv with PyAV and numpy: the mean
40
+ absolute difference (MAD, 0-255 levels) between the clips must exceed max(3 x the same-seed repeat's MAD,
41
+ 0.5). Byte-identical clips are always a FAIL; without --python, clips whose bytes differ leave the gate
42
+ UNPROVEN (bytes can differ while the pictures do not).
43
+
44
+ The probes are one randomly interleaved sequence after an untimed warm-up: each repeat follows its first
45
+ request with other probes between them, seeds are drawn fresh for every run (and recorded), and the
46
+ one-word change is drawn from a pool, so no probe is predictable from this code. Probes use one clip of
47
+ the clip set (``--clip-id``, default the first): its task, shape, steps and guidance. A refused or failed
48
+ probe is evidence: it leaves its gates UNPROVEN, never PASS. Only when the engine answers no probe at all
49
+ are the live gates NOT RUN.
50
+
51
+ No probe follows a request with the same prompt or image file, so an engine never has a legitimate
52
+ encoding to reuse (ComfyUI's node cache keeps the last run's outputs: a text encoder whose prompt did not
53
+ change is skipped, and no_cache fails on any costly cached stage but the fixed negative prompt's). Each
54
+ request/replay pair is a group with its own prompt (the clip's for the first, then the clip's plus
55
+ " (probe N)") and, for i2v, its own image file (the same picture with a comment chunk: image_variant); the
56
+ probes compared with each other are in one group (N/2 steps with its pair, the other seed with the first,
57
+ the other guidance with the last), the one-word change has an image file of its own, and the warm-up has
58
+ a prompt and image file no probe has. With --pairs 1 the first group is too big to keep its probes apart:
59
+ those neighbours are recorded and named in no_cache.
60
+ """
61
+
62
+ from __future__ import annotations
63
+
64
+ import dataclasses
65
+ import hashlib
66
+ import json
67
+ import random
68
+ import secrets
69
+ import time
70
+ import zlib
71
+ from pathlib import Path
72
+
73
+ from inferencekit import evidence as ev
74
+ from inferencekit.canonical import file_sha256
75
+ from inferencekit.ops import bench_clip as bc
76
+ from inferencekit.ops.bench_serve import _record_call
77
+ from inferencekit.ops.gates import _gate, _median, overall
78
+ from inferencekit.ops.quality_clip import probe_clips, run_worker
79
+ from inferencekit.proc import which
80
+ from inferencekit.registry import Arg, OpError, operation
81
+ from inferencekit.serve import clip_client as cc
82
+ from inferencekit.serve.client import Endpoint, api_key_from_env
83
+
84
+ GATES = ("steps_honoured", "cfg_honoured", "resolution_frames", "seed_honoured", "prompt_sensitivity", "no_cache")
85
+ LIVE = {"steps_honoured", "cfg_honoured", "seed_honoured", "prompt_sensitivity"}
86
+ # "clip_gates" is a newer evidence kind; on a store without it the record falls back to "gates".
87
+ KIND = "clip_gates" if "clip_gates" in ev.KINDS else "gates"
88
+ FORMAT = "inferencekit.gates.clip.v1"
89
+ STEP_RATIO_PASS = 1.10 # N steps must take >= 1.10x the time of N/2 ...
90
+ STEP_RATIO_FAIL = 1.03 # ... and below 1.03x the step count plainly changes nothing
91
+ SENSITIVITY = 3.0 # a changed input must move the clip > 3x as far as the same request repeated
92
+ MIN_MAD = 0.5 # ... and by more than half a grey level on average
93
+ REPRO_SHARE = 0.5 # the repeat may be at most half as far as a different seed
94
+ FPS_TOL = 0.01 # relative
95
+ AUDIO_TOL_S = 0.1 # plus one frame
96
+ SWAP_WORDS = ("crimson", "turquoise", "golden", "silver", "violet", "emerald", "amber", "cobalt", "scarlet",
97
+ "ivory", "copper", "indigo", "lavender", "charcoal", "saffron")
98
+
99
+
100
+ # --------------------------------------------------------------------------- probes
101
+
102
+ @dataclasses.dataclass
103
+ class Probe:
104
+ key: str
105
+ kind: str # base | replay | half_steps | other_seed | prompt | cfg
106
+ req: cc.ClipRequest
107
+ pair: int | None = None
108
+ meta: dict = dataclasses.field(default_factory=dict)
109
+ vs: str = "base0" # other_seed, prompt, cfg: the probe this one is compared with
110
+ res: cc.ClipResult | None = None
111
+ sha: str | None = None
112
+ path: Path | None = None
113
+ t_start: float | None = None
114
+
115
+ @property
116
+ def ok(self) -> bool:
117
+ return self.res is not None and self.res.ok and self.sha is not None
118
+
119
+ @property
120
+ def e2e_ms(self) -> float | None:
121
+ return self.res.e2e_ms if self.ok else None
122
+
123
+
124
+ def one_word_change(prompt: str, rng: random.Random) -> tuple[str, dict]:
125
+ """The prompt with one word replaced by a word from the pool (or one word put in front)."""
126
+ words = prompt.split(" ")
127
+ idx = [i for i, w in enumerate(words) if w.isalpha() and len(w) >= 4]
128
+ if idx:
129
+ i = rng.choice(idx)
130
+ old = words[i]
131
+ new = rng.choice([w for w in SWAP_WORDS if w != old.lower()])
132
+ words[i] = new.capitalize() if old[:1].isupper() else new
133
+ return " ".join(words), {"replaced": old, "with": words[i], "word_index": i}
134
+ new = rng.choice(SWAP_WORDS)
135
+ return f"{new} {prompt}", {"inserted": new}
136
+
137
+
138
+ def alt_guidance(g: float) -> float:
139
+ return 1.0 if g > 1.5 else round(g + 3.0, 6)
140
+
141
+
142
+ PROBE_SUFFIX = " (probe {n})" # the prompts of probe groups after the first
143
+ PNG_SIG = b"\x89PNG\r\n\x1a\n"
144
+
145
+
146
+ def image_variant(data: bytes, tag: str) -> bytes | None:
147
+ """The same picture as another file: ``tag`` in a comment that decoders skip (a PNG tEXt chunk after IHDR,
148
+ a JPEG COM segment after SOI and APP0). None for any other format."""
149
+ text = tag.encode("latin-1")
150
+ if data.startswith(PNG_SIG) and data[12:16] == b"IHDR":
151
+ end = 8 + 12 + int.from_bytes(data[8:12], "big") # the signature, then IHDR: length, type, data, crc
152
+ body = b"Comment\x00" + text
153
+ chunk = len(body).to_bytes(4, "big") + b"tEXt" + body + zlib.crc32(b"tEXt" + body).to_bytes(4, "big")
154
+ return data[:end] + chunk + data[end:]
155
+ if data.startswith(b"\xff\xd8\xff"):
156
+ at = 4 + int.from_bytes(data[4:6], "big") if data[2:4] == b"\xff\xe0" else 2 # JFIF's APP0 stays first
157
+ return data[:at] + b"\xff\xfe" + (len(text) + 2).to_bytes(2, "big") + text + data[at:]
158
+ return None
159
+
160
+
161
+ def clash(a: cc.ClipRequest, b: cc.ClipRequest) -> bool:
162
+ """Requests in a row that share a prompt or an image file: the second could reuse an encoding of the first
163
+ (ComfyUI's node cache keeps the last run's outputs)."""
164
+ return a.prompt == b.prompt or (a.image is not None and a.image == b.image)
165
+
166
+
167
+ def plan_probes(clip: dict, clipset, rng: random.Random, pairs: int) -> list[Probe]:
168
+ """Probes in their first-seen roles; ``order`` interleaves them.
169
+
170
+ Each request/replay pair is a group with an input of its own: the clip's prompt for group 0, the prompt
171
+ plus " (probe N)" for the others, and for i2v the clip's image file for group 0 and the same picture as
172
+ another file (image_variant) for the others. The probes compared with each other share their group's
173
+ inputs: N/2 steps with its pair, the other seed with group 0, the other guidance with the last group. The
174
+ one-word change gets a variant image of its own. So ``order`` can keep every request from following one
175
+ with the same prompt or image file, and no engine can serve a probe's encoding from the probe before it."""
176
+ seeds: list[int] = []
177
+ while len(seeds) < pairs + 1:
178
+ s = rng.getrandbits(31)
179
+ if s not in seeds:
180
+ seeds.append(s)
181
+ probes = []
182
+ half = clip["steps"] // 2
183
+ first = bc.make_request(clip, seeds[0], clipset)
184
+ prompts = [first.prompt] + [first.prompt + PROBE_SUFFIX.format(n=i + 1) for i in range(1, pairs)]
185
+ for i in range(pairs):
186
+ image = first.image
187
+ if image is not None and i:
188
+ image = image_variant(first.image, f"inferencekit probe group {i}") or first.image
189
+ base = dataclasses.replace(first, id=f"base{i}", seed=seeds[i], prompt=prompts[i], image=image)
190
+ probes.append(Probe(f"base{i}", "base", base, pair=i))
191
+ probes.append(Probe(f"replay{i}", "replay", dataclasses.replace(base, id=f"replay{i}"), pair=i))
192
+ if half >= 1:
193
+ probes.append(Probe(f"half{i}", "half_steps", dataclasses.replace(base, id=f"half{i}", steps=half),
194
+ pair=i, meta={"steps": half}))
195
+ b0, last = probes[0].req, next(p.req for p in probes if p.key == f"base{pairs - 1}")
196
+ probes.append(Probe("seed", "other_seed", dataclasses.replace(b0, id="seed", seed=seeds[pairs])))
197
+ prompt, how = one_word_change(b0.prompt, rng)
198
+ image = (image_variant(b0.image, "inferencekit probe prompt") or b0.image) if b0.image is not None else None
199
+ probes.append(Probe("prompt", "prompt", dataclasses.replace(b0, id="prompt", prompt=prompt, image=image),
200
+ meta=how))
201
+ g = alt_guidance(last.guidance_scale)
202
+ probes.append(Probe("cfg", "cfg", dataclasses.replace(last, id="cfg", guidance_scale=g),
203
+ meta={"guidance_scale": g}, vs=f"base{pairs - 1}"))
204
+ return probes
205
+
206
+
207
+ def warmup_request(b0: cc.ClipRequest, i: int, seed: int, probes: list[Probe]) -> cc.ClipRequest:
208
+ """An untimed warm-up at the probe shape with a prompt (and an image file) no probe has."""
209
+ image = (image_variant(b0.image, "inferencekit warm-up") or b0.image) if b0.image is not None else None
210
+ return dataclasses.replace(b0, id=f"warmup{i}", seed=seed, image=image,
211
+ prompt=bc.warmup_prompt(b0.prompt, {p.req.prompt for p in probes}))
212
+
213
+
214
+ def neighbour_clashes(seq: list[Probe]) -> list[list[str]]:
215
+ """[key, next key] of probes in a row that share a prompt or an image file."""
216
+ return [[a.key, b.key] for a, b in zip(seq, seq[1:]) if clash(a.req, b.req)]
217
+
218
+
219
+ def order(probes: list[Probe], rng: random.Random, tries: int = 200) -> list[Probe]:
220
+ """A random interleave that keeps each repeat after its first request with others between them, and no
221
+ probe right after one with the same prompt or image file; when no such order is found (one pair has
222
+ too few other probes to go between its own), the one with the fewest such neighbours."""
223
+ by = {p.key: p for p in probes}
224
+ best = None
225
+ for _ in range(tries):
226
+ chains = [[by[f"base{p.pair}"], p] for p in probes if p.kind == "replay"]
227
+ chains += [[p] for p in probes if p.kind not in ("base", "replay")]
228
+ seq: list[Probe] = []
229
+ while chains: # a random chain head each step, one that does not clash with the last probe when possible
230
+ free = [c for c in chains if not (seq and clash(seq[-1].req, c[0].req))]
231
+ c = rng.choice(free or chains)
232
+ seq.append(c.pop(0))
233
+ chains = [x for x in chains if x]
234
+ adjacent = sum(1 for a, b in zip(seq, seq[1:]) if b.kind == "replay" and a.key == f"base{b.pair}")
235
+ score = (adjacent, len(neighbour_clashes(seq)))
236
+ if best is None or score < best[0]:
237
+ best = (score, seq)
238
+ if score == (0, 0):
239
+ break
240
+ return best[1]
241
+
242
+
243
+ def run_probes(backend, seq: list[Probe], staging: Path, label: str) -> None:
244
+ for p in seq:
245
+ p.t_start = time.time()
246
+ p.res = backend.generate(p.req)
247
+ if p.res.ok and p.res.video is not None:
248
+ p.sha = hashlib.sha256(p.res.video).hexdigest()
249
+ p.path = staging / "outputs" / label / "probes" / f"{p.key}.{p.res.format}"
250
+ p.path.parent.mkdir(parents=True, exist_ok=True)
251
+ p.path.write_bytes(p.res.video)
252
+ p.res.video = None # on disk now
253
+
254
+
255
+ def probe_row(p: Probe, decoded: dict | None) -> dict:
256
+ body = p.req.body()
257
+ if "image" in body:
258
+ body["image"] = {"sha256": hashlib.sha256(p.req.image).hexdigest(), "bytes": len(p.req.image)}
259
+ r = p.res
260
+ return {"key": p.key, "kind": p.kind, "pair": p.pair, "meta": p.meta, "request": body, "ok": p.ok,
261
+ "status": r.status if r else None, "error": (r.error if r else "not sent"),
262
+ "e2e_ms": p.e2e_ms, "out_sha256": p.sha, "format": r.format if r and r.ok else None,
263
+ "effective": r.effective if r else None, "stage_ms": r.stage_ms if r else None,
264
+ "cache": r.cache if r else None, "decoded": decoded, "t_start": p.t_start}
265
+
266
+
267
+ # --------------------------------------------------------------------------- distances
268
+
269
+ DISTANCE_PAIRS = (("same", "base", "replay"), ("steps", "base", "half"))
270
+
271
+
272
+ def distance_plan(probes: dict[str, Probe], pairs: int) -> list[tuple[str, str, str]]:
273
+ """(name, a, b) clip pairs whose pixel distance the gates need."""
274
+ out = []
275
+ for i in range(pairs):
276
+ out.append((f"same{i}", f"base{i}", f"replay{i}"))
277
+ if f"half{i}" in probes:
278
+ out.append((f"steps{i}", f"base{i}", f"half{i}"))
279
+ out += [(k, probes[k].vs, k) for k in ("seed", "prompt", "cfg")]
280
+ return [x for x in out if probes[x[1]].ok and probes[x[2]].ok and probes[x[1]].sha != probes[x[2]].sha]
281
+
282
+
283
+ def distances(probes: dict[str, Probe], plan: list[tuple[str, str, str]], python: str | None, work: Path,
284
+ timeout_s: float) -> tuple[dict | None, str | None]:
285
+ """{name: {"mad", "psnr", "frames_compared", "ok", "error"}} on decoded pixels, or (None, why) without the
286
+ worker."""
287
+ if not python:
288
+ return None, "no --python: clips were compared by bytes only"
289
+ if not plan:
290
+ return {}, None
291
+ doc = run_worker(python, {"task": "compare", "metrics": ["mad", "psnr"], "frame_stride": 1,
292
+ "pairs": [{"key": n, "a": str(probes[a].path), "b": str(probes[b].path)}
293
+ for n, a, b in plan]}, work, "distances", timeout_s)
294
+ out = {}
295
+ for it in doc["items"]:
296
+ mean = it.get("mean") or {}
297
+ out[it["key"]] = {"ok": bool(it.get("ok")) and mean.get("mad") is not None, "mad": mean.get("mad"),
298
+ "psnr": mean.get("psnr"), "frames_compared": it.get("frames_compared"),
299
+ "frames": [it.get("frames_a"), it.get("frames_b")], "error": it.get("error")}
300
+ return out, None
301
+
302
+
303
+ class Judge:
304
+ """Shared facts for the gates: probes by key, pixel distances, the same-seed A/A floor, and the probes run
305
+ right after one with the same prompt or image file (``clashes``)."""
306
+
307
+ def __init__(self, probes: dict[str, Probe], dist: dict | None, pairs: int, clashes: list | None = None):
308
+ self.p, self.dist, self.pairs, self.clashes = probes, dist, pairs, list(clashes or [])
309
+ self.floor_rows = []
310
+ for i in range(pairs):
311
+ a, b = probes[f"base{i}"], probes[f"replay{i}"]
312
+ if not (a.ok and b.ok):
313
+ continue
314
+ if a.sha == b.sha:
315
+ self.floor_rows.append({"pair": i, "identical_bytes": True, "mad": 0.0})
316
+ elif dist is not None and (dist.get(f"same{i}") or {}).get("ok"):
317
+ self.floor_rows.append({"pair": i, "identical_bytes": False, "mad": dist[f"same{i}"]["mad"],
318
+ "psnr": dist[f"same{i}"]["psnr"]})
319
+ else:
320
+ self.floor_rows.append({"pair": i, "identical_bytes": False, "mad": None})
321
+ known = [r["mad"] for r in self.floor_rows]
322
+ # The largest repeat distance; None when no repeat was judged or one could not be compared.
323
+ self.floor = max(known) if known and None not in known else None
324
+
325
+ def differs(self, name: str, a: str, b: str) -> dict:
326
+ """How clip b differs from clip a: same_bytes | within_floor | differs | small | bytes_only | missing."""
327
+ pa, pb = self.p[a], self.p[b]
328
+ row = {"name": name, "a": a, "b": b}
329
+ if not (pa.ok and pb.ok):
330
+ return {**row, "state": "missing"}
331
+ if pa.sha == pb.sha:
332
+ return {**row, "state": "same_bytes", "mad": 0.0}
333
+ if self.dist is None:
334
+ return {**row, "state": "bytes_only"}
335
+ d = self.dist.get(name) or {}
336
+ if not d.get("ok"):
337
+ return {**row, "state": "missing", "error": d.get("error") or "not compared"}
338
+ row.update({"mad": d["mad"], "psnr": d["psnr"]})
339
+ if self.floor is None:
340
+ return {**row, "state": "small" if d["mad"] <= MIN_MAD else "no_floor"}
341
+ if d["mad"] <= self.floor:
342
+ return {**row, "state": "within_floor"}
343
+ if d["mad"] > max(SENSITIVITY * self.floor, MIN_MAD):
344
+ return {**row, "state": "differs"}
345
+ return {**row, "state": "small"}
346
+
347
+
348
+ def _verdict_of(rows: list[dict]) -> tuple[str, list[dict]]:
349
+ """FAIL when any changed input left the clip the same; PASS when every one moved it clearly."""
350
+ bad = [r for r in rows if r["state"] in ("same_bytes", "within_floor")]
351
+ if bad:
352
+ return "FAIL", bad
353
+ if rows and all(r["state"] == "differs" for r in rows):
354
+ return "PASS", []
355
+ return "UNPROVEN", [r for r in rows if r["state"] != "differs"]
356
+
357
+
358
+ STATE_TEXT = {"same_bytes": "byte-identical clips", "within_floor": "no farther apart than the same request repeated",
359
+ "small": "the clips barely differ (not above 3x the repeat floor and 0.5 grey levels)",
360
+ "bytes_only": "bytes differ but pixels were not compared (pass --python)",
361
+ "no_floor": "no same-seed repeat to compare against", "missing": "a probe failed or was not compared"}
362
+
363
+
364
+ def _explain(rows: list[dict]) -> str:
365
+ return "; ".join(f"{r['b']} vs {r['a']}: {STATE_TEXT.get(r['state'], r['state'])}"
366
+ + (f" (MAD {r['mad']:.3g})" if r.get("mad") is not None else "") for r in rows[:4])
367
+
368
+
369
+ def _failed_note(probes: list[Probe]) -> str | None:
370
+ bad = [p for p in probes if not p.ok]
371
+ if not bad:
372
+ return None
373
+ first = next((p.res.error for p in bad if p.res is not None and p.res.error), "not sent")
374
+ return f"{len(bad)} probe(s) failed (first: {str(first)[:160]})"
375
+
376
+
377
+ # --------------------------------------------------------------------------- the gates
378
+
379
+ def gate_steps(j: Judge, steps: int) -> dict:
380
+ if steps < 2:
381
+ return _gate("steps_honoured", "NOT RUN", "the probe clip asks for fewer than 2 steps: no N/2 to compare")
382
+ probes = list(j.p.values())
383
+ counter = [{"key": p.key, "asked": p.req.steps, "steps_run": (p.res.effective or {}).get("steps_run")}
384
+ for p in probes if p.ok]
385
+ reported = [c for c in counter if c["steps_run"] is not None]
386
+ wrong = [c for c in reported if c["steps_run"] != c["asked"]]
387
+ rows = [j.differs(f"steps{i}", f"base{i}", f"half{i}") for i in range(j.pairs)]
388
+ out_v, out_bad = _verdict_of(rows)
389
+ ratios = []
390
+ for i in range(j.pairs):
391
+ a, h = j.p[f"base{i}"], j.p[f"half{i}"]
392
+ if a.e2e_ms and h.e2e_ms:
393
+ ratios.append({"pair": i, "e2e_ms": a.e2e_ms, "half_e2e_ms": h.e2e_ms, "ratio": a.e2e_ms / h.e2e_ms,
394
+ "denoise_ratio": _stage_ratio(a, h, "denoise")})
395
+ rs = [r["ratio"] for r in ratios]
396
+ med = _median(rs)
397
+ typical = bool(rs) and 3 * sum(1 for x in rs if x >= STEP_RATIO_PASS) >= 2 * len(rs)
398
+ metrics = {"step_time_ratio_median": med, "pairs": len(rs), "steps_reported": len(reported),
399
+ "steps_mismatch": len(wrong), "half_steps": steps // 2}
400
+ details = {"counter": counter, "outputs": rows, "timing": ratios}
401
+ lim = ["an engine that runs a fixed fraction of the steps it is asked for scales like an honest one here; "
402
+ "quality clip against the reference at the same steps catches it"]
403
+ note = _failed_note([j.p[k] for k in j.p if k.startswith(("base", "half"))])
404
+ if note:
405
+ lim.append(note)
406
+ if wrong:
407
+ return _gate("steps_honoured", "FAIL", f"the engine reports steps_run != asked on {len(wrong)} probe(s) "
408
+ f"(e.g. {wrong[0]['key']}: asked {wrong[0]['asked']}, ran {wrong[0]['steps_run']})",
409
+ metrics, details, lim)
410
+ if out_v == "FAIL":
411
+ return _gate("steps_honoured", "FAIL", f"N/2 steps gave the same clip as N steps: {_explain(out_bad)}",
412
+ metrics, details, lim)
413
+ if med is not None and med < STEP_RATIO_FAIL:
414
+ return _gate("steps_honoured", "FAIL", f"{steps} steps take {med:.3f}x the time of {steps // 2} (median "
415
+ f"of {len(rs)} pair(s)): the step count does not change the work done", metrics, details, lim)
416
+ problems = []
417
+ if not reported:
418
+ problems.append("the engine reports no steps_run (effective) to check")
419
+ if out_v != "PASS":
420
+ problems.append(f"outputs: {_explain(out_bad)}")
421
+ if med is None:
422
+ problems.append("no N vs N/2 pair was timed")
423
+ elif not (med >= STEP_RATIO_PASS and typical):
424
+ problems.append(f"time ratio N vs N/2 {med:.3f}x (median of {len(rs)}) is below {STEP_RATIO_PASS}x or not "
425
+ "in 2/3 of the pairs")
426
+ if problems:
427
+ return _gate("steps_honoured", "UNPROVEN", "; ".join(problems), metrics, details, lim)
428
+ return _gate("steps_honoured", "PASS", f"steps_run == asked on {len(reported)} probe(s); N/2 steps changes the "
429
+ f"clip; {steps} steps take {med:.2f}x the time of {steps // 2}", metrics, details, lim)
430
+
431
+
432
+ def _stage_ratio(a: Probe, b: Probe, stage: str) -> float | None:
433
+ sa, sb = (a.res.stage_ms or {}).get(stage), (b.res.stage_ms or {}).get(stage)
434
+ ok = all(isinstance(x, (int, float)) and not isinstance(x, bool) and x > 0 for x in (sa, sb))
435
+ return sa / sb if ok else None
436
+
437
+
438
+ def gate_changed(j: Judge, name: str, probe: str, what: str) -> dict:
439
+ """cfg_honoured / prompt_sensitivity: one changed input, same seed, must change the clip."""
440
+ a, b = j.p[j.p[probe].vs], j.p[probe]
441
+ rows = [j.differs(probe, a.key, probe)]
442
+ v, bad = _verdict_of(rows)
443
+ metrics = {"mad": rows[0].get("mad"), "psnr": rows[0].get("psnr"), "repeat_floor_mad": j.floor}
444
+ details = {"change": b.meta, "outputs": rows, "floor": j.floor_rows}
445
+ if name == "cfg_honoured" and a.e2e_ms and b.e2e_ms:
446
+ metrics["time_ratio"] = a.e2e_ms / b.e2e_ms
447
+ lim = [x for x in [_failed_note([a, b])] if x]
448
+ if v == "PASS":
449
+ return _gate(name, "PASS", f"{what} changed the clip (MAD {rows[0]['mad']:.3g} vs the repeat's "
450
+ f"{j.floor:.3g})", metrics, details, lim)
451
+ return _gate(name, v, f"{what}: {_explain(bad)}", metrics, details, lim)
452
+
453
+
454
+ def gate_seed(j: Judge) -> dict:
455
+ rows = [j.differs("seed", j.p["seed"].vs, "seed")]
456
+ v, bad = _verdict_of(rows)
457
+ mads = [r["mad"] for r in rows if r.get("mad") is not None and r["state"] != "same_bytes"]
458
+ metrics = {"repeat_floor_mad": j.floor, "different_seed_mad_min": min(mads) if mads else None,
459
+ "repeats": len(j.floor_rows), "different_seed_pairs": len(rows)}
460
+ details = {"floor": j.floor_rows, "different_seeds": rows}
461
+ lim = [x for x in [_failed_note([p for k, p in j.p.items() if k.startswith(("base", "replay")) or k == "seed"])]
462
+ if x]
463
+ same = [r for r in rows if r["state"] == "same_bytes"]
464
+ if same: # checked first: definitive whatever the floor is
465
+ return _gate("seed_honoured", "FAIL", f"a different seed did not change the clip: {_explain(same)}",
466
+ metrics, details, lim)
467
+ if j.floor is not None and mads and j.floor > REPRO_SHARE * min(mads):
468
+ return _gate("seed_honoured", "FAIL", f"the same seed does not reproduce the clip: the repeat is MAD "
469
+ f"{j.floor:.3g} from the first, a different seed {min(mads):.3g}", metrics, details, lim)
470
+ if v == "FAIL":
471
+ return _gate("seed_honoured", "FAIL", f"a different seed did not change the clip: {_explain(bad)}",
472
+ metrics, details, lim)
473
+ if v == "PASS" and j.floor is not None:
474
+ return _gate("seed_honoured", "PASS", f"different seeds change the clip (MAD >= {min(mads):.3g}); the "
475
+ f"same seed repeats within MAD {j.floor:.3g}", metrics, details, lim)
476
+ why = _explain(bad) if bad else "no same-seed repeat could be compared"
477
+ return _gate("seed_honoured", "UNPROVEN", why, metrics, details, lim)
478
+
479
+
480
+ def check_decoded(d: dict | None, asked: dict, audio: bool | None) -> list[str]:
481
+ """What the decoded clip gets wrong against the request (empty = all good)."""
482
+ if not d or not d.get("ok"):
483
+ return []
484
+ v, problems = d.get("video") or {}, []
485
+ if not v.get("frames"):
486
+ return ["no video frames decoded"]
487
+ if (v.get("width"), v.get("height")) != (asked["width"], asked["height"]):
488
+ problems.append(f"decoded {v.get('width')}x{v.get('height')}, asked {asked['width']}x{asked['height']}")
489
+ if len(v.get("sizes") or []) > 1:
490
+ problems.append(f"frame size changes inside the clip: {v['sizes'][:4]}")
491
+ if v["frames"] != asked["num_frames"]:
492
+ problems.append(f"decoded {v['frames']} frames, asked {asked['num_frames']}")
493
+ if v.get("fps") is not None and abs(v["fps"] - asked["fps"]) > FPS_TOL * asked["fps"]:
494
+ problems.append(f"decoded timestamps give {v['fps']:.4g} fps, asked {asked['fps']:g}")
495
+ if audio:
496
+ a = d.get("audio") or {}
497
+ want = asked["num_frames"] / asked["fps"]
498
+ if not a.get("duration_s"):
499
+ problems.append("audio was asked for but none was decoded")
500
+ elif abs(a["duration_s"] - want) > AUDIO_TOL_S + 1 / asked["fps"]:
501
+ problems.append(f"decoded audio is {a['duration_s']:.3f} s, the clip is {want:.3f} s")
502
+ return problems
503
+
504
+
505
+ def header_claims(d: dict | None) -> list[str]:
506
+ """Where the container header disagrees with what decoding gave (recorded, never trusted)."""
507
+ if not d or not d.get("ok"):
508
+ return []
509
+ hv, v, out = (d.get("header") or {}).get("video") or {}, d.get("video") or {}, []
510
+ for k in ("width", "height", "frames"):
511
+ if hv.get(k) is not None and v.get(k) is not None and hv[k] != v[k]:
512
+ out.append(f"header {k} {hv[k]} vs decoded {v[k]}")
513
+ return out
514
+
515
+
516
+ def gate_resolution(items: list[dict], decoder: str | None, why_not: str | None, left_out: int = 0) -> dict:
517
+ """items: {key, asked, audio, decoded, source, problems?, error?}: ``problems`` found before decoding (a
518
+ file that is not the timed clip) fail the clip; ``error`` says why a clip has no decode (no file)."""
519
+ if not items:
520
+ return _gate("resolution_frames", "NOT RUN", why_not or "no clip to decode")
521
+ rows, bad, undecoded = [], [], []
522
+ for it in items:
523
+ d = it["decoded"]
524
+ probs = list(it.get("problems") or []) + check_decoded(d, it["asked"], it["audio"])
525
+ row = {"key": it["key"], "source": it["source"], "problems": probs, "header_mismatch": header_claims(d),
526
+ "decoded": {k: (d.get("video") or {}).get(k) for k in ("width", "height", "frames", "fps")}
527
+ if d and d.get("ok") else None,
528
+ "audio_s": ((d or {}).get("audio") or {}).get("duration_s") if d and d.get("ok") else None}
529
+ if probs:
530
+ bad.append(row)
531
+ elif not d or not d.get("ok"):
532
+ why = (d or {}).get("error") or it.get("error") or (f"not decoded: {why_not}" if why_not else "not decoded")
533
+ undecoded.append({"key": it["key"], "error": why})
534
+ rows.append(row)
535
+ decoded = sum(1 for r in rows if r["decoded"] is not None)
536
+ metrics = {"clips_decoded": decoded, "clips_wrong": len(bad), "clips_undecoded": len(undecoded),
537
+ "clips_left_out": left_out}
538
+ lim = [f"decoded with {decoder}"] if decoder else []
539
+ if left_out:
540
+ lim.append(f"--max-decode: {left_out} timed clip(s) of the bench clip run were not decoded (a random subset "
541
+ "was, drawn from the probe seed)")
542
+ if bad:
543
+ return _gate("resolution_frames", "FAIL", f"{len(bad)} clip(s) are not what was asked (e.g. {bad[0]['key']}: "
544
+ f"{'; '.join(bad[0]['problems'])})", metrics, rows, lim)
545
+ if undecoded:
546
+ return _gate("resolution_frames", "UNPROVEN", f"{len(undecoded)} clip(s) could not be decoded (e.g. "
547
+ f"{undecoded[0]['key']}: {undecoded[0]['error'][:160]})", metrics, rows, lim)
548
+ return _gate("resolution_frames", "PASS", f"{decoded} decoded clip(s) match the asked size, frame count, fps"
549
+ + (" and audio length" if any(it["audio"] for it in items) else ""), metrics, rows, lim)
550
+
551
+
552
+ def gate_no_cache(j: Judge | None, threshold: float, static: dict | None) -> dict:
553
+ problems, unproven, rows, metrics, lim = [], [], [], {}, []
554
+ if j is not None:
555
+ live = list(j.p.values())
556
+ cached = [{"key": p.key, "cache": p.res.cache} for p in live if p.res is not None and p.res.cache]
557
+ if cached:
558
+ after = {b for _, b in j.clashes} & {c["key"] for c in cached}
559
+ problems.append(f"{len(cached)} probe(s) report stages served from a cache (e.g. {cached[0]['key']}: "
560
+ f"{cached[0]['cache'][:4]})" + (
561
+ f"; {len(after)} of them ran right after a probe with the same prompt or image file "
562
+ "(no order without such neighbours was found: use --pairs 2 or more)" if after else ""))
563
+ reused = [p.key for p in live if p.res is not None and p.res.cache_allowed]
564
+ if reused:
565
+ lim.append(f"{len(reused)} probe(s): the engine reused the encoding of its fixed negative prompt "
566
+ f"({cc.NEGATIVE_PROMPT_CACHE}: computed once, not per clip; allowed for every engine)")
567
+ sp = []
568
+ for i in range(j.pairs):
569
+ a, b = j.p[f"base{i}"], j.p[f"replay{i}"]
570
+ if a.e2e_ms and b.e2e_ms:
571
+ sp.append(a.e2e_ms / b.e2e_ms)
572
+ rows.append({"pair": i, "first_e2e_ms": a.e2e_ms, "replay_e2e_ms": b.e2e_ms,
573
+ "speedup": a.e2e_ms / b.e2e_ms, "same_bytes": a.sha == b.sha})
574
+ med = _median(sp)
575
+ most = bool(sp) and 3 * sum(1 for x in sp if x > 1 + threshold) >= 2 * len(sp)
576
+ metrics.update({"replay_speedup_median": med, "replay_pairs": len(sp), "reported_cached": len(cached),
577
+ "neighbour_clashes": len(j.clashes)})
578
+ if j.clashes:
579
+ lim.append(f"{len(j.clashes)} probe(s) ran right after one with the same prompt or image file (e.g. "
580
+ f"{j.clashes[0][0]} -> {j.clashes[0][1]}): an engine that keeps the last run's encodings "
581
+ "(ComfyUI) reuses them there; use --pairs 2 or more")
582
+ if med is not None and med > 1 + threshold and most:
583
+ problems.append(f"replays are {med:.2f}x faster than their first request (median of {len(sp)}; limit "
584
+ f"{1 + threshold:.2f}x)")
585
+ elif not sp:
586
+ unproven.append("no request and its replay both succeeded")
587
+ distinct = [p for p in live if p.kind != "replay" and p.ok]
588
+ dup = _duplicates({p.key: p.sha for p in distinct})
589
+ metrics["distinct_probe_clips"] = len(distinct)
590
+ if dup:
591
+ problems.append(f"different requests got byte-identical clips: {dup[:3]} (an output cache, or an input "
592
+ "the engine ignores)")
593
+ note = _failed_note(live)
594
+ if note:
595
+ unproven.append(note)
596
+ if static is not None:
597
+ metrics.update({"bench_clips": static["n"], "bench_duplicates": len(static["duplicates"]),
598
+ "bench_cached": len(static["cached"])})
599
+ if static["duplicates"]:
600
+ problems.append(f"bench clip: different requests got byte-identical clips: {static['duplicates'][:3]}")
601
+ if static["cached"]:
602
+ problems.append(f"bench clip: {len(static['cached'])} timed clip(s) report cached stages (e.g. "
603
+ f"{static['cached'][0]})")
604
+ details = {"replays": rows, "static": static, "neighbour_clashes": j.clashes if j is not None else None}
605
+ if j is None and static is None:
606
+ return _gate("no_cache", "NOT RUN", "no live probes and no --bench-evidence")
607
+ if problems:
608
+ return _gate("no_cache", "FAIL", "; ".join(problems), metrics, details, lim)
609
+ if unproven:
610
+ return _gate("no_cache", "UNPROVEN", "; ".join(unproven), metrics, details, lim)
611
+ parts = []
612
+ if j is not None:
613
+ parts.append(f"replays {metrics['replay_speedup_median']:.2f}x (limit {1 + threshold:.2f}x); "
614
+ f"{metrics['distinct_probe_clips']} distinct requests, distinct clips")
615
+ if static is not None:
616
+ parts.append(f"bench clip: {static['n']} timed clip(s), no duplicate or cached clip")
617
+ return _gate("no_cache", "PASS", "; ".join(parts), metrics, details, lim)
618
+
619
+
620
+ def _duplicates(shas: dict[str, str]) -> list[list[str]]:
621
+ groups: dict[str, list[str]] = {}
622
+ for k, s in shas.items():
623
+ groups.setdefault(s, []).append(k)
624
+ return [sorted(v) for v in groups.values() if len(v) > 1]
625
+
626
+
627
+ # --------------------------------------------------------------------------- a bench clip record
628
+
629
+ def load_bench(store: ev.Store, ref: str, run_dir: str | None = None) -> dict:
630
+ """Rows, the directories its clips may be in, and the asked audio of a bench clip record.
631
+
632
+ Refused when the record or its requests.jsonl was edited after capture: a gates record built from an
633
+ edited input would carry a fresh id that verifies."""
634
+ rec = store.load(ref)
635
+ data = rec.get("data") or {}
636
+ if data.get("source") != "bench clip":
637
+ raise OpError(f"--bench-evidence {ref}: not a bench clip record")
638
+ problem = ev.integrity_problem(rec)
639
+ if problem:
640
+ raise OpError(f"--bench-evidence: {problem}; refusing to check it")
641
+ root = store.run_dir(rec["evidence_id"])
642
+ art = next((a for a in rec.get("artifacts", []) if a.get("role") == "requests"), None)
643
+ path = root / art["path"] if art and isinstance(art.get("path"), str) else None
644
+ if path is None or not path.is_file():
645
+ raise OpError(f"--bench-evidence {ref}: its requests.jsonl is not in the store ({root})")
646
+ if file_sha256(path)[0] != art.get("sha256"):
647
+ raise OpError(f"--bench-evidence {ref}: its requests.jsonl does not match the sha256 the record lists "
648
+ "(edited after capture?); refusing to check it")
649
+ rows = [json.loads(x) for x in path.read_text().splitlines() if x.strip()]
650
+ # Where the timed clips may be: this record's directory, those of the records it extends (each keeps the
651
+ # rounds it timed), and the run directories (bench clip's and this call's --run-dir).
652
+ roots = [root] + [store.run_dir(e) for e in data.get("outputs_from") or [] if isinstance(e, str)
653
+ and e.startswith("ev_") and "/" not in e and ".." not in e]
654
+ bench_run = ((data.get("request") or {}).get("args") or {}).get("run_dir")
655
+ for rd in (run_dir, bench_run): # records keep paths under the home directory as ~/...
656
+ d = Path(rd).expanduser() if isinstance(rd, str) and rd else None
657
+ if d is not None and d.is_dir() and d not in roots:
658
+ roots.append(d)
659
+ audio = (rec.get("identity") or {}).get("workload", {}).get("audio")
660
+ return {"record": rec, "root": root, "roots": roots, "rows": rows, "label": data.get("label"),
661
+ "hash_only": data.get("save_outputs") == "hash_only", "audio": audio if isinstance(audio, bool) else None}
662
+
663
+
664
+ def static_cache(bench: dict) -> dict:
665
+ """Different requests (clip id, seed) must never share a clip; no timed clip may report a cached stage."""
666
+ shas: dict[str, set] = {}
667
+ for r in bench["rows"]:
668
+ if r.get("ok") and r.get("out_sha256"):
669
+ shas.setdefault(r["out_sha256"], set()).add(f"{r['id']}@seed{r['seed']}")
670
+ dup = [sorted(v) for v in shas.values() if len(v) > 1]
671
+ cached = [f"{r['id']} r{r['round']}" for r in bench["rows"] if r.get("ok") and r.get("cache")]
672
+ return {"n": sum(1 for r in bench["rows"] if r.get("ok")), "duplicates": dup, "cached": cached}
673
+
674
+
675
+ def bench_clips(bench: dict, limit: int, rng: random.Random) -> tuple[list[dict], int]:
676
+ """The timed clips of a bench clip record to decode: [{key, path, asked, state, error}] with state ok (the
677
+ file has the row's sha256), changed (files found, none with it) or lost (no file), and how many timed
678
+ clips ``limit`` left out (0 = decode all; else a random ``limit`` of them)."""
679
+ timed = [r for r in bench["rows"] if r.get("ok") and r.get("out_sha256")]
680
+ chosen = timed
681
+ if limit and len(timed) > limit:
682
+ chosen = [timed[i] for i in sorted(rng.sample(range(len(timed)), limit))]
683
+ out = []
684
+ for r in chosen:
685
+ key = f"bench:r{r.get('round')}/{r.get('id')}"
686
+ rel = Path("outputs", str(bench["label"]), f"r{r.get('round')}", f"{r.get('id')}.{r.get('format')}")
687
+ it = {"key": key, "path": None, "asked": r.get("asked"), "state": "lost", "error": None}
688
+ if not (r.get("format") and isinstance(r.get("asked"), dict)) or rel.is_absolute() or ".." in rel.parts:
689
+ out.append({**it, "error": "the row names no usable clip file"})
690
+ continue
691
+ found = [root / rel for root in bench["roots"] if (root / rel).is_file()]
692
+ hit = next((f for f in found if file_sha256(f)[0] == r["out_sha256"]), None)
693
+ if hit is not None:
694
+ out.append({**it, "path": hit, "state": "ok"})
695
+ elif found:
696
+ out.append({**it, "state": "changed", "error": "clip file is not the clip that was timed (its sha256 "
697
+ "differs from the row's out_sha256)"})
698
+ else:
699
+ out.append({**it, "error": "no clip file kept (--save-outputs hash_only)" if bench["hash_only"] else
700
+ "no clip file in the record's store directory, the records it extends or the run directory"})
701
+ return out, len(timed) - len(chosen)
702
+
703
+
704
+ # --------------------------------------------------------------------------- the operation
705
+
706
+ @operation("gates_clip", ("gates", "clip"), "Anti-cheat gates for a video clip engine: steps and guidance honoured, "
707
+ "decoded resolution / frame count / fps / audio length, seeds and prompts change the clip, no cached "
708
+ "outputs (replay timing, unique clips); optionally the timed clips of a bench clip record.",
709
+ [Arg("base_url", "string", "Engine root, e.g. http://127.0.0.1:8000 (the live gates need it). Never "
710
+ "stored: records keep an HMAC of it (as bench clip does).", sensitive=True),
711
+ Arg("clips", "string", "Clip set JSONL (as bench clip): the probes use one of its clips."),
712
+ Arg("clip_id", "string", "Clip of the set the probes are built from (default: the first)."),
713
+ Arg("endpoint_kind", "string", "clip_http (POST /v1/clips) or comfyui (needs --workflow).",
714
+ default="clip_http", choices=cc.ENDPOINT_KINDS),
715
+ Arg("workflow", "string", "comfyui: the API-format workflow JSON."),
716
+ Arg("label", "string", "Engine label for the record (default: the endpoint kind)."),
717
+ Arg("model", "string", "Model id for the record's identity."),
718
+ Arg("engine_version", "string", "Engine version, recorded in the identity."),
719
+ Arg("bench_evidence", "string", "Evidence id of a bench clip run (with --extend, its last record): "
720
+ "its timed clips, every round, are hash-checked, decoded and checked for duplicates and cached "
721
+ "stages."),
722
+ Arg("max_decode", "integer", "Decode at most this many of the bench clip run's clips, a random subset "
723
+ "drawn from the probe seed (0 = all).", default=0),
724
+ Arg("python", "string", "Python of the video pipeline's venv (PyAV + numpy): decodes clips and "
725
+ "compares their pixels. Without it, ffprobe decodes and clips are compared by bytes only."),
726
+ Arg("pairs", "integer", "Request/replay pairs (each also gets an N/2-steps probe).", default=2),
727
+ Arg("warmup", "integer", "Untimed warm-up clips at the probe shape before the probes.", default=1),
728
+ Arg("threshold", "number", "no_cache: largest allowed replay speedup - 1.", default=0.10),
729
+ Arg("seed", "integer", "Probe seed (default: drawn at random for every run and recorded; pass it only "
730
+ "to reproduce a run)."),
731
+ Arg("api_key_env", "string", "Name of the environment variable holding the API key (never stored)."),
732
+ Arg("timeout_s", "number", "Per-clip timeout, seconds.", default=cc.DEFAULT_TIMEOUT_S),
733
+ Arg("run_dir", "string", "Run directory: the record is also copied to <run-dir>/evidence/.")])
734
+ def gates_clip(call, base_url, clips, clip_id, endpoint_kind, label, model, engine_version, bench_evidence,
735
+ max_decode, python, pairs, warmup, threshold, seed, api_key_env, timeout_s, workflow, run_dir):
736
+ if not base_url and not bench_evidence:
737
+ raise OpError("give --base-url (live probes, with --clips) and/or --bench-evidence (a bench clip run)")
738
+ if base_url and not clips:
739
+ raise OpError("--base-url needs --clips (the clip set the probes are built from)")
740
+ if pairs < 1:
741
+ raise OpError("pairs must be >= 1")
742
+ if warmup < 0 or max_decode < 0:
743
+ raise OpError("warmup and max_decode must be >= 0")
744
+ if endpoint_kind == "comfyui" and base_url and not workflow:
745
+ raise OpError("--endpoint-kind comfyui needs --workflow (the API-format workflow JSON)")
746
+ label = label or endpoint_kind
747
+ store = ev.Store()
748
+ bench = load_bench(store, bench_evidence, run_dir) if bench_evidence else None
749
+ rcall, ep, clipset, clip = call, None, None, None
750
+ if base_url:
751
+ clipset = bc.load_clip_set(clips)
752
+ clip = next((c for c in clipset.clips if c["id"] == clip_id), None) if clip_id else clipset.clips[0]
753
+ if clip is None:
754
+ raise OpError(f"--clip-id {clip_id!r} is not in the clip set")
755
+ try:
756
+ ep = Endpoint.parse(base_url, api_key_from_env(api_key_env))
757
+ except ValueError as e:
758
+ raise OpError(str(e)) from None
759
+ rcall, _ = _record_call(call, ep)
760
+ probe_seed = seed if seed is not None else secrets.randbits(31)
761
+ rng = random.Random(probe_seed)
762
+
763
+ with store.staging_dir() as staging:
764
+ probes: dict[str, Probe] = {}
765
+ warm, judge, dist, why_bytes, seq = [], None, None, None, []
766
+ if ep is not None:
767
+ try:
768
+ backend = cc.make_backend(endpoint_kind, ep, timeout_s=timeout_s, workflow=workflow)
769
+ except (ValueError, LookupError) as e:
770
+ raise OpError(str(e)) from None
771
+ try:
772
+ status, detail = backend.health()
773
+ if status != 200:
774
+ raise OpError(f"engine not ready: GET {cc.HEALTH_PATH} answered {status or 'nothing'} ({detail})")
775
+ planned = plan_probes(clip, clipset, rng, pairs)
776
+ probes = {p.key: p for p in planned}
777
+ for i in range(warmup):
778
+ wseed = rng.getrandbits(31)
779
+ wreq = warmup_request(planned[0].req, i, wseed, planned)
780
+ res = backend.generate(wreq)
781
+ warm.append({"seed": wseed, "prompt": wreq.prompt, "ok": res.ok, "e2e_ms": res.e2e_ms,
782
+ "error": res.error})
783
+ seq = order(planned, rng)
784
+ run_probes(backend, seq, staging, label)
785
+ finally:
786
+ backend.close()
787
+ live_ok = any(p.ok for p in probes.values())
788
+ files, asked = [], {}
789
+ for p in probes.values():
790
+ if p.ok:
791
+ files.append((p.key, p.path))
792
+ asked[p.key] = ({"width": p.req.width, "height": p.req.height, "num_frames": p.req.num_frames,
793
+ "fps": p.req.fps}, p.req.audio, "probe")
794
+ # Its own generator, so the live probes do not depend on whether a bench record was given.
795
+ bench_items, left_out = bench_clips(bench, max_decode, random.Random(f"bench:{probe_seed}")) if bench \
796
+ else ([], 0)
797
+ for it in bench_items:
798
+ if it["state"] == "ok":
799
+ files.append((it["key"], it["path"]))
800
+ asked[it["key"]] = (it["asked"], bench["audio"], "bench clip")
801
+ decoded, decoder, why_not = {}, None, None
802
+ if files: # the probes are done: a decoder that fails must not throw them away
803
+ try:
804
+ decoded, decoder = probe_clips(files, python, staging / "worker", timeout_s)
805
+ except OpError as e:
806
+ why_not = str(e)
807
+ if python and which("ffprobe"):
808
+ decoded, decoder = probe_clips(files, None, staging / "worker", timeout_s)
809
+ decoder += f" (the --python worker failed: {why_not[:200]})"
810
+ if live_ok:
811
+ try:
812
+ dist, why_bytes = distances(probes, distance_plan(probes, pairs), python, staging / "worker",
813
+ timeout_s)
814
+ except OpError as e:
815
+ dist, why_bytes = None, f"pixel comparison failed, clips compared by bytes only: {str(e)[:300]}"
816
+ judge = Judge(probes, dist, pairs, neighbour_clashes(seq))
817
+
818
+ gates = []
819
+
820
+ def live(g: dict) -> dict:
821
+ g["applicable"] = ep is not None
822
+ return g
823
+
824
+ not_run = _failed_note(list(probes.values())) or "no --base-url: live probes not run"
825
+ if judge is None:
826
+ for name in ("steps_honoured", "cfg_honoured"):
827
+ gates.append(live(_gate(name, "NOT RUN", not_run)))
828
+ else:
829
+ gates.append(live(gate_steps(judge, clip["steps"])))
830
+ gates.append(live(gate_changed(judge, "cfg_honoured", "cfg", f"guidance {clip['guidance_scale']:g} -> "
831
+ f"{probes['cfg'].meta['guidance_scale']:g}")))
832
+ res_items = [{"key": k, "asked": asked[k][0], "audio": asked[k][1], "source": asked[k][2],
833
+ "decoded": decoded.get(k)} for k, _ in files]
834
+ res_items += [{"key": it["key"], "asked": it["asked"] or {}, "audio": bench["audio"], "source": "bench clip",
835
+ "decoded": None, "error": it["error"],
836
+ "problems": [it["error"]] if it["state"] == "changed" else []}
837
+ for it in bench_items if it["state"] != "ok"]
838
+ if not res_items and bench is not None and why_not is None:
839
+ why_not = "the bench clip record has no timed clip (every clip failed?), so none was decoded"
840
+ g = gate_resolution(res_items, decoder, why_not, left_out)
841
+ g["applicable"] = bool(files) or bench is not None # a bench record's clips are expected to be decoded
842
+ gates.append(g)
843
+ if judge is None:
844
+ for name in ("seed_honoured", "prompt_sensitivity"):
845
+ gates.append(live(_gate(name, "NOT RUN", not_run)))
846
+ else:
847
+ gates.append(live(gate_seed(judge)))
848
+ gates.append(live(gate_changed(judge, "prompt_sensitivity", "prompt", "a one-word change")))
849
+ g = gate_no_cache(judge, threshold, static_cache(bench) if bench else None)
850
+ g["applicable"] = ep is not None or bench is not None
851
+ gates.append(g)
852
+ if why_bytes and judge is not None:
853
+ for g in gates:
854
+ if g["gate"] in LIVE:
855
+ g["limitations"].append(why_bytes)
856
+
857
+ log = [probe_row(p, decoded.get(p.key)) for p in probes.values()]
858
+ return _record(rcall, store, staging, gates, log=log, warm=warm, label=label, model=model,
859
+ engine_version=engine_version, endpoint_kind=endpoint_kind if ep else None, clipset=clipset,
860
+ clip=clip, probe_seed=probe_seed, decoder=decoder, dist=dist, judge=judge, bench=bench,
861
+ threshold=threshold, python_given=bool(python), seq=[p.key for p in seq])
862
+
863
+
864
+ def _metrics_of(gates: list[dict]) -> list[dict]:
865
+ out = []
866
+ for g in gates:
867
+ for k, v in g["metrics"].items():
868
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
869
+ continue
870
+ unit = ("x" if "ratio" in k or "speedup" in k else "levels" if "mad" in k else "dB" if "psnr" in k
871
+ else "count")
872
+ out.append(ev.metric(f"{g['gate']}.{k}", v, unit, "median" if "median" in k else
873
+ "min" if k.endswith("_min") else "value" if unit != "count" else "count"))
874
+ return out
875
+
876
+
877
+ def _record(rcall, store, staging, gates, *, log, warm, label, model, engine_version, endpoint_kind, clipset, clip,
878
+ probe_seed, decoder, dist, judge, bench, threshold, python_given, seq=()) -> dict:
879
+ verdict = overall(gates)
880
+ artifacts = []
881
+ if log:
882
+ p = staging / "probes.jsonl"
883
+ p.write_text("".join(json.dumps(ev.redact(x), ensure_ascii=False) + "\n" for x in log), encoding="utf-8")
884
+ artifacts.append(ev.artifact(p, staging, "probe requests and replies"))
885
+ files = {f"probes/{x['key']}.{x['format']}": x["out_sha256"] for x in log if x["ok"]}
886
+ idx = staging / "outputs" / label / "index.json"
887
+ idx.parent.mkdir(parents=True, exist_ok=True)
888
+ idx.write_text(json.dumps({"label": label, "files": dict(sorted(files.items()))}, indent=1) + "\n")
889
+ artifacts.append(ev.artifact(idx, staging, "outputs_index"))
890
+ for f in (staging / "worker").glob("*.results.json") if (staging / "worker").is_dir() else []:
891
+ f.unlink()
892
+ lim = [f"{g['gate']}: {x}" for g in gates for x in g["limitations"]]
893
+ if clip is not None:
894
+ lim.append(f"probes use one clip of the set ({clip['id']}, {clip['task']}); run again with --clip-id for "
895
+ "another task or shape")
896
+ if judge is not None and judge.pairs > 1:
897
+ lim.append(f"probe groups after the first use the clip's prompt plus '{PROBE_SUFFIX.format(n='N').strip()}'"
898
+ + (" and the clip's picture as another file" if clip and clip.get("task") == "i2v" else "")
899
+ + ", so no probe follows one with the same prompt or image file")
900
+ if clip is not None and clip.get("task") == "i2v" and log and \
901
+ image_variant(clipset.images[clip["id"]][0], "check") is None:
902
+ lim.append("the clip's image is not a PNG or JPEG file, so every probe sends the same file: an engine that "
903
+ "keeps the last run's image encoding (ComfyUI) reuses it and fails no_cache")
904
+ bad_warm = [w for w in warm if not w["ok"]]
905
+ if bad_warm:
906
+ lim.append(f"{len(bad_warm)} warm-up clip(s) failed ({bad_warm[0]['error']})")
907
+ missing = [g["gate"] for g in gates if g["verdict"] == "NOT RUN"]
908
+ summary = f"gates clip {verdict}: " + ", ".join(f"{g['gate']} {g['verdict']}" for g in gates)
909
+ workload = {"mode": "single", "batch": 1, "endpoint_kind": endpoint_kind}
910
+ if clip is not None:
911
+ workload.update({k: clip.get(k) for k in bc.SHAPE_KEYS})
912
+ workload["clip_set"] = clipset.identity()
913
+ workload["audio"] = clip.get("audio")
914
+ ident = ev.identity(engine={"name": label, "version": engine_version}, model={"id": model}, workload=workload)
915
+ data = {"verdict": verdict, "gates": {g["gate"]: g for g in gates}, "gates_format": FORMAT, "label": label,
916
+ "not_run_but_expected": [g["gate"] for g in gates if g["verdict"] == "NOT RUN" and g.get("applicable")],
917
+ "probe_seed": probe_seed, "probe_clip": clip["id"] if clip else None, "warmup": warm,
918
+ "probe_order": list(seq), "neighbour_clashes": judge.clashes if judge else None,
919
+ "decoder": decoder, "pixels_compared": dist is not None, "python_given": python_given,
920
+ "repeat_floor_mad": judge.floor if judge else None,
921
+ "bench_evidence": bench["record"]["evidence_id"] if bench else None,
922
+ "thresholds": {"replay_speedup_max": 1 + threshold, "step_ratio_pass": STEP_RATIO_PASS,
923
+ "step_ratio_fail": STEP_RATIO_FAIL, "sensitivity_x_floor": SENSITIVITY,
924
+ "min_mad": MIN_MAD, "repro_share": REPRO_SHARE, "fps_tol": FPS_TOL,
925
+ "audio_tol_s": AUDIO_TOL_S},
926
+ "definitions": {"mad": "mean absolute difference of decoded RGB24 pixels, 0-255, over the frames of both "
927
+ "clips", "repeat_floor_mad": "largest MAD between a request and its replay"}}
928
+ rec = ev.build(rcall, kind=KIND, claim_class="observed", summary=summary, identity_=ident,
929
+ metrics=_metrics_of(gates), artifacts=artifacts, missing=missing, limitations=lim, data=data)
930
+ return store.save(rec, staging)