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,920 @@
1
+ """Render a loaded run (``rundir.Run``) as one self-contained, offline HTML page.
2
+
3
+ Page shape:
4
+ * a summary table: per engine, speed per bucket, % of ceiling, quality and gate
5
+ verdicts, cost, agreement with the reference and how many cells were NOT RUN;
6
+ * filters: bucket, engine, "only rows that differ";
7
+ * one table per bucket: rows are inputs, columns are the reference then the engines.
8
+ LLM cells show the text with the first divergence from the reference marked and the
9
+ word diff highlighted (token-level position when both sides carry token ids). STT
10
+ cells show word errors against the ground truth; TTS cells show an audio player and
11
+ the ASR transcript's word errors. Video cells show the clip in a player beside the
12
+ reference clip (one table per round, so every clip in a row had the same seed), with
13
+ the client-timed clip time, the engine's self-reported stage times and shape, and
14
+ PSNR/SSIM/LPIPS when quality clip measured them. A missing output is a NOT RUN cell,
15
+ never a blank.
16
+
17
+ Safety: every piece of model, input or file-derived text goes through ``esc``; the
18
+ script reads only data-* attributes the page wrote itself; no external URL is loaded
19
+ (audio and video links are relative to the page). Claim classes: numbers read from metrics
20
+ files are marked observed (o); numbers this page computes (medians across inputs, divergence
21
+ positions, word error rates) are marked derived (d); what an engine says about itself (a
22
+ clip's stage times, steps and shape) is marked self-reported (s).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import html
28
+ import os
29
+ import statistics
30
+ from urllib.parse import quote
31
+
32
+ from inferencekit.review.diff import Alignment, align_words, first_token_divergence, word_diff
33
+ from inferencekit.review.rundir import ROUND_DIR, Cell, Item, Run
34
+
35
+ # Speed columns per modality: (metric key aliases, label, unit, higher is better).
36
+ SPEED = {
37
+ "llm": [(("decode_tok_s", "tok_s", "output_tok_s"), "decode", "tok/s", True),
38
+ (("ttft_ms", "ttft_p50_ms"), "TTFT", "ms", False)],
39
+ "tts": [(("ttfa_ms", "ttfa_p50_ms"), "TTFA", "ms", False), (("rtf",), "RTF", "", False),
40
+ (("first_pcm_ms",), "first PCM", "ms", False),
41
+ (("chars_s", "chars_per_s"), "chars", "/s", True)],
42
+ "stt": [(("rtf",), "RTF", "", False), (("first_partial_ms",), "first partial", "ms", False),
43
+ (("final_ms", "finalization_ms"), "final", "ms", False)],
44
+ "video": [(("clip_s",), "clip", "s", False), (("clips_per_hour",), "clips", "/h", True)],
45
+ "image": [(("image_ms", "e2e_ms"), "image", "ms", False),
46
+ (("images_per_hour",), "images", "/h", True)],
47
+ }
48
+ # Per-cell chips, in this order, then any other numeric field.
49
+ CHIPS = [(("ttft_ms",), "TTFT", "ms"), (("decode_tok_s", "tok_s"), "decode", "tok/s"), (("tpot_ms",), "TPOT", "ms"),
50
+ (("e2e_ms", "total_ms", "latency_ms"), "e2e", "ms"), (("output_tokens", "completion_tokens"), "out", "tok"),
51
+ (("ttfa_ms",), "TTFA", "ms"), (("rtf",), "RTF", ""), (("duration_s",), "dur", "s"),
52
+ (("first_pcm_ms",), "first PCM", "ms"), (("first_signal_ms",), "first nonzero chunk", "ms"),
53
+ (("first_audio_ms",), "simulated nonzero onset", "ms"),
54
+ (("complete_audio_ms",), "complete audio", "ms"), (("audio_duration_s",), "PCM duration", "s"),
55
+ (("max_stream_gap_ms",), "max stream gap", "ms"), (("underrun_ms",), "simulated stall", "ms"),
56
+ (("max_gap_ms", "gap_ms"), "max gap", "ms"), (("underruns",), "underruns", ""), (("wer",), "WER", ""),
57
+ (("cer",), "CER", ""), (("utmos", "mos"), "MOS", ""), (("sim", "speaker_sim"), "spk sim", ""),
58
+ (("lufs",), "LUFS", ""), (("first_partial_ms",), "first partial", "ms"), (("final_ms",), "final", "ms")]
59
+ MAX_EXTRA_CHIPS = 4
60
+ PREVIEW_CHARS = 160
61
+
62
+
63
+ def esc(x) -> str:
64
+ return html.escape("" if x is None else str(x), quote=True)
65
+
66
+
67
+ def fmt(x: float | None) -> str:
68
+ if x is None:
69
+ return "n/a"
70
+ if abs(x) >= 10_000:
71
+ return f"{x:,.0f}"
72
+ return f"{x:.4g}"
73
+
74
+
75
+ def value_of(v):
76
+ """(value, ci95 or None) from a number or {"value"|"median"|"p50"|"mean": x, "ci95": [lo, hi]}."""
77
+ if isinstance(v, bool):
78
+ return None
79
+ if isinstance(v, (int, float)):
80
+ return float(v), None
81
+ if isinstance(v, dict):
82
+ for k in ("value", "median", "p50", "mean"):
83
+ x = v.get(k)
84
+ if isinstance(x, (int, float)) and not isinstance(x, bool):
85
+ ci = v.get("ci95") if k == "value" else v.get(f"ci95_{k}", v.get("ci95"))
86
+ ok = isinstance(ci, list) and len(ci) == 2 and all(isinstance(c, (int, float)) for c in ci)
87
+ return float(x), (ci if ok else None)
88
+ return None
89
+
90
+
91
+ def lookup(d: dict, aliases) -> tuple[float, list | None] | None:
92
+ for k in aliases:
93
+ if k in (d or {}):
94
+ got = value_of(d[k])
95
+ if got is not None:
96
+ return got
97
+ return None
98
+
99
+
100
+ def cc(kind: str) -> str:
101
+ title = {"o": "observed: read from the run's metrics files (measured by the benchmark client)",
102
+ "d": "derived: computed by this page from the run's files",
103
+ "s": "self-reported by the engine: not measured by the client and not verified"}[kind]
104
+ return f'<abbr class="cc cc-{kind}" title="{title}">{kind}</abbr>'
105
+
106
+
107
+ VERDICT_CLASS = {"PASS": "pass", "FAIL": "fail", "UNPROVEN": "warn", "NOT RUN": "notrun"}
108
+
109
+
110
+ def badge(text: str, kind: str = "", title: str = "", verdict: str | None = None) -> str:
111
+ """A small label; coloured by ``verdict`` (or the text itself) when it is PASS / FAIL / UNPROVEN / NOT RUN."""
112
+ cls = VERDICT_CLASS.get(str(verdict or text).upper(), kind or "info")
113
+ t = f' title="{esc(title)}"' if title else ""
114
+ return f'<span class="badge b-{cls}"{t}>{esc(text)}</span>'
115
+
116
+
117
+ # --------------------------------------------------------------------------- derived comparisons
118
+
119
+ def compare_cell(run: Run, item: Item, cell: Cell) -> dict:
120
+ """Derived facts about one cell: divergence from the reference, or word errors vs ground truth."""
121
+ out: dict = {"input": item.id, "bucket": item.bucket, "engine": cell.engine, "status": cell.status}
122
+ if cell.status != "ok":
123
+ return out
124
+ if run.modality == "image":
125
+ return out # Viewing a PNG does not establish semantic quality or comparative image similarity.
126
+ if run.modality == "llm":
127
+ ref = run.cells.get((run.reference, run.key(item))) if run.reference else None
128
+ if cell.engine == run.reference or ref is None or ref.status != "ok" or cell.text is None or ref.text is None:
129
+ return out
130
+ td = word_diff(ref.text, cell.text)
131
+ out["diff"] = td
132
+ # Identical means the same string: the word diff ignores whitespace, so newlines, spacing and
133
+ # trailing whitespace are checked here, and the marker goes on the first differing character.
134
+ out["identical_text"] = ref.text == cell.text
135
+ out["first_divergence_word"] = td.first_word
136
+ out["reference_chars"] = len(ref.text)
137
+ if td.identical and not out["identical_text"]:
138
+ out["whitespace_only"] = True
139
+ out["first_divergence_char"] = len(os.path.commonprefix([ref.text, cell.text]))
140
+ out["marker_char"] = out["first_divergence_char"]
141
+ if cell.token_ids is not None and ref.token_ids is not None:
142
+ d = first_token_divergence(ref.token_ids, cell.token_ids)
143
+ out["first_divergence_token"] = d
144
+ out["identical_tokens"] = d is None
145
+ out["reference_tokens"] = len(ref.token_ids)
146
+ if d is not None and cell.pieces and "".join(cell.pieces) == cell.text:
147
+ out["marker_char"] = len("".join(cell.pieces[:d]))
148
+ out["identical"] = out.get("identical_tokens", out["identical_text"])
149
+ if out["identical"]:
150
+ out.pop("marker_char", None) # same tokens: nothing to mark, whatever the detokenized spacing
151
+ return out
152
+ if run.modality == "video":
153
+ row = cell.clip_row or {}
154
+ mismatch = row.get("effective_mismatch")
155
+ if isinstance(mismatch, list) and mismatch:
156
+ out["effective_mismatch"] = [str(x) for x in mismatch] # the engine says it ran something else
157
+ if row.get("cache"):
158
+ out["cached"] = True
159
+ if row.get("stage_sum_ok") is False:
160
+ out["stages_over_e2e"] = True
161
+ ref = run.cells.get((run.reference, run.key(item))) if run.reference else None
162
+ if cell.engine != run.reference and ref is not None and ref.status == "ok" and (ref.clip_row or {}).get(
163
+ "out_sha256") and ref.clip_row.get("out_sha256") == row.get("out_sha256"):
164
+ out["same_bytes_as_reference"] = True
165
+ return out
166
+ hyp = cell.text if run.modality == "stt" else cell.transcript
167
+ if item.ground_truth and hyp is not None:
168
+ al = align_words(item.ground_truth, hyp)
169
+ out["alignment"] = al
170
+ out.update({"wer": al.wer, "sub": al.count("sub"), "del": al.count("del"), "ins": al.count("ins"),
171
+ "ref_words": al.ref_len, "exact_alignment": al.exact})
172
+ return out
173
+
174
+
175
+ # --------------------------------------------------------------------------- cell bodies
176
+
177
+ MARK = '<span class="fd" title="first divergence from the reference" aria-label="first divergence"></span>'
178
+
179
+
180
+ def render_diff(info: dict, text: str) -> str:
181
+ td = info["diff"]
182
+ m = info.get("marker_char")
183
+ pos = len(td.lead)
184
+ if m is None and not td.identical:
185
+ for tag, _r, c in td.segments: # word-level: the start of the first differing run
186
+ if tag != "equal":
187
+ m = pos
188
+ break
189
+ pos += len(c)
190
+ lead = td.lead
191
+ if m is not None and m <= len(lead) and (m < len(lead) or not td.segments): # leading whitespace differs
192
+ out, marked = [esc(lead[:m]), MARK, esc(lead[m:])], True
193
+ else:
194
+ out, marked = [esc(lead)], m is None
195
+ pos = len(lead)
196
+ for tag, r, c in td.segments:
197
+ end = pos + len(c)
198
+ if tag == "equal":
199
+ if not marked and pos <= m < end:
200
+ k = m - pos
201
+ out += [esc(c[:k]), MARK, esc(c[k:])]
202
+ marked = True
203
+ else:
204
+ out.append(esc(c))
205
+ else:
206
+ if not marked and m == pos:
207
+ out.append(MARK)
208
+ marked = True
209
+ if r:
210
+ out.append(f'<del title="only in the reference">{esc(r)}</del>')
211
+ if c:
212
+ if not marked and pos < m < end:
213
+ k = m - pos
214
+ out.append(f"<ins>{esc(c[:k])}</ins>{MARK}<ins>{esc(c[k:])}</ins>")
215
+ marked = True
216
+ else:
217
+ out.append(f"<ins>{esc(c)}</ins>")
218
+ pos = end
219
+ if not marked:
220
+ out.append(MARK)
221
+ return "".join(out)
222
+
223
+
224
+ def render_alignment(al: Alignment) -> str:
225
+ parts = []
226
+ for op, r, h in al.ops:
227
+ if op == "ok":
228
+ parts.append(esc(h))
229
+ elif op == "sub":
230
+ parts.append(f'<span class="w-sub" title="substitution; ground truth: {esc(r)}">{esc(h)}</span>')
231
+ elif op == "ins":
232
+ parts.append(f'<span class="w-ins" title="insertion">{esc(h)}</span>')
233
+ else:
234
+ parts.append(f'<span class="w-del" title="deletion: missing from the output">{esc(r)}</span>')
235
+ return " ".join(parts)
236
+
237
+
238
+ def audio_tag(rel: str) -> str:
239
+ return f'<audio controls preload="none" src="../{esc(quote(rel))}"></audio>'
240
+
241
+
242
+ def video_tag(rel: str) -> str:
243
+ return f'<video controls muted playsinline preload="metadata" src="../{esc(quote(rel))}"></video>'
244
+
245
+
246
+ def image_tag(rel: str) -> str:
247
+ return (f'<a href="../{esc(quote(rel))}"><img class="generated" loading="lazy" src="../{esc(quote(rel))}" '
248
+ 'alt="Generated image; open for full resolution"></a>')
249
+
250
+
251
+ def _secs(ms) -> str:
252
+ return f"{fmt(ms / 1000)} s"
253
+
254
+
255
+ def clip_chips(cell: Cell) -> str:
256
+ """Video cell chips: client-timed clip time (observed), the engine's stage times and effective shape
257
+ (self-reported), then the frame-quality numbers quality clip measured against the reference (observed)."""
258
+ m, row = cell.metrics, cell.clip_row or {}
259
+ observed, reported = [], []
260
+ got = value_of(m.get("clip_s")) if "clip_s" in m else None
261
+ if got is not None:
262
+ observed.append(f'<span class="chip">clip {esc(fmt(got[0]))} s</span>')
263
+ for keys, label, unit in ((("psnr", "psnr_db"), "PSNR", " dB"), (("ssim",), "SSIM", ""),
264
+ (("lpips",), "LPIPS", "")):
265
+ key = next((k for k in keys if k in m), None)
266
+ got = value_of(m.get(key)) if key else None
267
+ if got is not None:
268
+ observed.append(f'<span class="chip">{label} {esc(fmt(got[0]))}{unit}</span>')
269
+ stages = row.get("stage_ms") if isinstance(row.get("stage_ms"), dict) else {}
270
+ for name, v in list(stages.items())[:6]:
271
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
272
+ reported.append(f'<span class="chip">{esc(name)} {esc(_secs(v))}</span>')
273
+ shape = [f"{row['w']}x{row['h']}" if row.get("w") and row.get("h") else "",
274
+ f"{row['frames']} frames" if row.get("frames") else "", f"{row['fps']} fps" if row.get("fps") else "",
275
+ f"{row['steps_run']} steps" if row.get("steps_run") else "", str(row.get("attention") or ""),
276
+ str(row.get("dtype") or "")]
277
+ if any(shape):
278
+ reported.append(f'<span class="chip">{esc(", ".join(x for x in shape if x))}</span>')
279
+ out = f'<div class="chips">{"".join(observed)}{cc("o")}</div>' if observed else ""
280
+ return out + (f'<div class="chips">{"".join(reported)}{cc("s")}</div>' if reported else "")
281
+
282
+
283
+ def clip_spec_text(clip: dict) -> str:
284
+ """What a clip request asked for, in one line: task, size, frames, fps, steps, CFG, seed, image."""
285
+ parts = [str(clip.get("task") or "")]
286
+ if clip.get("width") and clip.get("height"):
287
+ parts.append(f"{clip['width']}x{clip['height']}")
288
+ if clip.get("num_frames"):
289
+ parts.append(f"{clip['num_frames']} frames" + (f" @ {clip['fps']} fps" if clip.get("fps") else ""))
290
+ if clip.get("steps"):
291
+ parts.append(f"{clip['steps']} steps")
292
+ if clip.get("guidance_scale") is not None:
293
+ parts.append(f"CFG {clip['guidance_scale']}")
294
+ if clip.get("seed") is not None:
295
+ parts.append(f"seed {clip['seed']}")
296
+ if clip.get("audio") is not None:
297
+ parts.append("audio" if clip["audio"] else "no audio")
298
+ if clip.get("image_sha256"):
299
+ parts.append(f"image sha256 {str(clip['image_sha256'])[:12]}")
300
+ return " \u00b7 ".join(p for p in parts if p)
301
+
302
+
303
+ def chips(metrics: dict, derived=()) -> str:
304
+ shown, computed, used = [], [], set()
305
+ for aliases, label, unit in CHIPS:
306
+ for k in aliases:
307
+ got = value_of(metrics.get(k)) if k in metrics else None
308
+ if got is not None:
309
+ target = computed if k in derived else shown
310
+ target.append(f'<span class="chip">{esc(label)} {esc(fmt(got[0]))}{esc(unit and " " + unit)}</span>')
311
+ used.update(aliases)
312
+ break
313
+ extra = 0
314
+ for k, v in metrics.items():
315
+ if k in used or extra >= MAX_EXTRA_CHIPS:
316
+ continue
317
+ got = value_of(v)
318
+ if got is not None:
319
+ target = computed if k in derived else shown
320
+ target.append(f'<span class="chip">{esc(k)} {esc(fmt(got[0]))}</span>')
321
+ extra += 1
322
+ return ((f'<div class="chips">{"".join(shown)}{cc("o")}</div>' if shown else "") +
323
+ (f'<div class="chips">{"".join(computed)}{cc("d")}</div>' if computed else ""))
324
+
325
+
326
+ def quality_badges(metrics: dict) -> list[str]:
327
+ out = []
328
+ v = metrics.get("verdict")
329
+ if isinstance(v, str):
330
+ out.append(badge(v, title="per-input verdict from the metrics file"))
331
+ if isinstance(metrics.get("pass"), bool):
332
+ out.append(badge("PASS" if metrics["pass"] else "FAIL", title="per-input check from the metrics file"))
333
+ q = metrics.get("quality")
334
+ if isinstance(q, dict):
335
+ for name, val in list(q.items())[:4]:
336
+ verdict = val.get("verdict") if isinstance(val, dict) else val
337
+ if isinstance(verdict, str):
338
+ out.append(badge(f"{name}: {verdict}", title=f"quality check '{name}' from the metrics file",
339
+ verdict=verdict))
340
+ return out
341
+
342
+
343
+ def severity(cell: Cell, info: dict) -> float:
344
+ """How bad a cell looks, for 'worst first': 2 = failed / not run, 1+ = diverged early / word errors."""
345
+ if cell.status != "ok":
346
+ return 2.0
347
+ if info.get("effective_mismatch") or info.get("cached"):
348
+ return 1.9
349
+ if info.get("identical") is False:
350
+ if info.get("first_divergence_token") is not None:
351
+ d, n = info["first_divergence_token"], info.get("reference_tokens") or 1
352
+ elif info.get("first_divergence_word") is not None:
353
+ d, n = info["first_divergence_word"], info["diff"].ref_words or 1
354
+ else: # whitespace only: by character
355
+ d, n = info.get("first_divergence_char") or 0, info.get("reference_chars") or 1
356
+ return 1.0 + max(0.0, 1.0 - d / n) * 0.99 # earlier divergence = worse
357
+ if info.get("wer"):
358
+ return 1.0 + min(info["wer"], 1.0) * 0.99
359
+ return 0.0
360
+
361
+
362
+ def cell_html(run: Run, item: Item, cell: Cell, info: dict, idx: int) -> str:
363
+ head = f'<div class="cell-head">{esc(cell.engine)}</div>'
364
+ if cell.status == "not_run":
365
+ return (f'<td class="cell st-notrun" data-e="{idx}">{head}{badge("NOT RUN")}'
366
+ f'<div class="why">{esc(cell.reason)}</div></td>')
367
+ badges = []
368
+ if cell.status == "failed":
369
+ badges.append(badge("FAILED", "fail", cell.reason))
370
+ body = f'<div class="why">{esc(cell.reason)}</div>' if cell.status == "failed" and cell.reason else ""
371
+ if run.modality == "llm":
372
+ if cell.engine == run.reference:
373
+ badges.append(badge("reference", "info"))
374
+ elif "diff" in info:
375
+ if info.get("identical"):
376
+ badges.append(badge("identical", "pass", "same tokens as the reference" if "identical_tokens" in info
377
+ else "same text as the reference (no token ids to compare)") + cc("d"))
378
+ elif info.get("whitespace_only") and info.get("first_divergence_token") is None:
379
+ badges.append(badge(f"whitespace differs @ char {info['first_divergence_char']}", "warn",
380
+ "same words as the reference, but spacing, newlines or trailing whitespace "
381
+ "differ: not identical") + cc("d"))
382
+ elif info.get("first_divergence_token") is not None:
383
+ d, n = info["first_divergence_token"], info.get("reference_tokens")
384
+ note = "same text, " if info.get("identical_text") else ""
385
+ badges.append(badge(f"{note}diverges @ token {d}" + (f"/{n}" if n else ""), "warn",
386
+ "first token id that differs from the reference") + cc("d"))
387
+ else:
388
+ badges.append(badge(f"diverges @ word {info['first_divergence_word']}", "warn",
389
+ "no token ids on both sides: first differing word") + cc("d"))
390
+ elif run.reference and cell.text is not None:
391
+ badges.append(badge("no reference output", "notrun"))
392
+ if cell.reasoning:
393
+ body += (f'<details class="reasoning"><summary>reasoning ({len(cell.reasoning):,} chars, not compared)'
394
+ f'</summary><div class="text">{esc(cell.reasoning)}</div></details>')
395
+ if cell.text is not None:
396
+ inner = render_diff(info, cell.text) if "diff" in info else esc(cell.text)
397
+ body += f'<div class="text">{inner}</div>' if cell.text else '<div class="why">(empty output)</div>'
398
+ elif cell.token_ids is not None:
399
+ body += f'<div class="why">{len(cell.token_ids)} token ids, no text</div>'
400
+ elif run.modality == "image":
401
+ if cell.engine == run.reference:
402
+ badges.append(badge("reference", "info"))
403
+ if cell.image:
404
+ body += image_tag(cell.image)
405
+ elif run.modality == "video":
406
+ if cell.engine == run.reference:
407
+ badges.append(badge("reference", "info"))
408
+ if info.get("effective_mismatch"):
409
+ badges.append(badge("ran something else", "fail", "the engine reports: " + "; ".join(
410
+ info["effective_mismatch"])) + cc("s"))
411
+ if info.get("cached"):
412
+ badges.append(badge("cached stages", "fail", "the engine reports stages served from a cache") + cc("s"))
413
+ if info.get("stages_over_e2e"):
414
+ badges.append(badge("stages > clip time", "warn", "self-reported stage times sum to more than the "
415
+ "client-timed clip: overlapping stages or wrong self-reports") + cc("d"))
416
+ if info.get("same_bytes_as_reference"):
417
+ badges.append(badge("same bytes as the reference", "info", "the clip file is byte-identical to the "
418
+ "reference clip") + cc("d"))
419
+ if cell.video:
420
+ body += video_tag(cell.video)
421
+ elif cell.text is not None:
422
+ body += f'<div class="text">{esc(cell.text)}</div>'
423
+ else:
424
+ if cell.audio:
425
+ body += audio_tag(cell.audio)
426
+ if "alignment" in info:
427
+ wer = info["wer"]
428
+ label = f"WER {100 * wer:.1f}%" if wer is not None else "WER n/a"
429
+ badges.append(badge(label, "pass" if wer == 0 else "warn",
430
+ f"S {info['sub']} / D {info['del']} / I {info['ins']} over {info['ref_words']} words"
431
+ + ("" if info["exact_alignment"] else " (anchored alignment)")) + cc("d"))
432
+ body += f'<div class="text">{render_alignment(info["alignment"])}</div>'
433
+ elif run.modality == "stt" and cell.text is not None:
434
+ body += f'<div class="text">{esc(cell.text)}</div>'
435
+ elif run.modality == "tts" and cell.audio:
436
+ body += '<div class="why">no ASR transcript for this clip</div>'
437
+ elif cell.text is not None:
438
+ body += f'<div class="text">{esc(cell.text)}</div>'
439
+ if cell.finish_reason and cell.finish_reason not in ("stop", "length"):
440
+ badges.append(badge(f"finish: {cell.finish_reason}", "warn"))
441
+ badges += quality_badges(cell.metrics)
442
+ status = "st-failed" if cell.status == "failed" else ("st-diff" if info.get("identical") is False
443
+ or (info.get("wer") or 0) > 0
444
+ or info.get("effective_mismatch") else "st-ok")
445
+ from inferencekit.review.media import DERIVED_AUDIO
446
+
447
+ numbers = clip_chips(cell) if run.modality == "video" else chips(
448
+ cell.metrics, DERIVED_AUDIO if run.engine(cell.engine).media_kind == "audio" else ())
449
+ return (f'<td class="cell {status}" data-e="{idx}">{head}<div class="badges">{"".join(badges)}</div>'
450
+ f'{numbers}{body}</td>')
451
+
452
+
453
+ def input_html(run: Run, item: Item) -> str:
454
+ parts = [f'<div class="iid">{esc(item.id)}</div>']
455
+ if item.audio:
456
+ parts.append(audio_tag(item.audio))
457
+ if item.clip:
458
+ parts.append(f'<div class="spec">{esc(clip_spec_text(item.clip))}</div>')
459
+ if run.modality == "video":
460
+ parts.append('<button type="button" class="play" data-play="1" title="play every clip in this row from the '
461
+ 'start, together">play row</button>')
462
+ text = item.text
463
+ if run.modality == "stt" and item.audio and text and text.strip() == (item.ground_truth or "").strip():
464
+ text = None # the .txt beside the audio is its ground truth, shown once below
465
+ if text:
466
+ if len(text) > PREVIEW_CHARS:
467
+ parts.append(f'<details><summary>{esc(text[:PREVIEW_CHARS])}&hellip; '
468
+ f'<span class="more">({len(text):,} chars)</span></summary>'
469
+ f'<div class="text">{esc(text)}</div></details>')
470
+ else:
471
+ parts.append(f'<div class="text">{esc(text)}</div>')
472
+ elif not item.audio and not item.clip:
473
+ parts.append('<div class="why">input file not found under inputs/</div>')
474
+ if item.ground_truth and run.modality == "stt":
475
+ parts.append(f'<div class="gt"><b>ground truth:</b> {esc(item.ground_truth)}</div>')
476
+ return f'<th scope="row" class="input">{"".join(parts)}</th>'
477
+
478
+
479
+ # --------------------------------------------------------------------------- summary
480
+
481
+ def speed_buckets(run: Run) -> list[str]:
482
+ """Summary speed columns: one per bucket, or for video one over all rounds (a round is a seed, not a size)."""
483
+ return ["all"] if run.modality == "video" else run.buckets()
484
+
485
+
486
+ def bucket_title(run: Run, b: str) -> str:
487
+ if b == "all" and run.modality == "video":
488
+ return "all rounds"
489
+ if run.modality in ("video", "image", "tts") and ROUND_DIR.match(b):
490
+ return f"round {b[1:]}"
491
+ return b.replace("_", " ")
492
+
493
+
494
+ def engine_summary(run: Run, label: str, infos: dict) -> dict:
495
+ eng = run.engine(label)
496
+ cells = [(item, run.cells[(label, run.key(item))]) for item in run.items]
497
+ s = {"engine": label, "role": eng.role, "inputs": len(cells),
498
+ "ok": sum(1 for _, c in cells if c.status == "ok"),
499
+ "failed": sum(1 for _, c in cells if c.status == "failed"),
500
+ "not_run": sum(1 for _, c in cells if c.status == "not_run"), "speed": {}, "evidence": []}
501
+ for b in speed_buckets(run):
502
+ row = {}
503
+ for aliases, name, unit, _higher in SPEED.get(run.modality, SPEED["llm"]):
504
+ got = lookup(eng.per_bucket.get(b, {}), aliases)
505
+ if got is not None:
506
+ claim = next((eng.metric_classes[k] for k in aliases if k in eng.metric_classes), "observed")
507
+ row[name] = {"value": got[0], "ci95": got[1], "unit": unit, "claim_class": claim}
508
+ continue
509
+ vals = [lookup(c.metrics, aliases) for item, c in cells
510
+ if item.bucket == b or (b == "all" and run.modality == "video")]
511
+ vals = [v[0] for v in vals if v is not None]
512
+ if vals:
513
+ row[name] = {"value": statistics.median(vals), "ci95": None, "unit": unit, "n": len(vals),
514
+ "claim_class": "derived"}
515
+ s["speed"][b] = row
516
+ m = eng.metrics
517
+ pct = value_of(m.get("pct_of_ceiling"))
518
+ ceiling = value_of(m.get("ceiling_tok_s"))
519
+ if pct is not None:
520
+ s["pct_of_ceiling"] = {"value": pct[0], "claim_class": "observed"}
521
+ elif ceiling is not None and ceiling[0] > 0:
522
+ overall = lookup(m, ("decode_tok_s",)) or lookup(eng.per_bucket.get("all", {}), ("decode_tok_s",))
523
+ if overall is None:
524
+ vals = [lookup(c.metrics, ("decode_tok_s",)) for _, c in cells]
525
+ vals = [v[0] for v in vals if v is not None]
526
+ overall = (statistics.median(vals), None) if vals else None
527
+ if overall is not None:
528
+ s["pct_of_ceiling"] = {"value": 100 * overall[0] / ceiling[0], "claim_class": "derived"}
529
+ s["quality"] = _verdicts(m.get("quality"))
530
+ gates = m.get("gates")
531
+ if isinstance(gates, str):
532
+ s["gates"] = {"verdict": gates, "violations": []}
533
+ elif isinstance(gates, dict):
534
+ s["gates"] = {"verdict": str(gates.get("verdict") or "UNPROVEN"),
535
+ "violations": [str(v) for v in gates.get("violations") or []]}
536
+ cost = m.get("cost")
537
+ got = value_of(cost) if not isinstance(cost, dict) else lookup(
538
+ cost, ("usd_per_1m_output_tokens", "usd_per_1m", "usd_per_1m_tokens", "usd_per_1m_chars",
539
+ "usd_per_audio_hour"))
540
+ if got is not None:
541
+ unit = "$ / 1M output tokens" if run.modality == "llm" else "$ per unit"
542
+ if isinstance(cost, dict) and isinstance(cost.get("unit"), str):
543
+ unit = cost["unit"]
544
+ s["cost"] = {"value": got[0], "unit": unit, "claim_class": "observed",
545
+ "gpu_price_usd_h": cost.get("gpu_price_usd_h") if isinstance(cost, dict) else None}
546
+ ev_ids = m.get("evidence")
547
+ if isinstance(ev_ids, list):
548
+ s["evidence"] = [str(e) for e in ev_ids][:20]
549
+ # Verdicts from evidence records (quality / gates / capacity records) complement the metrics file.
550
+ for q in eng.ev_quality:
551
+ s["quality"].append({**q, "claim_class": "derived" if q.get("verdict") else "observed"})
552
+ if "gates" not in s and eng.ev_gates:
553
+ s["gates"] = {"verdict": eng.ev_gates["verdict"], "violations": eng.ev_gates["violations"]}
554
+ cap = eng.capacity
555
+ if cap:
556
+ s["capacity"] = {"n_star": cap.get("n_star"), "reason": cap.get("reason"), "level_kind": cap.get("level_kind"),
557
+ "slo": cap.get("slo"), "claim_class": "derived", "edited": cap.get("edited")}
558
+ if "cost" not in s and cap.get("usd_per_1m") is not None and not cap.get("edited"):
559
+ s["cost"] = {"value": cap["usd_per_1m"], "unit": "$ / 1M output tokens at N*", "claim_class": "derived",
560
+ "gpu_price_usd_h": cap.get("price")}
561
+ s["evidence"] = list(dict.fromkeys(s["evidence"] + eng.evidence))[:20]
562
+ if eng.stages:
563
+ s["stages_ms"] = {"value": dict(eng.stages), "claim_class": "self-reported"}
564
+ mine = [infos[(label, run.key(item))] for item in run.items]
565
+ if run.modality == "llm" and label != run.reference and run.reference:
566
+ compared = [i for i in mine if "identical" in i]
567
+ s["vs_reference"] = {"compared": len(compared), "identical": sum(1 for i in compared if i["identical"]),
568
+ "claim_class": "derived"}
569
+ tok = [i["first_divergence_token"] for i in compared if i.get("first_divergence_token") is not None]
570
+ if tok:
571
+ s["vs_reference"]["median_first_divergence_token"] = statistics.median(tok)
572
+ errs = [i for i in mine if i.get("wer") is not None]
573
+ if errs:
574
+ words = sum(i["ref_words"] for i in errs)
575
+ wrong = sum(i["sub"] + i["del"] + i["ins"] for i in errs)
576
+ s["wer"] = {"value": wrong / words if words else None, "clips": len(errs), "claim_class": "derived"}
577
+ return s
578
+
579
+
580
+ def _verdicts(q) -> list[dict]:
581
+ out = []
582
+ items = q.items() if isinstance(q, dict) else ((x.get("name"), x) for x in q if isinstance(x, dict)) \
583
+ if isinstance(q, list) else []
584
+ for name, val in items:
585
+ if isinstance(val, str):
586
+ out.append({"name": str(name), "verdict": val})
587
+ elif isinstance(val, dict):
588
+ got = value_of(val)
589
+ out.append({"name": str(name or val.get("name") or "quality"),
590
+ "verdict": str(val.get("verdict") or "UNPROVEN"),
591
+ "value": got[0] if got else None, "unit": val.get("unit")})
592
+ return out
593
+
594
+
595
+ def _quality_badge(x: dict) -> str:
596
+ value = f"{fmt(x['value'])}{x['unit'] if x.get('unit') == '%' else (' ' + x['unit'] if x.get('unit') else '')}" \
597
+ if x.get("value") is not None else ""
598
+ if x.get("verdict"):
599
+ return badge(f"{x['name']}: {x['verdict']}", verdict=x["verdict"],
600
+ title=f"value {value}" if value else "") + (cc("d") if x.get("evidence") else "")
601
+ return badge(f"{x['name']} {value or 'n/a'}", "info", "measured; no verdict") + cc("o")
602
+
603
+
604
+ def _speed_cell(row: dict) -> str:
605
+ if not row:
606
+ return '<span class="na">not measured</span>'
607
+ parts = []
608
+ for name, v in row.items():
609
+ ci = v.get("ci95")
610
+ ci_text = f' <span class="ci">({fmt(ci[0])}&ndash;{fmt(ci[1])})</span>' if ci else ""
611
+ unit = f" {v['unit']}" if v["unit"] else ""
612
+ n = f' <span class="ci">(median of {v["n"]})</span>' if v.get("n") else ""
613
+ parts.append(f'<div>{esc(name)} <b>{esc(fmt(v["value"]))}</b>{esc(unit)}{ci_text}{n}'
614
+ f'{cc("o" if v["claim_class"] == "observed" else "d")}</div>')
615
+ return "".join(parts)
616
+
617
+
618
+ NO_EVIDENCE = ("no evidence attached", ("no evidence records were attached to this page: pass --evidence <id> "
619
+ "(gates, quality, capacity records) or copy them into <run-dir>/evidence/"))
620
+
621
+
622
+ def capacity_cell(cap: dict | None, absent: str) -> str:
623
+ if not cap:
624
+ return f"<td>{absent}</td>"
625
+ if cap.get("edited"):
626
+ return f"<td>{badge('UNPROVEN', title='capacity evidence was edited after capture')}</td>"
627
+ unit = "req/s" if cap.get("level_kind") == "rate" else "streams"
628
+ reason = cap.get("reason") or ""
629
+ if cap.get("n_star") is None:
630
+ if reason.startswith("SLO first fails"): # missed at the lowest level tried
631
+ return (f"<td>{badge('FAIL', title=reason)} <span class='sub'>SLO missed at every level "
632
+ f"tried</span></td>")
633
+ verdict = "NOT RUN" if reason in ("no load levels ran", "") else "UNPROVEN"
634
+ return f"<td>{badge(verdict, title=reason or 'N* not computed')} <span class='sub'>{esc(reason)}</span></td>"
635
+ slo = cap.get("slo") or {}
636
+ slo_txt = ", ".join(f"{esc(k)} &le; {esc(fmt(v))}" for k, v in slo.items()
637
+ if isinstance(v, (int, float)) and not isinstance(v, bool))
638
+ lower = "at least " if reason.startswith("SLO holds at every level") else ""
639
+ return (f"<td>{lower}<b>{esc(fmt(cap['n_star']))}</b> {unit}{cc('d')}"
640
+ f"<div class='sub'>{slo_txt or 'no SLO recorded'}</div></td>")
641
+
642
+
643
+ def summary_html(run: Run, summaries: list[dict]) -> str:
644
+ buckets = speed_buckets(run)
645
+ absent = (badge(NO_EVIDENCE[0], "notrun", NO_EVIDENCE[1]) if not run.evidence_attached else badge("NOT RUN"))
646
+ head = ["engine"] + [bucket_title(run, b) for b in buckets] + ["% of ceiling", "quality", "gates", "cost"]
647
+ has_cap = any("capacity" in s for s in summaries)
648
+ if has_cap:
649
+ head.insert(head.index("cost"), "N*")
650
+ if run.modality == "llm" and run.reference:
651
+ head.append("vs reference")
652
+ if any("wer" in s for s in summaries):
653
+ head.append("WER")
654
+ head.append("outputs")
655
+ rows = []
656
+ for idx, s in enumerate(summaries):
657
+ cells = [f'<th scope="row">{esc(s["engine"])}' + (f' <span class="role">{esc(s["role"])}</span>'
658
+ if s.get("role") else "") + "</th>"]
659
+ stages = ""
660
+ if s.get("stages_ms"): # video: the engine's own stage medians, under the client-timed clip time
661
+ stages = ("<div class='sub'>stages: " + ", ".join(f"{esc(k)} {esc(_secs(v))}" for k, v in
662
+ list(s["stages_ms"]["value"].items())[:6])
663
+ + f"{cc('s')}</div>")
664
+ cells += [f"<td>{_speed_cell(s['speed'].get(b, {}))}{stages}</td>" for b in buckets]
665
+ pct = s.get("pct_of_ceiling")
666
+ cells.append(f"<td>{esc(fmt(pct['value']))}%{cc('o' if pct['claim_class'] == 'observed' else 'd')}</td>"
667
+ if pct else '<td><span class="na">no ceiling</span></td>')
668
+ q = s["quality"]
669
+ cells.append("<td>" + ("".join(_quality_badge(x) for x in q) if q else absent) + "</td>")
670
+ g = s.get("gates")
671
+ cells.append("<td>" + (badge(g["verdict"], title="; ".join(g["violations"])) if g else absent) + "</td>")
672
+ if has_cap:
673
+ cells.append(capacity_cell(s.get("capacity"), absent))
674
+ c = s.get("cost")
675
+ cells.append(f"<td>${esc(fmt(c['value']))} <span class='unit'>{esc(c['unit'].lstrip('$ '))}</span>"
676
+ f"{cc('o' if c.get('claim_class', 'observed') == 'observed' else 'd')}</td>" if c
677
+ else '<td><span class="na">unknown</span></td>')
678
+ if run.modality == "llm" and run.reference:
679
+ vr = s.get("vs_reference")
680
+ if s["engine"] == run.reference:
681
+ cells.append("<td>(reference)</td>")
682
+ elif vr and vr["compared"]:
683
+ med = vr.get("median_first_divergence_token")
684
+ cells.append(f"<td>{vr['identical']}/{vr['compared']} identical"
685
+ + (f"<div class='sub'>median first divergence: token {esc(fmt(med))}</div>"
686
+ if med is not None else "") + f"{cc('d')}</td>")
687
+ else:
688
+ cells.append('<td><span class="na">nothing to compare</span></td>')
689
+ if any("wer" in x for x in summaries):
690
+ w = s.get("wer")
691
+ cells.append(f"<td>{100 * w['value']:.2f}% <span class='sub'>({w['clips']} clips)</span>{cc('d')}</td>"
692
+ if w and w["value"] is not None else '<td><span class="na">n/a</span></td>')
693
+ nr = f"<div class='bad'>{s['not_run']} NOT RUN</div>" if s["not_run"] else ""
694
+ fl = f"<div class='bad'>{s['failed']} failed</div>" if s["failed"] else ""
695
+ cells.append(f"<td>{s['ok']}/{s['inputs']}{nr}{fl}</td>")
696
+ rows.append(f'<tr data-e="{idx}">{"".join(cells)}</tr>')
697
+ ths = "".join(f'<th scope="col">{esc(h)}</th>' for h in head)
698
+ return (f'<div class="scroll"><table class="summary"><thead><tr>{ths}</tr></thead>'
699
+ f'<tbody>{"".join(rows)}</tbody></table></div>')
700
+
701
+
702
+ # --------------------------------------------------------------------------- page
703
+
704
+ CSS = """
705
+ :root{--bg:#fbfbfa;--panel:#fff;--fg:#1d1f23;--muted:#5d636e;--line:#dfe1e5;--accent:#2457c5;
706
+ --ok:#1b7a3d;--okbg:#e3f4e8;--bad:#b42318;--badbg:#fde8e6;--warn:#8a5a00;--warnbg:#fdf1d7;
707
+ --na:#6b7280;--nabg:#eceef1;--ins:#d7f2dd;--del:#fadcd9;--sub:#fdeab8;--mark:#d03b2f}
708
+ @media (prefers-color-scheme:dark){:root{--bg:#121417;--panel:#1a1d21;--fg:#e6e8eb;--muted:#9aa1ab;
709
+ --line:#2d3238;--accent:#7aa2ff;--ok:#5fd38a;--okbg:#173322;--bad:#ff8a80;--badbg:#3a1a18;--warn:#f2c46b;
710
+ --warnbg:#3a2e12;--na:#a3a9b2;--nabg:#262a30;--ins:#1d4429;--del:#4a2220;--sub:#4a3a12;--mark:#ff7a6e}}
711
+ *{box-sizing:border-box}
712
+ html{-webkit-text-size-adjust:100%}
713
+ body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.45 system-ui,-apple-system,"Segoe UI",
714
+ Roboto,sans-serif}
715
+ header,main,footer,.bar{padding:0 16px;max-width:100%}
716
+ header{padding-top:16px}
717
+ h1{font-size:20px;margin:0 0 4px}
718
+ h2{font-size:16px;margin:24px 0 8px}
719
+ .meta{color:var(--muted);font-size:13px}
720
+ .meta span{margin-right:12px;display:inline-block}
721
+ .warnings{font-size:13px;color:var(--warn);margin:8px 0;padding-left:18px}
722
+ .scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--line);border-radius:8px;
723
+ background:var(--panel)}
724
+ table{border-collapse:collapse;width:100%}
725
+ th,td{border-bottom:1px solid var(--line);padding:8px;vertical-align:top;text-align:left}
726
+ thead th{font-size:12px;color:var(--muted);font-weight:600;background:var(--panel);white-space:nowrap}
727
+ table.summary td,table.summary th{font-size:13px;white-space:nowrap}
728
+ .role{font-size:11px;color:var(--muted);font-weight:400;display:block}
729
+ .ci,.sub,.unit,.more{color:var(--muted);font-size:12px}
730
+ .na{color:var(--na);font-style:italic}
731
+ .bad{color:var(--bad);font-size:12px}
732
+ .bar{position:sticky;top:0;z-index:2;background:var(--bg);padding-top:8px;padding-bottom:8px;
733
+ border-bottom:1px solid var(--line);display:flex;flex-wrap:wrap;gap:6px 14px;align-items:center}
734
+ .bar .grp{display:flex;flex-wrap:wrap;gap:4px;align-items:center}
735
+ .bar .lbl{font-size:12px;color:var(--muted);margin-right:2px}
736
+ .bar label{font-size:12px;border:1px solid var(--line);border-radius:999px;padding:2px 9px;background:var(--panel);
737
+ cursor:pointer;user-select:none;white-space:nowrap}
738
+ .bar input{margin:0 4px 0 0;vertical-align:-1px}
739
+ table.grid{table-layout:fixed;min-width:calc(220px + var(--cols) * 260px)}
740
+ table.grid col.c-in{width:220px}
741
+ th.input{font-weight:400;background:var(--panel)}
742
+ .iid{font:600 12px ui-monospace,SFMono-Regular,Menlo,monospace;margin-bottom:4px;word-break:break-all}
743
+ .text{white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere;max-height:22em;overflow-y:auto;
744
+ font-size:13px}
745
+ details summary{cursor:pointer;white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px}
746
+ details.reasoning{margin-bottom:6px}details.reasoning summary{font-size:12px;color:var(--muted)}
747
+ .gt{font-size:12px;color:var(--muted);margin-top:6px;overflow-wrap:anywhere}
748
+ .cell-head{display:none;font-weight:600;font-size:12px;color:var(--muted);margin-bottom:4px}
749
+ .badges{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px;align-items:center}
750
+ .badge{display:inline-block;font-size:11px;font-weight:600;border-radius:4px;padding:1px 6px;white-space:nowrap}
751
+ .b-pass{background:var(--okbg);color:var(--ok)}.b-fail{background:var(--badbg);color:var(--bad)}
752
+ .b-warn{background:var(--warnbg);color:var(--warn)}.b-notrun{background:var(--nabg);color:var(--na)}
753
+ .b-info{background:var(--nabg);color:var(--fg)}
754
+ .chips{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px;align-items:center}
755
+ .chip{font-size:11px;border:1px solid var(--line);border-radius:4px;padding:0 5px;color:var(--muted);
756
+ white-space:nowrap}
757
+ .cc{font-size:9px;text-decoration:none;border:1px solid var(--line);border-radius:3px;padding:0 3px;
758
+ margin-left:3px;color:var(--muted);vertical-align:1px;cursor:help}
759
+ .cc-d{border-style:dashed}
760
+ .why{color:var(--na);font-size:12px;font-style:italic}
761
+ td.st-notrun{background:repeating-linear-gradient(135deg,transparent 0 8px,var(--nabg) 8px 9px)}
762
+ td.st-failed{box-shadow:inset 3px 0 0 var(--bad)}
763
+ td.st-diff{box-shadow:inset 3px 0 0 var(--warn)}
764
+ ins{background:var(--ins);text-decoration:none;border-radius:2px}
765
+ del{background:var(--del);color:var(--muted);border-radius:2px}
766
+ .fd{display:inline-block;width:0;height:1.1em;border-left:2px solid var(--mark);vertical-align:-0.2em;
767
+ margin:0 1px;position:relative}
768
+ .fd::before{content:"";position:absolute;top:-4px;left:-5px;border:4px solid transparent;
769
+ border-top-color:var(--mark)}
770
+ .w-sub{background:var(--sub);border-radius:2px}
771
+ .w-ins{background:var(--ins);border-radius:2px}
772
+ .w-del{background:var(--del);text-decoration:line-through;border-radius:2px}
773
+ audio{width:100%;max-width:320px;height:32px;display:block;margin:2px 0 6px}
774
+ video{width:100%;max-width:480px;display:block;margin:2px 0 6px;background:#000;border-radius:4px}
775
+ img.generated{width:100%;max-width:480px;height:auto;display:block;margin:2px 0 6px;border-radius:4px}
776
+ .spec{font-size:12px;color:var(--muted);margin:4px 0;overflow-wrap:anywhere}
777
+ button.play{font:12px system-ui,sans-serif;border:1px solid var(--line);border-radius:999px;padding:2px 10px;
778
+ background:var(--panel);color:var(--fg);cursor:pointer;margin:4px 0}
779
+ .cc-s{border-style:dotted}
780
+ section.bucket{content-visibility:auto;contain-intrinsic-size:auto 1200px}
781
+ .legend{font-size:12px;color:var(--muted);display:flex;flex-wrap:wrap;gap:4px 14px;margin:8px 0}
782
+ footer{color:var(--muted);font-size:12px;padding-top:16px;padding-bottom:32px;overflow-wrap:anywhere}
783
+ footer code{font-size:11px}
784
+ [hidden]{display:none!important}
785
+ @media (max-width:760px){
786
+ table.grid{min-width:0;table-layout:auto}
787
+ table.grid colgroup,table.grid thead{display:none}
788
+ table.grid,table.grid tbody,table.grid tr,table.grid th,table.grid td{display:block;width:100%}
789
+ table.grid tr{border-bottom:2px solid var(--line)}
790
+ table.grid th.input{border-bottom:1px dashed var(--line)}
791
+ .cell-head{display:block}
792
+ .text{max-height:16em}
793
+ .bar{position:static}
794
+ }
795
+ """
796
+
797
+ JS = """
798
+ (function(){
799
+ var hide={b:{},e:{}},onlyDiff=false;
800
+ function apply(){
801
+ document.querySelectorAll('section.bucket').forEach(function(s){s.hidden=!!hide.b[s.dataset.b];});
802
+ document.querySelectorAll('[data-e]').forEach(function(el){el.hidden=!!hide.e[el.dataset.e];});
803
+ document.querySelectorAll('tr.item').forEach(function(r){r.hidden=onlyDiff&&r.dataset.flag!=='1';});
804
+ }
805
+ document.querySelectorAll('input[data-fb]').forEach(function(cb){cb.addEventListener('change',function(){
806
+ hide.b[cb.dataset.fb]=!cb.checked;apply();});});
807
+ document.querySelectorAll('input[data-fe]').forEach(function(cb){cb.addEventListener('change',function(){
808
+ hide.e[cb.dataset.fe]=!cb.checked;apply();});});
809
+ var d=document.getElementById('only-diff');
810
+ if(d){d.addEventListener('change',function(){onlyDiff=d.checked;apply();});}
811
+ document.querySelectorAll('button[data-play]').forEach(function(b){b.addEventListener('click',function(){
812
+ var row=b.closest('tr');if(!row){return;}
813
+ row.querySelectorAll('video').forEach(function(v){try{v.currentTime=0;}catch(e){}var p=v.play();
814
+ if(p&&p.catch){p.catch(function(){});}});});});
815
+ var w=document.getElementById('worst-first');
816
+ if(w){w.addEventListener('change',function(){
817
+ document.querySelectorAll('table.grid tbody').forEach(function(tb){
818
+ var rows=Array.prototype.slice.call(tb.querySelectorAll('tr.item'));
819
+ rows.sort(function(a,b){return w.checked?(parseFloat(b.dataset.sev)-parseFloat(a.dataset.sev))||
820
+ (parseInt(a.dataset.i,10)-parseInt(b.dataset.i,10)):(parseInt(a.dataset.i,10)-parseInt(b.dataset.i,10));});
821
+ rows.forEach(function(r){tb.appendChild(r);});
822
+ });});}
823
+ })();
824
+ """
825
+
826
+
827
+ def render(run: Run, generated: str, tool_version: str) -> tuple[str, dict]:
828
+ """(html, data): the page and a machine-readable copy of what it shows."""
829
+ infos = {(info.label, run.key(item)): compare_cell(run, item, run.cells[(info.label, run.key(item))])
830
+ for item in run.items for info in run.engines}
831
+ summaries = [engine_summary(run, e.label, infos) for e in run.engines]
832
+ buckets = run.buckets()
833
+ labels = [e.label for e in run.engines]
834
+
835
+ filters = ['<div class="grp"><span class="lbl">Buckets</span>']
836
+ for bi, b in enumerate(buckets):
837
+ n = sum(1 for i in run.items if i.bucket == b)
838
+ filters.append(f'<label><input type="checkbox" checked data-fb="{bi}">{esc(bucket_title(run, b))} '
839
+ f'({n})</label>')
840
+ filters.append('</div><div class="grp"><span class="lbl">Engines</span>')
841
+ for ei, label in enumerate(labels):
842
+ filters.append(f'<label><input type="checkbox" checked data-fe="{ei}">{esc(label)}</label>')
843
+ filters.append('</div><div class="grp"><label><input type="checkbox" id="only-diff">only rows that differ, '
844
+ 'fail or were not run</label><label><input type="checkbox" id="worst-first">worst first</label>'
845
+ '</div>')
846
+
847
+ sections = []
848
+ for bi, b in enumerate(buckets):
849
+ rows = []
850
+ for item in (i for i in run.items if i.bucket == b):
851
+ tds, flag, sev = [], False, 0.0
852
+ for ei, label in enumerate(labels):
853
+ cell = run.cells[(label, run.key(item))]
854
+ info = infos[(label, run.key(item))]
855
+ if (cell.status != "ok" or info.get("identical") is False or (info.get("wer") or 0) > 0
856
+ or info.get("effective_mismatch") or info.get("cached")):
857
+ flag = True # a missing reference output also leaves the row unchecked
858
+ sev = max(sev, severity(cell, info))
859
+ tds.append(cell_html(run, item, cell, info, ei))
860
+ rows.append(f'<tr class="item" data-i="{len(rows)}" data-flag="{1 if flag else 0}" data-sev="{sev:.4f}">'
861
+ f'{input_html(run, item)}{"".join(tds)}</tr>')
862
+ cols = '<col class="c-in">' + "<col>" * len(labels)
863
+ ths = '<th scope="col">input</th>' + "".join(
864
+ f'<th scope="col" data-e="{ei}">{esc(label)}' + (" (reference)" if label == run.reference
865
+ and label != "reference" else "")
866
+ + "</th>" for ei, label in enumerate(labels))
867
+ n = len(rows)
868
+ seeds = sorted({str(i.clip["seed"]) for i in run.items if i.bucket == b and i.clip
869
+ and i.clip.get("seed") is not None}) if run.modality == "video" else []
870
+ seed = f", seed {', '.join(seeds[:3])}" if seeds else ""
871
+ sections.append(f'<section class="bucket" data-b="{bi}"><h2>{esc(bucket_title(run, b))} '
872
+ f'<span class="sub">({n} input{"s" if n != 1 else ""}{esc(seed)})</span></h2>'
873
+ f'<div class="scroll"><table class="grid" style="--cols:{len(labels)}"><colgroup>{cols}'
874
+ f'</colgroup><thead><tr>{ths}</tr></thead><tbody>{"".join(rows)}</tbody></table></div>'
875
+ f'</section>')
876
+ if not run.items:
877
+ sections.append('<p class="na">No inputs or outputs found in this run directory.</p>')
878
+
879
+ name = run.root.resolve().name
880
+ warnings = "".join(f"<li>{esc(w)}</li>" for w in run.warnings[:30])
881
+ if len(run.warnings) > 30:
882
+ warnings += f"<li>... and {len(run.warnings) - 30} more (see review/data.json)</li>"
883
+ if run.modality == "video":
884
+ legend = ('<div class="legend"><span>one table per round: every clip in a row was asked for with the same '
885
+ 'seed, prompt and shape</span><span>"play row" starts every clip of the row together</span>'
886
+ f'<span>{cc("o")} observed (client timer, quality clip) {cc("d")} derived (computed by this page) '
887
+ f'{cc("s")} self-reported by the engine</span></div>')
888
+ elif run.modality == "image":
889
+ legend = ('<div class="legend"><span>open an image for full resolution; viewing it does not establish '
890
+ 'semantic quality</span>'
891
+ f'<span>{cc("o")} observed (client timer) {cc("d")} derived</span></div>')
892
+ else:
893
+ legend = ('<div class="legend"><span><ins>inserted</ins> / <del>deleted</del> vs the reference</span>'
894
+ f'<span>{MARK} first divergence</span>'
895
+ '<span><span class="w-sub">substitution</span> <span class="w-ins">insertion</span> '
896
+ '<span class="w-del">deletion</span> vs the ground truth</span>'
897
+ f'<span>{cc("o")} observed (metrics files) {cc("d")} derived (computed by this page)</span></div>')
898
+ ev_lines = "".join(f"<div>{esc(s['engine'])}: <code>{esc(', '.join(s['evidence']))}</code></div>"
899
+ for s in summaries if s["evidence"])
900
+ page = (
901
+ "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
902
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
903
+ "<meta name=\"color-scheme\" content=\"light dark\">"
904
+ f"<title>Review {esc(name)}</title><style>{CSS}</style></head><body>"
905
+ f"<header><h1>Review: {esc(name)}</h1><div class=\"meta\"><span>modality <b>{esc(run.modality)}</b></span>"
906
+ f"<span>reference <b>{esc(run.reference or 'none')}</b></span><span>{len(run.items)} inputs</span>"
907
+ f"<span>{len(labels)} column{'s' if len(labels) != 1 else ''}</span>"
908
+ f"<span>built {esc(generated)} by inferencekit {esc(tool_version)}</span></div>"
909
+ + (f'<ul class="warnings">{warnings}</ul>' if warnings else "")
910
+ + f"<h2>Summary</h2>{summary_html(run, summaries)}{legend}</header>"
911
+ f'<nav class="bar" aria-label="filters">{"".join(filters)}</nav>'
912
+ f"<main>{''.join(sections)}</main>"
913
+ f"<footer>{ev_lines}<div>Built from {len(run.sources)} files under the run directory; their sha256 "
914
+ "digests are in review/data.json. Verdicts: PASS / FAIL / UNPROVEN / NOT RUN; a missing output is NOT RUN, "
915
+ f"never a pass.</div></footer><script>{JS}</script></body></html>\n")
916
+
917
+ data = {"run": name, "modality": run.modality, "reference": run.reference, "engines": summaries,
918
+ "buckets": buckets, "warnings": run.warnings,
919
+ "cells": [{k: v for k, v in info.items() if k not in ("diff", "alignment")} for info in infos.values()]}
920
+ return page, data