cozy-eval 0.1.0__tar.gz

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 (81) hide show
  1. cozy_eval-0.1.0/.gitignore +11 -0
  2. cozy_eval-0.1.0/GATE.md +316 -0
  3. cozy_eval-0.1.0/LICENSE +21 -0
  4. cozy_eval-0.1.0/PKG-INFO +413 -0
  5. cozy_eval-0.1.0/PROVENANCE.md +260 -0
  6. cozy_eval-0.1.0/README.md +322 -0
  7. cozy_eval-0.1.0/calibration/banked-pairs.json +3224 -0
  8. cozy_eval-0.1.0/calibration/run_banked.py +183 -0
  9. cozy_eval-0.1.0/examples/bench_quickstart.py +112 -0
  10. cozy_eval-0.1.0/parity/analyze_h2h.py +70 -0
  11. cozy_eval-0.1.0/parity/h2h_results_20260727.json +427 -0
  12. cozy_eval-0.1.0/parity/make_fixtures.py +40 -0
  13. cozy_eval-0.1.0/parity/run_oracle.py +41 -0
  14. cozy_eval-0.1.0/pyproject.toml +72 -0
  15. cozy_eval-0.1.0/src/cozy_eval/__init__.py +106 -0
  16. cozy_eval-0.1.0/src/cozy_eval/__main__.py +3 -0
  17. cozy_eval-0.1.0/src/cozy_eval/_stats.py +99 -0
  18. cozy_eval-0.1.0/src/cozy_eval/backends/__init__.py +24 -0
  19. cozy_eval-0.1.0/src/cozy_eval/backends/distributional.py +95 -0
  20. cozy_eval-0.1.0/src/cozy_eval/backends/perceptual.py +76 -0
  21. cozy_eval-0.1.0/src/cozy_eval/backends/reference.py +171 -0
  22. cozy_eval-0.1.0/src/cozy_eval/backends/signal.py +245 -0
  23. cozy_eval-0.1.0/src/cozy_eval/bench/__init__.py +120 -0
  24. cozy_eval-0.1.0/src/cozy_eval/bench/catalog.py +52 -0
  25. cozy_eval-0.1.0/src/cozy_eval/bench/checklists/cozy_hard_eval_v1.json +1479 -0
  26. cozy_eval-0.1.0/src/cozy_eval/bench/checklists/hard_video_v1.json +235 -0
  27. cozy_eval-0.1.0/src/cozy_eval/bench/checklists/magicbrush_edit_v1.json +658 -0
  28. cozy_eval-0.1.0/src/cozy_eval/bench/decompose/__init__.py +76 -0
  29. cozy_eval-0.1.0/src/cozy_eval/bench/decompose/engine.py +312 -0
  30. cozy_eval-0.1.0/src/cozy_eval/bench/decompose/sets/compositional-v1_cases.json +1986 -0
  31. cozy_eval-0.1.0/src/cozy_eval/bench/decompose/sets/compositional-v1_checklists.json +3045 -0
  32. cozy_eval-0.1.0/src/cozy_eval/bench/decompose/sets/compositional-v1_prompts.json +872 -0
  33. cozy_eval-0.1.0/src/cozy_eval/bench/decompose/vocab.py +75 -0
  34. cozy_eval-0.1.0/src/cozy_eval/bench/device.py +29 -0
  35. cozy_eval-0.1.0/src/cozy_eval/bench/errors.py +47 -0
  36. cozy_eval-0.1.0/src/cozy_eval/bench/judge.py +70 -0
  37. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/__init__.py +25 -0
  38. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/adherence.py +560 -0
  39. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/data/niqe_pristine.npz +0 -0
  40. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/geneval.py +491 -0
  41. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/hpsv3.py +213 -0
  42. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/iqa.py +257 -0
  43. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/musiq.py +427 -0
  44. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/ocr.py +116 -0
  45. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/preference.py +262 -0
  46. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/similarity.py +192 -0
  47. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/temporal.py +177 -0
  48. cozy_eval-0.1.0/src/cozy_eval/bench/metrics/vqascore.py +162 -0
  49. cozy_eval-0.1.0/src/cozy_eval/bench/promptset.py +182 -0
  50. cozy_eval-0.1.0/src/cozy_eval/bench/promptsets/hard_eval_v1.json +295 -0
  51. cozy_eval-0.1.0/src/cozy_eval/bench/promptsets/hard_video_v1.json +185 -0
  52. cozy_eval-0.1.0/src/cozy_eval/bench/registry.py +446 -0
  53. cozy_eval-0.1.0/src/cozy_eval/bench/suite.py +449 -0
  54. cozy_eval-0.1.0/src/cozy_eval/bench/verdict.py +216 -0
  55. cozy_eval-0.1.0/src/cozy_eval/bench/video.py +460 -0
  56. cozy_eval-0.1.0/src/cozy_eval/benchmarks.py +340 -0
  57. cozy_eval-0.1.0/src/cozy_eval/cli.py +121 -0
  58. cozy_eval-0.1.0/src/cozy_eval/control.py +164 -0
  59. cozy_eval-0.1.0/src/cozy_eval/frames.py +126 -0
  60. cozy_eval-0.1.0/src/cozy_eval/gate.py +256 -0
  61. cozy_eval-0.1.0/src/cozy_eval/image.py +80 -0
  62. cozy_eval-0.1.0/src/cozy_eval/protocol.py +248 -0
  63. cozy_eval-0.1.0/tests/fixtures/oracle_musiq.json +14 -0
  64. cozy_eval-0.1.0/tests/fixtures/oracle_niqe.json +14 -0
  65. cozy_eval-0.1.0/tests/fixtures/scene.png +0 -0
  66. cozy_eval-0.1.0/tests/fixtures/scene_blur.png +0 -0
  67. cozy_eval-0.1.0/tests/fixtures/scene_noise.png +0 -0
  68. cozy_eval-0.1.0/tests/fixtures/whitenoise.png +0 -0
  69. cozy_eval-0.1.0/tests/test_bench_suite.py +354 -0
  70. cozy_eval-0.1.0/tests/test_decompose.py +182 -0
  71. cozy_eval-0.1.0/tests/test_gate.py +302 -0
  72. cozy_eval-0.1.0/tests/test_geneval.py +284 -0
  73. cozy_eval-0.1.0/tests/test_hpsv3.py +120 -0
  74. cozy_eval-0.1.0/tests/test_iqa.py +80 -0
  75. cozy_eval-0.1.0/tests/test_musiq.py +80 -0
  76. cozy_eval-0.1.0/tests/test_public_api.py +364 -0
  77. cozy_eval-0.1.0/tests/test_registry_extension.py +86 -0
  78. cozy_eval-0.1.0/tests/test_verdict.py +141 -0
  79. cozy_eval-0.1.0/tests/test_video.py +363 -0
  80. cozy_eval-0.1.0/tests/test_vqascore.py +85 -0
  81. cozy_eval-0.1.0/uv.lock +3326 -0
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+
11
+ .env
@@ -0,0 +1,316 @@
1
+ # The gate protocol
2
+
3
+ This is the document a producer lane follows verbatim. It defines what to render,
4
+ which metrics are admissible, what the thresholds are, and where those thresholds
5
+ came from. Deviating from it silently is how a wrong verdict ships.
6
+
7
+ ---
8
+
9
+ ## 1. Pick the lane. This is not optional.
10
+
11
+ A quality comparison between two arms is meaningful only once you have decided
12
+ what the change did to the **sampling trajectory**.
13
+
14
+ | The change… | Lane | Admissible metrics | Valid at |
15
+ |---|---|---|---|
16
+ | leaves the latent trajectory identical and alters only what happens after it — VAE decode dtype, decoder tiling, colour conversion, output resize, video encoder, container mux | `same-trajectory` | PSNR, SSIM, LPIPS, VMAF (reference metrics) | n = 1 |
17
+ | perturbs the trajectory — weight/activation quantization, weight storage cast, `torch.compile`, attention backend, scheduler, step count, guidance, LoRA attach/fuse, denoiser dtype, offload placement, hardware SKU, new weights | `population` | no-reference statistics only, over a paired prompt set | n ≥ 8 |
18
+
19
+ `classify_change()` implements the table; `require_reference_lane()` raises
20
+ `TrajectoryPerturbingError` rather than returning a number. One perturbing change
21
+ contaminates a mixed set.
22
+
23
+ ### Why the refusal exists
24
+
25
+ Measured CPU-only on banked LTX-2.3 renders at 1920×1088 (`~/cozy/samples/w8a8-audit/`):
26
+
27
+ 1. **The no-op consumes the whole budget.** A compile-only control — zero
28
+ quantization change — scores LPIPS **0.196–0.249** against a fleet fp8 budget
29
+ of 0.25.
30
+ 2. **It is divergence, not drift.** The distance is already **0.29–0.41 at frame 0**
31
+ and flat-to-falling across the clip. Accumulating numerical error grows; a
32
+ different take starts far apart and stays there.
33
+ 3. **The ranking inverts.** An fp8-storage-cast arm and an unscaled-w8a8 arm carry
34
+ *identical weight bytes*, but the cast arm computes its GEMMs in bf16 and is
35
+ therefore strictly the more accurate path. It scores **0.3875 vs 0.3052** —
36
+ worse. A metric that ranks a strictly-better arm below a strictly-worse one
37
+ cannot choose between arms.
38
+
39
+ The same logic applies to images; it is only less visible there, because one
40
+ 1024² frame diverges less than 121 frames do.
41
+
42
+ ---
43
+
44
+ ## 2. Render conditions (population lane)
45
+
46
+ Both arms MUST share all of these, and the protocol stamp MUST record them:
47
+
48
+ * **n ≥ 8 paired prompts.** Hard floor 6; below 6 the distributional benchmark is
49
+ not evaluated at all. At n = 1 the *shipped-clean* LTX w8a8 recipe scores an
50
+ imaging index of **0.934** on one prompt and **1.005** over eight. One prompt
51
+ measures the take.
52
+ * **Same seeds, same prompt set, same order.** The prompts do not need to be the
53
+ te#79 fixed set, but they must be fixed for the family and reused across runs.
54
+ * **Same pod.** Host CPU and GPU variant move these numbers on their own
55
+ (an H100 PCIe and an H100 SXM are not interchangeable). Cross-pod arms return
56
+ `INDETERMINATE`.
57
+ * **Same execution lane.** Both arms compiled, or both eager — never one of each.
58
+ Compile alone perturbs the trajectory, so a compiled-candidate-vs-eager-reference
59
+ comparison measures compile plus quantization and cannot attribute either.
60
+ **The arms must be the ones you will serve**: if production serves compiled,
61
+ gate compiled.
62
+ * **Native production resolution and step count.** A 512² proxy understates native
63
+ damage by roughly 2.5× on image families; there is no reason to expect video to
64
+ be kinder.
65
+ * Frames are scored **pre-encode** where possible. Pass decoded frames straight to
66
+ `score_clip`; the encoder is itself a `same-trajectory` change and does not
67
+ belong inside a quantization verdict.
68
+
69
+ ---
70
+
71
+ ## 3. The three benchmarks
72
+
73
+ Each answers a question the other two cannot. Section 5 shows a degradation that
74
+ only one of them catches, for each of the three.
75
+
76
+ ### B1 — Imaging (per-frame, no-reference)
77
+
78
+ Six per-frame statistics, split into **detail** (Laplacian variance, spectral
79
+ high-frequency ratio above quarter-Nyquist, local contrast) and **tone** (luma
80
+ standard deviation, RGB saturation, 64-bin luma histogram entropy). Each is taken
81
+ as a ratio to the reference arm and aggregated as the **median across prompts**,
82
+ so one divergent take cannot drive the verdict.
83
+
84
+ ```
85
+ imaging_index = detail_retention^(2/3) · tonal_retention^(1/3)
86
+ ```
87
+
88
+ Catches: softening, detail loss, flattening/greying, and — via the upper bound —
89
+ noise injection and over-sharpening.
90
+
91
+ ### B2 — Temporal
92
+
93
+ * `jerk_ratio` = mean |L[t] − 2L[t−1] + L[t−2]| / mean |L[t] − L[t−1]|. Smooth
94
+ motion keeps this low whatever the motion magnitude; per-frame instability
95
+ raises it. **The tightest statistic in the suite**: per-prompt σ_log = 0.0139 on
96
+ the clean population.
97
+ * `flicker` = frame-mean luma standard deviation as a percentage of the mean.
98
+ * `shimmer`, `motion_energy` — advisory diagnostics, not budgets (see §6).
99
+
100
+ Catches: exposure flicker, frame-to-frame jitter, motion collapse.
101
+
102
+ ### B3 — Distributional fidelity
103
+
104
+ A **paired test across the prompt set**: per-metric paired *t* on log-ratios with
105
+ Holm-Bonferroni correction, gated additionally on a ≥2 % practical effect, plus a
106
+ paired per-frame Fréchet distance on the six signal features whitened by the
107
+ reference population.
108
+
109
+ Catches: consistent population-level shifts too small to move any single index,
110
+ and — by construction — it is the only benchmark that cannot be fooled by take
111
+ noise, because it asks whether the shift is *consistent across prompts*.
112
+
113
+ **Why not FVD / JEDi.** A deep-feature Fréchet distance is a population statistic
114
+ whose estimator bias dominates at n in the single digits; published FVD uses
115
+ thousands of clips. A per-artifact producer gate cannot afford that. Because the
116
+ prompt set here is *identical between arms*, content is differenced out by
117
+ construction and a paired test has real power at n = 8. `[distributional]` ships
118
+ `frechet()` for lanes that can afford n ≥ 64, and it refuses below that.
119
+
120
+ ---
121
+
122
+ ## 4. Thresholds and their provenance
123
+
124
+ Calibrated on the pairs in §5. Clean reference population throughout:
125
+ **LTX-2.3-distilled, w8a8-pcs compiled vs bf16 compiled, n = 8 prompts,
126
+ 1280×704 × 121 f, 8 steps, H100-80GB-HBM3, both arms compiled, same pod.**
127
+
128
+ | Budget | Value | Clean measures | Nearest failing case |
129
+ |---|---|---|---|
130
+ | `imaging_index` | 0.92 ≤ x ≤ 1.25 | 1.005 | 0.869 (Wan 4-step per-tensor fp8, same-pod ref); 1.645 (σ=0.02 pixel noise) |
131
+ | `imaging_worst_prompt` | ≥ 0.85 | 0.941 (worst of 8) | 0.647 (Wan fp8 population's worst prompt) |
132
+ | `jerk_excess` | ≤ +0.04 | +0.013 | +0.056 (2 % per-frame gain jitter); +0.053 (animegen 4-step mush); +0.104 (pixel noise) |
133
+ | `flicker_ratio` | ≤ 1.25 | 1.043 | **nothing banked trips it** — see below |
134
+ | `significant_features` | = 0 | 0 | 1 (8 % desaturation); 3 (25 % blur blend) |
135
+ | `population_frechet` | ≤ 0.20 | 0.068 | 0.286 (25 % blur blend); 4.792 (pixel noise) |
136
+
137
+ **Same-trajectory lane** — anchored on a real post-latent change: x264 re-encode
138
+ of banked LTX clips at CRF 18/23/28/34, measuring 40.6–44.4 / 37.6–42.4 /
139
+ 33.9–38.7 / 30.4–35.2 dB mean PSNR.
140
+
141
+ | Budget | Value | Rationale |
142
+ |---|---|---|
143
+ | `psnr_mean` | ≥ 36 dB | passes a CRF-23-class change, fails CRF-28-class |
144
+ | `psnr_worst` | ≥ 32 dB | worst frame at CRF 23 = 35.4 dB, at CRF 28 = 32.2 dB |
145
+ | `ssim_mean` | ≥ 0.97 | **provisional** — windowed SSIM barely moves on these anchors; PSNR is operative |
146
+
147
+ **`jerk_excess` is the operative temporal statistic; `flicker_ratio` is a wide
148
+ backstop.** The 2 % gain-jitter control raises flicker to 1.248 — inside the 1.25
149
+ budget — and is caught on jerk (+0.056) instead. The budget is not tightened to make
150
+ the control fail: `flicker`'s per-prompt spread on the clean population is
151
+ σ_log = 0.18, an order of magnitude looser than jerk's 0.0139, so a threshold that
152
+ would trip on 1.248 would be about 2σ from the clean median and start failing clean
153
+ arms. Flicker earns its place by catching gross exposure instability, not marginal
154
+ cases.
155
+
156
+ Sensitivity floors, stated honestly: the temporal axis resolves ~1.5 % frame-to-frame
157
+ gain noise (1 % jitter passes at jerk +0.022, 2 % fails at +0.056). The imaging axis
158
+ resolves roughly a 25 % blur blend. The distributional axis resolves an 8 %
159
+ saturation shift held consistently across 8 prompts.
160
+
161
+ `population_frechet` is fixed against **one** clean population and is the weakest
162
+ threshold here. Re-estimate it as more clean arms bank.
163
+
164
+ ---
165
+
166
+ ## 4b. The null control — which budgets is this family allowed to be judged on?
167
+
168
+ Every threshold in §4 was fixed on ONE family at one resolution. A threshold is
169
+ only a threshold where the *clean* population sits comfortably inside it, and
170
+ that is a per-family fact. **Measure it; do not assume it.**
171
+
172
+ A **null control** is an arm with identical weights and different seeds. Zero
173
+ model change, so anything it trips is take spread. Declare it with
174
+ `ChangeKind.SEED` — the only change a control may carry — and run it through
175
+ `measure_null_control()`:
176
+
177
+ ```python
178
+ control = measure_null_control(control_pairs, control_protocol) # ChangeKind.SEED
179
+ report = run_population_gate(pairs, protocol, null_control=control)
180
+ ```
181
+
182
+ The gate then reports a budget the control trips as **disregarded** — still
183
+ measured, still printed, never decisive — fails only on budgets the control
184
+ proved trustworthy, and ceilings the verdict at `INDETERMINATE` whenever
185
+ anything was disregarded, because a PASS on partial evidence is not a PASS.
186
+
187
+ Measured, 2026-07-27, two image families, n = 8 each, one control arm apiece:
188
+
189
+ | Budget | null controls measured | Transfers? |
190
+ |---|---|---|
191
+ | `significant_features` = 0 | 0 and 0 | **yes** — and it is what caught a real over-sharpening arm |
192
+ | `imaging_index` 0.92–1.25 | 1.0087 and 1.0025 | **yes**, and tight |
193
+ | `imaging_worst_prompt` ≥ 0.85 | 0.9182 and **0.6102** | family-dependent |
194
+ | `population_frechet` ≤ 0.20 | **0.4688** and **1.3275** | **no** — 2.3× and 6.6× the budget at zero model change |
195
+
196
+ Without the control, the second family reads a confident `FAIL` on two budgets
197
+ its own zero-change arm fails harder. That is a threshold-transfer artifact, and
198
+ demoting an artifact on it is exactly the class of wrong verdict this library
199
+ exists to refuse. **Carry a control arm on any family outside the calibrated
200
+ envelope of §4 — it is one extra arm on a pod you already bought** (measured:
201
+ $0.13 of a $0.38 pod).
202
+
203
+ ---
204
+
205
+ ## 4c. Degenerate arms: NO SIGNAL is not a FAIL
206
+
207
+ A candidate whose frames are a constant fill — uniformly black, white or flat —
208
+ scores an imaging index of 0.0 and an unbounded Fréchet distance against any real
209
+ reference. Those numbers are arithmetic, not evidence: they invite ranking one
210
+ broken arm against another. Both population gates detect it up front and return
211
+ `DEGENERATE` with no benchmark numbers at all. A degenerate *reference* is
212
+ refused for the same reason — every statistic here is a ratio to it.
213
+
214
+ Real case: a Wan 2.2 per-row fp8 arm that emitted 8/8 uniformly black clips
215
+ previously returned `FAIL`, `imaging_index 0.0000`, `population_frechet 13001`.
216
+ It now returns `DEGENERATE`. Look at the render; do not rank the arm.
217
+
218
+ The same-trajectory lane is deliberately NOT covered: there, the reference render
219
+ is the same take, so PSNR against it measures the blackout correctly and a FAIL
220
+ is the right, non-misleading verdict.
221
+
222
+ ---
223
+
224
+ ## 5. Validation table
225
+
226
+ `imaging_index` / worst-prompt / `jerk_excess` / `flicker_ratio` /
227
+ significant-feature count / paired Fréchet. Verdict letters are
228
+ imaging·temporal·distributional; `-` means not evaluated below the n ≥ 6 floor.
229
+ Regenerate with `python calibration/run_banked.py --samples-root …`; the evidence
230
+ is `calibration/banked-pairs.json`.
231
+
232
+ | Pair | Expected | IMG | wIMG | jerkX | flick | nsig | pFQD | Verdict |
233
+ |---|---|---|---|---|---|---|---|---|
234
+ | LTX 2.3 w8a8-pcs compiled vs bf16 compiled (n=8) | CLEAN | **1.005** | 0.941 | +0.013 | 1.043 | 0 | 0.068 | `PPP` |
235
+ | ↳ the same recipe judged on ONE prompt | CLEAN (trap) | 0.934 | 0.934 | −0.015 | 1.014 | – | – | `PP-` indeterminate |
236
+ | LTX compile-only control (no quant at all) | CLEAN control | 0.980 | 0.980 | +0.000 | 0.993 | – | – | `PP-` indeterminate |
237
+ | LTX fp8-storage cast (demoted rung) | observation | 0.957 | 0.957 | −0.011 | 1.157 | – | – | `PP-` indeterminate |
238
+ | Wan 2.2 4-step per-tensor fp8 (n=2) | **DEGRADED** | **0.727** | 0.647 | +0.018 | 0.899 | – | – | **`FP-`** |
239
+ | Wan 2.2 4-step per-tensor fp8, same-pod ref | **DEGRADED** | **0.869** | 0.869 | −0.001 | 0.786 | – | – | **`FP-`** |
240
+ | Wan 2.2 12-step per-tensor fp8 | CLEAN | 1.009 | 1.009 | +0.005 | 1.004 | – | – | `PP-` |
241
+ | animegen 4-step double-shift mush | **SEVERELY BAD** | **0.626** | 0.626 | **+0.053** | 0.706 | – | – | **`FF-`** |
242
+ | animegen 8-step double-shift | near-null | 1.038 | 1.038 | −0.009 | 0.881 | – | – | `PP-` |
243
+ | animegen 12-step double-shift | near-null | 1.038 | 1.038 | +0.008 | 0.951 | – | – | `PP-` |
244
+ | Wan naive timestep grid | bad, **semantic** | 0.976 | 0.976 | +0.009 | 0.837 | – | – | `PP-` **misses it — §6** |
245
+ | SYNTHETIC soften, 50 % blur blend (n=8) | imaging axis | **0.748** | 0.690 | −0.022 | 1.000 | 3 | 1.218 | `FPF` |
246
+ | SYNTHETIC soften, 25 % blur blend (n=8) | imaging axis | **0.869** | 0.841 | −0.011 | 1.000 | 3 | 0.286 | `FPF` |
247
+ | SYNTHETIC desaturate to 92 % (n=8) | tonal axis | 0.991 | 0.991 | +0.000 | 1.000 | **1** | 0.008 | **`PPF`** |
248
+ | SYNTHETIC 2 % per-frame gain jitter (n=8) | temporal axis | 0.998 | 0.996 | **+0.056** | 1.248 | 0 | 0.006 | **`PFP`** |
249
+ | SYNTHETIC 1 % per-frame gain jitter (n=8) | below floor | 0.999 | 0.998 | +0.022 | 1.068 | 0 | 0.001 | `PPP` |
250
+ | SYNTHETIC σ=0.02 per-pixel noise (n=8) | mixed | **1.645** | 1.161 | +0.104 | 0.996 | 3 | 4.790 | `FFF` |
251
+
252
+ **Separation margins.** Clean population 1.005 vs the tightest real degraded
253
+ observation 0.869 → the 0.92 threshold sits +9.0 % above the clean measurement and
254
+ −5.6 % below the nearest failure. On the temporal axis, +0.013 clean vs +0.053
255
+ failing, threshold +0.04: +0.027 / −0.013.
256
+
257
+ **Each benchmark catches something the others miss** — the whole reason there are
258
+ three:
259
+
260
+ * **imaging only** (`FP-`) — the Wan 4-step per-tensor fp8 arm. Index 0.727, jerk
261
+ +0.018 which is *inside* the clean band, and the population test cannot run at
262
+ n=2. This is the real, banked, production-relevant failure.
263
+ * **temporal only** (`PFP`) — 2 % per-frame gain jitter. Imaging index 0.998, zero
264
+ significant paired features, Fréchet 0.006 — all three pass — and `jerk_excess`
265
+ +0.056 fails.
266
+ * **distributional only** (`PPF`) — an 8 % desaturation held consistently across all
267
+ eight prompts. Imaging index 0.991 and jerk +0.000 both pass comfortably; the
268
+ paired test flags `saturation` at Holm p < 0.05 with a 2.7 % effect.
269
+
270
+ ---
271
+
272
+ ## 6. What this gate does NOT measure
273
+
274
+ State these to anyone reading a PASS.
275
+
276
+ * **Semantic and compositional failure.** The banked Wan naive-timestep-grid pair
277
+ is a real bug — it produces *background-figure cloning* and a flatter grade —
278
+ and this gate passes it (index 0.976, every axis silent). Signal statistics
279
+ cannot see a duplicated subject, a dropped prompt element, or text that renders
280
+ as gibberish. That needs a CLIP/VLM-class scorer or a human, and it is a fourth
281
+ axis, deliberately out of scope here.
282
+ * **Absolute quality.** Every number is a ratio to a reference arm. A gate PASS
283
+ says "no worse than the reference", never "good".
284
+ * **Aesthetics and prompt adherence.**
285
+ * **Motion energy and shimmer** are reported but not budgeted: both are strongly
286
+ content-driven (motion σ_log = 0.095 per prompt on the clean population) and
287
+ shimmer also falls under plain softening, so neither separates its own axis.
288
+ They appear as advisory notes.
289
+ * **Audio**, for families that generate it.
290
+ * Thresholds are calibrated on **LTX-2.3 and Wan-2.2 class video at 720p–1080p,
291
+ 4–12 steps**. A family far outside that (very low resolution, very long
292
+ clips, heavily stylised flat-shaded output) should re-derive them from its own
293
+ clean population before trusting a PASS.
294
+
295
+ ---
296
+
297
+ ## 7. Producer lane checklist
298
+
299
+ 1. Declare every `ChangeKind` the candidate arm introduces. Do not guess; if the
300
+ change is not in the enum, add it and decide its lane deliberately.
301
+ 2. If the lane is `same-trajectory`: `run_reference_gate`, n ≥ 1, done.
302
+ 3. Otherwise render **both arms** on one pod, same seeds, ≥ 8 prompts, both in the
303
+ serving execution lane, at production resolution and step count.
304
+ 4. Outside §4's calibrated envelope — any image family, any family far from
305
+ LTX/Wan-class 720p–1080p video — render a **third arm on the same pod**: the
306
+ same checkpoint at seeds + 1, and `measure_null_control(...)` it (§4b).
307
+ 5. `score_pairs(...)` → `run_population_gate(pairs, protocol, null_control=control)`.
308
+ 6. Persist `report.to_dict()` next to the artifact. It contains the verdict, every
309
+ measured value, every budget with its provenance, which budgets were
310
+ disregarded and why, the control's own numbers, and the protocol stamp.
311
+ `ClipScore.to_dict()` / `from_dict()` persist the per-clip scores losslessly if
312
+ you want to re-gate without re-rendering.
313
+ 7. `INDETERMINATE` is not a pass, and neither is `DEGENERATE`. Never promote an
314
+ artifact on either.
315
+ 8. A FAIL is a **verdict**, not a fault: publish the report, keep the artifact
316
+ staged, do not promote it.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cozy Creator and Paul Fidika
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.