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.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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])}… '
|
|
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])}–{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)} ≤ {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
|