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,844 @@
1
+ """Read a run directory (``runs/<run-id>/``) into the rows and columns of the review page.
2
+
3
+ Layout read:
4
+
5
+ inputs/<bucket>/<id>.json | .txt | .wav exact request bodies, texts or audio
6
+ outputs/<engine>/<bucket>/<id>.json | .txt | .wav
7
+ metrics/<engine>.json | .jsonl speed + quality per input and per bucket
8
+ manifest.json, intake.yaml optional: engine order, roles, modality
9
+
10
+ Video clips (bench clip's layout) use the round as the bucket: ``outputs/<engine>/r<k>/<id>.mp4``,
11
+ the exact request body each engine was sent at ``inputs/<engine>/r<k>/<id>.json`` (rows come from
12
+ those when there is no ``inputs/r<k>/<id>.json``), ``metrics/<engine>.clip.json`` for the speed
13
+ summary, and per-clip times and self-reported stages from the bench clip record's ``requests`` rows.
14
+
15
+ Everything is read defensively, by field presence: a missing file is a NOT RUN cell, an
16
+ unreadable one is a FAILED cell with the reason, and unknown fields are ignored. The
17
+ ``metrics/<engine>.json`` fields this module understands are listed in ``load_metrics``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import re
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+
27
+ BUCKET_ORDER = ("very_short", "short", "medium", "long")
28
+ AUDIO_EXT = (".wav", ".flac", ".mp3", ".ogg", ".opus")
29
+ VIDEO_EXT = (".mp4", ".webm", ".mov", ".mkv", ".m4v")
30
+ IMAGE_EXT = (".png",)
31
+ MODALITIES = ("llm", "tts", "stt", "video", "image")
32
+ ROUND_DIR = re.compile(r"^r\d+$") # bench clip: outputs/<label>/r<round>/<id>.<fmt>
33
+ # What a clip request body says about the clip (bench clip's inputs/<label>/r<k>/<id>.json), shown with the input.
34
+ CLIP_FIELDS = ("task", "width", "height", "num_frames", "fps", "steps", "guidance_scale", "seed", "noise_seed",
35
+ "audio", "negative_prompt", "sampler", "shift")
36
+ GROUND_TRUTH_SUFFIXES = ("gt.txt", "ref.txt", "truth.txt", "transcript.txt")
37
+ REFERENCE_DEFAULT = "reference"
38
+ _NATURAL = re.compile(r"(\d+)")
39
+
40
+
41
+ def natural_key(s: str):
42
+ return [int(p) if p.isdigit() else p.lower() for p in _NATURAL.split(s)]
43
+
44
+
45
+ def bucket_key(b: str):
46
+ # Known buckets in their order, then the rest in natural order (bench clip's rounds: r2 before r10).
47
+ return (BUCKET_ORDER.index(b), []) if b in BUCKET_ORDER else (len(BUCKET_ORDER), natural_key(b))
48
+
49
+
50
+ @dataclass
51
+ class Item:
52
+ """One input (a row): what was sent, and the ground truth when there is one."""
53
+
54
+ id: str
55
+ bucket: str
56
+ text: str | None = None # the prompt / the text to speak, flattened for display
57
+ messages: list | None = None # chat messages, when the input is a chat request body
58
+ audio: str | None = None # run-dir-relative path of an input audio file
59
+ ground_truth: str | None = None # expected transcript (STT) or the text itself (TTS)
60
+ clip: dict | None = None # video: what the clip request asked for (task, shape, steps, seed, image)
61
+ files: list[str] = field(default_factory=list)
62
+
63
+
64
+ @dataclass
65
+ class Cell:
66
+ """One engine's output for one input."""
67
+
68
+ engine: str
69
+ status: str = "not_run" # ok | not_run | failed
70
+ reason: str = ""
71
+ text: str | None = None
72
+ token_ids: list | None = None
73
+ pieces: list[str] | None = None # per-token text, when the output carries it (logprobs)
74
+ audio: str | None = None
75
+ video: str | None = None # run-dir-relative path of the generated clip
76
+ image: str | None = None
77
+ pcm: str | None = None # raw PCM needs verified index metadata before playback
78
+ clip_row: dict | None = None # video: bench clip's row for this clip (client time, stages, effective)
79
+ transcript: str | None = None # ASR of generated audio (TTS) or the transcript itself (STT)
80
+ finish_reason: str | None = None
81
+ reasoning: str | None = None # a reasoning model's separate thinking text, when the output has it
82
+ metrics: dict = field(default_factory=dict)
83
+ files: list[str] = field(default_factory=list)
84
+
85
+
86
+ @dataclass
87
+ class EngineInfo:
88
+ label: str
89
+ role: str | None = None
90
+ metrics: dict = field(default_factory=dict) # top-level fields of metrics/<engine>.json
91
+ per_input: dict = field(default_factory=dict) # input id -> {metric: value}
92
+ per_bucket: dict = field(default_factory=dict) # bucket -> {metric: value}
93
+ metrics_file: str | None = None
94
+ # From evidence records about this label (run-dir evidence/ or --evidence):
95
+ ev_quality: list = field(default_factory=list) # [{name, verdict, value, unit, evidence}]
96
+ ev_gates: dict | None = None # {verdict, violations, evidence}
97
+ capacity: dict | None = None # {n_star, reason, level_kind, slo, usd_per_1m, price, ...}
98
+ evidence: list = field(default_factory=list) # evidence ids that mention this label
99
+ clip_rows: dict = field(default_factory=dict) # video: "r<k>/<id>" -> bench clip request row
100
+ stages: dict = field(default_factory=dict) # video: stage -> median ms over clips (self-reported)
101
+ media_rows: dict = field(default_factory=dict)
102
+ media_kind: str | None = None
103
+ metric_classes: dict = field(default_factory=dict)
104
+
105
+
106
+ @dataclass
107
+ class Run:
108
+ root: Path
109
+ modality: str
110
+ engines: list[EngineInfo]
111
+ reference: str | None
112
+ items: list[Item]
113
+ cells: dict[tuple[str, str], Cell]
114
+ warnings: list[str] = field(default_factory=list)
115
+ sources: list[str] = field(default_factory=list) # run-relative paths read to build the page
116
+ evidence_attached: int = 0 # evidence records read (evidence/, --evidence, the store)
117
+
118
+ def buckets(self) -> list[str]:
119
+ return sorted({i.bucket for i in self.items}, key=bucket_key)
120
+
121
+ def engine(self, label: str) -> EngineInfo | None:
122
+ return next((e for e in self.engines if e.label == label), None)
123
+
124
+ def key(self, item: Item) -> str:
125
+ return f"{item.bucket}/{item.id}"
126
+
127
+
128
+ # --------------------------------------------------------------------------- helpers
129
+
130
+ def _read_json(path: Path):
131
+ return json.loads(path.read_text(encoding="utf-8"))
132
+
133
+
134
+ def _split_name(name: str) -> tuple[str, str]:
135
+ """'short-0007.gt.txt' -> ('short-0007', 'gt.txt'). A clip ('scene.01.mp4' -> ('scene.01', 'mp4')) keeps
136
+ every dot but the last: bench clip ids may contain dots, and a clip file is always <id>.<video ext>."""
137
+ if Path(name).suffix.lower() in VIDEO_EXT + IMAGE_EXT + (".pcm",):
138
+ return name[:-len(Path(name).suffix)], Path(name).suffix.lower()[1:]
139
+ stem, _, suffix = name.partition(".")
140
+ return stem, suffix.lower()
141
+
142
+
143
+ def flatten_messages(messages) -> str:
144
+ parts = []
145
+ for m in messages or []:
146
+ if not isinstance(m, dict):
147
+ continue
148
+ content = m.get("content")
149
+ if isinstance(content, list): # OpenAI content parts
150
+ content = " ".join(str(p.get("text", "")) for p in content if isinstance(p, dict))
151
+ parts.append(f"{m.get('role', '?')}: {content if content is not None else ''}")
152
+ return "\n\n".join(parts)
153
+
154
+
155
+ def token_pieces(obj: dict) -> list[str] | None:
156
+ """Per-token text of an output, from explicit fields or OpenAI-style logprobs."""
157
+ for key in ("token_texts", "tokens"):
158
+ v = obj.get(key)
159
+ if isinstance(v, list) and v and all(isinstance(x, str) for x in v):
160
+ return v
161
+ lp = obj.get("logprobs")
162
+ if isinstance(lp, dict):
163
+ content = lp.get("content")
164
+ if isinstance(content, list) and content and all(isinstance(c, dict) and "token" in c for c in content):
165
+ return [str(c["token"]) for c in content]
166
+ toks = lp.get("tokens")
167
+ if isinstance(toks, list) and toks and all(isinstance(x, str) for x in toks):
168
+ return toks
169
+ return None
170
+
171
+
172
+ def _output_text(obj: dict) -> str | None:
173
+ if isinstance(obj.get("text"), str):
174
+ return obj["text"]
175
+ choices = obj.get("choices") # a raw OpenAI response saved as-is
176
+ if isinstance(choices, list) and choices and isinstance(choices[0], dict):
177
+ c = choices[0]
178
+ if isinstance(c.get("message"), dict) and isinstance(c["message"].get("content"), str):
179
+ return c["message"]["content"]
180
+ if isinstance(c.get("text"), str):
181
+ return c["text"]
182
+ return None
183
+
184
+
185
+ def _ids(value) -> list | None:
186
+ return value if isinstance(value, list) and all(isinstance(x, int) for x in value) else None
187
+
188
+
189
+ def detect_modality(root: Path, items: list[Item], cells: dict) -> str:
190
+ for name in ("manifest.json", "run.json"):
191
+ p = root / name
192
+ if p.is_file():
193
+ try:
194
+ m = _read_json(p)
195
+ mod = m.get("modality") or (m.get("model") or {}).get("modality")
196
+ if mod in MODALITIES:
197
+ return mod
198
+ except (OSError, ValueError, AttributeError):
199
+ pass
200
+ intake = root / "intake.yaml"
201
+ if intake.is_file(): # one top-level key; no YAML parser needed for it
202
+ found = re.search(r"(?m)^modality:\s*['\"]?(llm|tts|stt|video|image)\b", intake.read_text(errors="replace"))
203
+ if found:
204
+ return found.group(1)
205
+ if any(c.video for c in cells.values()):
206
+ return "video"
207
+ if any(c.image for c in cells.values()) or any((root / "metrics").glob("*.image.json")):
208
+ return "image"
209
+ if any(c.audio or c.pcm for c in cells.values()) or any((root / "metrics").glob("*.audio.json")):
210
+ return "tts"
211
+ if any(i.audio for i in items):
212
+ return "stt"
213
+ return "llm"
214
+
215
+
216
+ # --------------------------------------------------------------------------- loading
217
+
218
+ def load_prompt_set(path: Path, items: dict[str, Item], run: Run) -> None:
219
+ """Fill rows from a prompt-set JSONL ({"id", "bucket", "messages" | "prompt" | "text"} per line)."""
220
+ from inferencekit import prompts as sets
221
+
222
+ try:
223
+ lines = sets.read_text(sets.existing(path)).splitlines() # .jsonl or .jsonl.gz
224
+ except (OSError, ValueError) as e: # missing, corrupt .jsonl.gz, or not UTF-8
225
+ run.warnings.append(f"prompt set {path.name}: unreadable ({e})")
226
+ return
227
+ for n, line in enumerate(lines, 1):
228
+ if not line.strip():
229
+ continue
230
+ try:
231
+ obj = json.loads(line)
232
+ except ValueError:
233
+ run.warnings.append(f"prompt set {path.name}:{n}: not JSON")
234
+ continue
235
+ if not isinstance(obj, dict) or not obj.get("id"):
236
+ continue
237
+ # Unbucketed media fixtures apply to every observed round of that id.
238
+ targets = [i for i in items.values() if i.id == str(obj["id"]) and ROUND_DIR.match(i.bucket)] \
239
+ if not obj.get("bucket") else []
240
+ if not targets:
241
+ targets = [items.setdefault(f"{obj.get('bucket') or 'unbucketed'}/{obj['id']}",
242
+ Item(id=str(obj["id"]), bucket=str(obj.get("bucket") or "unbucketed")))]
243
+ for item in targets:
244
+ if item.text is None:
245
+ if isinstance(obj.get("messages"), list):
246
+ item.messages = obj["messages"]
247
+ item.text = flatten_messages(obj["messages"])
248
+ elif isinstance(obj.get("prompt") or obj.get("text"), str):
249
+ item.text = obj.get("prompt") or obj.get("text")
250
+ if item.ground_truth is None and isinstance(obj.get("ground_truth"), str):
251
+ item.ground_truth = obj["ground_truth"]
252
+
253
+
254
+ def load_inputs(root: Path, run: Run) -> dict[str, Item]:
255
+ items: dict[str, Item] = {}
256
+ base = root / "inputs"
257
+ if not base.is_dir():
258
+ return items
259
+ # bench serve also keeps each engine's exact request bodies at inputs/<label>/<bucket>/<id>.json (and
260
+ # inputs/<label>/load-<level>/...); those are per-engine copies, not inputs: only inputs/<bucket>/<id>
261
+ # (the shared path) and inputs/<id> are rows.
262
+ out_dir = root / "outputs"
263
+ labels = {d.name for d in out_dir.iterdir() if d.is_dir()} if out_dir.is_dir() else set()
264
+ for ending in (".clip.json", ".image.json", ".audio.json"):
265
+ labels.update(p.name[:-len(ending)] for p in (root / "metrics").glob(f"*{ending}"))
266
+ clip_copies: dict[str, list[tuple[str, dict]]] = {} # "r<k>/<id>" -> [(path, clip request body)] per engine
267
+ for path in sorted(p for p in base.rglob("*") if p.is_file()):
268
+ rel = path.relative_to(base)
269
+ if (len(rel.parts) == 3 and rel.parts[0] in labels and ROUND_DIR.match(rel.parts[1])
270
+ and path.suffix == ".json"):
271
+ body = _clip_body(path) # bench clip: the exact body this engine was sent for this clip and round
272
+ if body is not None: # <id>.json, and the id may contain dots
273
+ clip_copies.setdefault(f"{rel.parts[1]}/{path.name[:-len('.json')]}", []).append(
274
+ (path.relative_to(root).as_posix(), body))
275
+ continue
276
+ if len(rel.parts) > 2 or (len(rel.parts) == 2 and rel.parts[0] in labels):
277
+ continue
278
+ bucket = rel.parts[0] if len(rel.parts) > 1 else "unbucketed"
279
+ stem, suffix = _split_name(path.name)
280
+ if ROUND_DIR.match(bucket) and path.suffix == ".json": # a clip request r<k>/<id>.json: the id may hold dots
281
+ stem, suffix = path.name[:-len(".json")], "json"
282
+ item = items.setdefault(f"{bucket}/{stem}", Item(id=stem, bucket=bucket))
283
+ rel_root = path.relative_to(root).as_posix()
284
+ item.files.append(rel_root)
285
+ run.sources.append(rel_root)
286
+ try:
287
+ if suffix in GROUND_TRUTH_SUFFIXES:
288
+ item.ground_truth = path.read_text(encoding="utf-8", errors="replace").strip()
289
+ elif suffix == "json":
290
+ body = _read_json(path)
291
+ if isinstance(body, dict):
292
+ if isinstance(body.get("messages"), list):
293
+ item.messages = body["messages"]
294
+ item.text = flatten_messages(body["messages"])
295
+ elif isinstance(body.get("prompt"), str):
296
+ item.text = body["prompt"]
297
+ elif isinstance(body.get("text"), str) or isinstance(body.get("input"), str):
298
+ item.text = body.get("text") if isinstance(body.get("text"), str) else body["input"]
299
+ if "num_frames" in body or ("width" in body and "height" in body):
300
+ item.clip = clip_spec(body)
301
+ for k in ("ground_truth", "reference_text", "transcript"):
302
+ if isinstance(body.get(k), str):
303
+ item.ground_truth = body[k]
304
+ break
305
+ elif suffix == "txt":
306
+ item.text = path.read_text(encoding="utf-8", errors="replace")
307
+ elif path.suffix.lower() in AUDIO_EXT and not item.audio:
308
+ item.audio = rel_root
309
+ except (OSError, ValueError) as e:
310
+ run.warnings.append(f"{rel_root}: unreadable ({e})")
311
+ for key, copies in sorted(clip_copies.items()):
312
+ if key in items: # the shared path inputs/r<k>/<id>.json wins over the per-engine copies
313
+ continue
314
+ bucket, _, cid = key.partition("/")
315
+ first_path, body = copies[0]
316
+ item = items[key] = Item(id=cid, bucket=bucket, text=body["prompt"] if isinstance(body.get("prompt"), str)
317
+ else None, clip=clip_spec(body), files=[p for p, _ in copies])
318
+ run.sources += item.files
319
+ differ = [p for p, b in copies[1:] if clip_spec(b) != item.clip or b.get("prompt") != body.get("prompt")]
320
+ if differ:
321
+ run.warnings.append(f"{key}: the engines were not sent the same clip request ({first_path} differs from "
322
+ f"{', '.join(differ[:3])}): their clips are not comparable")
323
+ for item in items.values():
324
+ if item.audio and item.ground_truth is None and item.text is not None and item.messages is None:
325
+ item.ground_truth = item.text.strip() # STT: the .txt next to the audio is its transcript
326
+ return items
327
+
328
+
329
+ def _clip_body(path: Path) -> dict | None:
330
+ """A saved video/image request body, or None for e.g. a chat request copy."""
331
+ try:
332
+ body = _read_json(path)
333
+ except (OSError, ValueError):
334
+ return None
335
+ return body if isinstance(body, dict) and ("num_frames" in body or
336
+ ("width" in body and "height" in body)) else None
337
+
338
+
339
+ def clip_spec(body: dict) -> dict:
340
+ """The fields of a clip request shown with the input (the image as its sha256 and size, never its bytes)."""
341
+ spec = {k: body[k] for k in CLIP_FIELDS if k in body and isinstance(body[k], (str, int, float, bool))}
342
+ image = body.get("image")
343
+ if isinstance(image, dict) and isinstance(image.get("sha256"), str):
344
+ spec["image_sha256"] = image["sha256"]
345
+ if isinstance(image.get("bytes"), int):
346
+ spec["image_bytes"] = image["bytes"]
347
+ return spec
348
+
349
+
350
+ def load_metrics(root: Path, label: str, run: Run) -> EngineInfo:
351
+ """metrics/<label>.json or .jsonl.
352
+
353
+ Understood fields (all optional):
354
+ per_input | inputs {id: {metric: value}} or [{"id": ..., metric: value}]
355
+ per_bucket | buckets {bucket: {metric: value}}
356
+ quality {name: verdict | {"verdict", "value", "unit"}} or a list of those
357
+ gates "PASS" or {"verdict", "violations"}
358
+ cost {"usd_per_1m_output_tokens" | "usd_per_1m": x, "gpu_price_usd_h": y} or a number
359
+ ceiling_tok_s, pct_of_ceiling, role, evidence (list of ids)
360
+ A value is a number or {"value"|"median": x, "ci95": [lo, hi]}. A .jsonl file holds one
361
+ {"id": ..., metric: value} line per input. Video: ``metrics/<label>.clip.json`` (bench clip:
362
+ {"speed": {"all": {clip_ms, clips_per_hour}, "stages_self_reported": ...}, "rounds": [...]}) is
363
+ read as well, for the summary row and the per-round values.
364
+ """
365
+ info = EngineInfo(label=label)
366
+ load_clip_metrics(root, label, info, run)
367
+ jpath, lpath = root / "metrics" / f"{label}.json", root / "metrics" / f"{label}.jsonl"
368
+ try:
369
+ if jpath.is_file():
370
+ data = _read_json(jpath)
371
+ info.metrics_file = jpath.relative_to(root).as_posix()
372
+ if not isinstance(data, dict):
373
+ raise ValueError("not a JSON object")
374
+ per_input = data.get("per_input", data.get("inputs"))
375
+ per_bucket = data.get("per_bucket", data.get("buckets"))
376
+ speed = data.get("speed") # bench serve: {"speed": {"per_bucket": ..., "per_input_median": ...}}
377
+ if isinstance(speed, dict):
378
+ per_input = per_input if per_input is not None else speed.get("per_input_median",
379
+ speed.get("per_input"))
380
+ per_bucket = per_bucket if per_bucket is not None else speed.get("per_bucket")
381
+ info.metrics = {k: v for k, v in data.items()
382
+ if k not in ("per_input", "inputs", "per_bucket", "buckets", "speed")}
383
+ elif lpath.is_file():
384
+ info.metrics_file = lpath.relative_to(root).as_posix()
385
+ per_input = [json.loads(line) for line in lpath.read_text(encoding="utf-8").splitlines() if line.strip()]
386
+ per_bucket = None
387
+ else:
388
+ return info
389
+ except (OSError, ValueError) as e:
390
+ run.warnings.append(f"metrics/{label}: unreadable ({e})")
391
+ return info
392
+ run.sources.append(info.metrics_file)
393
+ clip_bucket = dict(info.per_bucket) # what the clip summary filled; the metrics file adds to it
394
+ if isinstance(per_input, dict):
395
+ info.per_input = {str(k): v for k, v in per_input.items() if isinstance(v, dict)}
396
+ elif isinstance(per_input, list):
397
+ for row in per_input:
398
+ if isinstance(row, dict) and (row.get("id") or row.get("input_id")):
399
+ rid = str(row.get("id") or row.get("input_id"))
400
+ info.per_input[rid] = {k: v for k, v in row.items() if k not in ("id", "input_id")}
401
+ if isinstance(per_bucket, dict):
402
+ info.per_bucket = {**clip_bucket, **{str(k): v for k, v in per_bucket.items() if isinstance(v, dict)}}
403
+ if isinstance(info.metrics.get("role"), str):
404
+ info.role = info.metrics["role"]
405
+ return info
406
+
407
+
408
+ def load_clip_metrics(root: Path, label: str, info: EngineInfo, run: Run) -> None:
409
+ """metrics/<label>.clip.json (bench clip --run-dir): the 'all' speed (clip time in s, clips per hour), each
410
+ round's values as that round's bucket, and the self-reported stage medians. All observed or derived by bench
411
+ clip; the page converts ms to s only."""
412
+ path = root / "metrics" / f"{label}.clip.json"
413
+ if not path.is_file():
414
+ return
415
+ try:
416
+ data = _read_json(path)
417
+ if not isinstance(data, dict):
418
+ raise ValueError("not a JSON object")
419
+ except (OSError, ValueError) as e:
420
+ run.warnings.append(f"metrics/{label}.clip.json: unreadable ({e})")
421
+ return
422
+ rel = path.relative_to(root).as_posix()
423
+ run.sources.append(rel)
424
+ info.metrics_file = info.metrics_file or rel
425
+ speed = data.get("speed") if isinstance(data.get("speed"), dict) else {}
426
+ overall = speed.get("all") if isinstance(speed.get("all"), dict) else {}
427
+ row = {}
428
+ cm = overall.get("clip_ms")
429
+ if isinstance(cm, dict) and isinstance(cm.get("median"), (int, float)):
430
+ ci = cm.get("ci95_median")
431
+ ok = isinstance(ci, list) and len(ci) == 2 and all(isinstance(c, (int, float)) for c in ci)
432
+ row["clip_s"] = {"value": cm["median"] / 1000, "ci95": [ci[0] / 1000, ci[1] / 1000] if ok else None}
433
+ cph = overall.get("clips_per_hour")
434
+ if isinstance(cph, dict) and isinstance(cph.get("median"), (int, float)):
435
+ ci = cph.get("ci95_median")
436
+ row["clips_per_hour"] = {"value": cph["median"], "ci95": ci if isinstance(ci, list) and len(ci) == 2
437
+ and all(isinstance(c, (int, float)) for c in ci) else None}
438
+ if row:
439
+ info.per_bucket["all"] = row
440
+ for r in data.get("rounds") or []:
441
+ if isinstance(r, dict) and isinstance(r.get("index"), int):
442
+ vals = {}
443
+ if isinstance(r.get("clip_ms"), (int, float)):
444
+ vals["clip_s"] = r["clip_ms"] / 1000
445
+ if isinstance(r.get("clips_per_hour"), (int, float)):
446
+ vals["clips_per_hour"] = r["clips_per_hour"]
447
+ if vals:
448
+ info.per_bucket[f"r{r['index']}"] = vals
449
+ stages = speed.get("stages_self_reported")
450
+ if isinstance(stages, dict):
451
+ info.stages = {str(k): float(v["median"]) for k, v in stages.items()
452
+ if isinstance(v, dict) and isinstance(v.get("median"), (int, float))}
453
+
454
+
455
+ def load_cell(root: Path, engine: str, item: Item, run: Run) -> Cell:
456
+ cell = Cell(engine=engine)
457
+ folder = root / "outputs" / engine / item.bucket
458
+ if item.bucket == "unbucketed":
459
+ folder = root / "outputs" / engine
460
+ found = sorted(p for p in folder.glob(f"{item.id}.*") if p.is_file()) if folder.is_dir() else []
461
+ found = [p for p in found if _split_name(p.name)[0] == item.id or p.name == f"{item.id}.json"]
462
+ for path in found:
463
+ rel = path.relative_to(root).as_posix()
464
+ cell.files.append(rel)
465
+ run.sources.append(rel)
466
+ _, suffix = _split_name(path.name)
467
+ if path.name == f"{item.id}.json":
468
+ suffix = "json"
469
+ try:
470
+ if suffix == "json":
471
+ obj = _read_json(path)
472
+ if not isinstance(obj, dict):
473
+ raise ValueError("not a JSON object")
474
+ if obj.get("error"):
475
+ cell.status, cell.reason = "failed", str(obj["error"])
476
+ cell.text = _output_text(obj) if cell.text is None else cell.text
477
+ cell.token_ids = _ids(obj.get("token_ids")) or cell.token_ids
478
+ cell.pieces = token_pieces(obj) or cell.pieces
479
+ cell.finish_reason = obj.get("finish_reason") if isinstance(obj.get("finish_reason"), str) else None
480
+ for k in ("reasoning", "reasoning_content"):
481
+ if isinstance(obj.get(k), str) and obj[k]:
482
+ cell.reasoning = obj[k]
483
+ for k in ("transcript", "asr_text"):
484
+ if isinstance(obj.get(k), str):
485
+ cell.transcript = obj[k]
486
+ elif suffix in ("asr.txt", "transcript.txt"):
487
+ cell.transcript = path.read_text(encoding="utf-8", errors="replace")
488
+ elif suffix in GROUND_TRUTH_SUFFIXES:
489
+ continue
490
+ elif suffix == "txt":
491
+ if cell.text is None:
492
+ cell.text = path.read_text(encoding="utf-8", errors="replace")
493
+ elif path.suffix.lower() in AUDIO_EXT and not cell.audio:
494
+ cell.audio = rel
495
+ elif path.suffix.lower() in VIDEO_EXT and not cell.video:
496
+ cell.video = rel
497
+ elif path.suffix.lower() in IMAGE_EXT and not cell.image:
498
+ cell.image = rel
499
+ elif path.suffix.lower() == ".pcm" and not cell.pcm:
500
+ cell.pcm = rel
501
+ except (OSError, ValueError) as e:
502
+ cell.status, cell.reason = "failed", f"{rel}: unreadable ({e})"
503
+ info = run.engine(engine)
504
+ if info is not None:
505
+ cell.metrics = dict(info.per_input.get(item.id) or info.per_input.get(f"{item.bucket}/{item.id}") or {})
506
+ for k in ("transcript", "asr_text"):
507
+ if cell.transcript is None and isinstance(cell.metrics.get(k), str):
508
+ cell.transcript = cell.metrics[k]
509
+ if cell.metrics.get("error") and cell.status != "failed":
510
+ cell.status, cell.reason = "failed", str(cell.metrics["error"])
511
+ if cell.status == "failed":
512
+ return cell
513
+ if cell.text is not None or cell.audio or cell.video or cell.image or cell.pcm or cell.token_ids is not None:
514
+ cell.status = "ok"
515
+ elif found:
516
+ cell.status, cell.reason = "failed", "output file has no text, token ids, audio, video or image"
517
+ else:
518
+ cell.status, cell.reason = "not_run", "no output file for this input"
519
+ return cell
520
+
521
+
522
+ def _manifest_engines(root: Path) -> list[dict]:
523
+ p = root / "manifest.json"
524
+ if not p.is_file():
525
+ return []
526
+ try:
527
+ engines = _read_json(p).get("engines")
528
+ except (OSError, ValueError, AttributeError):
529
+ return []
530
+ return [e for e in engines if isinstance(e, dict)] if isinstance(engines, list) else []
531
+
532
+
533
+ def _label_of(rec: dict) -> str | None:
534
+ data = rec.get("data") or {}
535
+ label = data.get("label") or ((rec.get("identity") or {}).get("engine") or {}).get("name")
536
+ return str(label) if label else None
537
+
538
+
539
+ def _metric(rec: dict, names, subject=None) -> float | None:
540
+ for name in names:
541
+ for m in rec.get("metrics") or []:
542
+ if (isinstance(m, dict) and m.get("name") == name and isinstance(m.get("value"), (int, float))
543
+ and (subject is None or m.get("subject") in (subject, None))):
544
+ return float(m["value"])
545
+ return None
546
+
547
+
548
+ def attach_evidence(run: Run, records: list[tuple[str, dict]]) -> None:
549
+ """Quality, gate and capacity verdicts per engine label, from evidence records (v1 or v2, by field presence)."""
550
+ from inferencekit.evidence import integrity_problem
551
+
552
+ seen = set()
553
+ for source, rec in records:
554
+ if not isinstance(rec, dict) or not str(rec.get("evidence_id", "")).startswith("ev_"):
555
+ continue
556
+ if rec["evidence_id"] in seen: # the same record from evidence/ and --evidence counts once
557
+ continue
558
+ seen.add(rec["evidence_id"])
559
+ run.evidence_attached += 1
560
+ data = rec.get("data") or {}
561
+ kind = data.get("v2_kind") or ("gates" if data.get("gates_format") else rec.get("kind"))
562
+ eid = rec["evidence_id"]
563
+ problem = integrity_problem(rec)
564
+ if problem:
565
+ run.warnings.append(f"{source}: {problem}: its verdicts are shown as UNPROVEN")
566
+ touched = []
567
+ if kind == "quality_trajectory":
568
+ for label, verdict in (data.get("verdicts") or {}).items():
569
+ info = run.engine(str(label))
570
+ if info is not None:
571
+ info.ev_quality.append({"name": "top-1 agreement", "unit": "%", "evidence": eid,
572
+ "verdict": "UNPROVEN" if problem else str(verdict),
573
+ "value": _metric(rec, ("top1_agreement_pct",), str(label))})
574
+ touched.append(info)
575
+ elif kind == "clip_quality":
576
+ touched += clip_quality(run, rec, source, problem)
577
+ else:
578
+ info = run.engine(_label_of(rec) or "")
579
+ if info is None:
580
+ continue
581
+ touched.append(info)
582
+ if kind == "clip_benchmark" and data.get("source") == "bench clip":
583
+ for key, row in clip_rows(run, rec, source).items(): # merged: quality clip may have come first
584
+ info.clip_rows.setdefault(key, {}).update(row)
585
+ elif kind in ("image_benchmark", "audio_benchmark"):
586
+ from inferencekit.review.media import attach_rows, attach_verdicts
587
+
588
+ media_kind = "image" if kind == "image_benchmark" else "audio"
589
+ if data.get("source") == f"bench {media_kind}":
590
+ attach_verdicts(info, media_kind, data, eid, problem)
591
+ if not problem:
592
+ attach_rows(run, info, rec, source)
593
+ elif kind == "quality_task":
594
+ name = str(data.get("task") or "task")
595
+ if name == "sampling":
596
+ value, unit = _metric(rec, ("first_token_tv_excess",), info.label), "probability"
597
+ else:
598
+ value, unit = _metric(rec, (f"{name}_accuracy_pct", "accuracy_pct", "niah_accuracy_pct"),
599
+ info.label), "%"
600
+ info.ev_quality.append({"name": name, "unit": unit, "evidence": eid, "value": value,
601
+ "verdict": "UNPROVEN" if problem else data.get("verdict")})
602
+ elif kind in ("gates", "clip_gates"):
603
+ gates = data.get("gates") or {}
604
+ info.ev_gates = {"verdict": "UNPROVEN" if problem else str(data.get("verdict") or "UNPROVEN"),
605
+ "violations": [f"{g}: {v.get('verdict')}" for g, v in gates.items()
606
+ if isinstance(v, dict) and v.get("verdict") != "PASS"],
607
+ "evidence": eid}
608
+ elif kind == "capacity":
609
+ # bench serve writes n_star=None for several reasons (SLO missed at the first level, no SLO,
610
+ # closed loop, no levels); data.reason says which, data.level_kind whether it is streams or req/s.
611
+ star = data["n_star"] if "n_star" in data else _metric(rec, ("n_star", "rate_star_rps"))
612
+ level_kind = data.get("level_kind") or ("rate" if _metric(rec, ("rate_star_rps",)) is not None
613
+ else "concurrency")
614
+ info.capacity = {"n_star": star, "reason": str(data["reason"]) if data.get("reason") else None,
615
+ "level_kind": level_kind, "slo": data.get("slo"), "evidence": eid,
616
+ "usd_per_1m": _metric(rec, ("usd_per_1m_output_tokens",)),
617
+ "price": data.get("gpu_price_usd_h"), "edited": bool(problem)}
618
+ for info in touched:
619
+ if eid not in info.evidence:
620
+ info.evidence.append(eid)
621
+
622
+
623
+ # Frame-quality metrics of a clip_quality record (quality clip): (name, unit, names it may carry), mean over a
624
+ # clip's frames, then over clips; psnr_db is the alias other writers use.
625
+ CLIP_QUALITY = (("psnr", "dB", ("psnr", "psnr_db")), ("ssim", "", ("ssim",)), ("lpips", "", ("lpips",)))
626
+ CLIP_ROW_KEYS = ("ok", "error", "seed", "e2e_ms", "stage_ms", "stage_sum_ms", "stage_sum_ok", "steps_run", "w", "h",
627
+ "frames", "fps", "dtype", "attention", "effective_mismatch", "cache", "out_sha256")
628
+ _QUALITY_KEY = re.compile(r"^engine:(.+)\|(r\d+/[^/]+)$") # quality clip's clip_metrics rows: engine:<label>|r<k>/<id>
629
+
630
+
631
+ def artifact_lines(run: Run, rec: dict, source: str, role: str, what: str) -> list[dict] | None:
632
+ """The JSON lines of a record's artifact with ``role``, found in the evidence store or, for a run directory
633
+ inside the store root (``runs/<run-id>``), next to it. The file must have the sha256 the record lists;
634
+ otherwise nothing is read and the page says why (``what`` names what is then not shown)."""
635
+ from inferencekit.canonical import file_sha256
636
+ from inferencekit.evidence import Store
637
+
638
+ art = next((a for a in rec.get("artifacts") or [] if isinstance(a, dict) and a.get("role") == role), None)
639
+ eid = rec["evidence_id"]
640
+ if not art or not isinstance(art.get("path"), str) or ".." in Path(art["path"]).parts:
641
+ return None
642
+ for base in (Store().run_dir(eid), run.root.parent / eid):
643
+ path = base / art["path"]
644
+ if not path.is_file():
645
+ continue
646
+ if file_sha256(path)[0] != art.get("sha256"):
647
+ run.warnings.append(f"{source}: its {role} file at {path.name} does not match the record's sha256: "
648
+ f"{what} not shown")
649
+ return None
650
+ out = []
651
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
652
+ try:
653
+ row = json.loads(line) if line.strip() else None
654
+ except ValueError:
655
+ continue
656
+ if isinstance(row, dict):
657
+ out.append(row)
658
+ return out
659
+ run.warnings.append(f"{source}: record {eid[:15]}: its {role} file is not in the evidence store (pass "
660
+ f"--runs-dir for that store): {what} not shown")
661
+ return None
662
+
663
+
664
+ def clip_rows(run: Run, rec: dict, source: str) -> dict:
665
+ """``"r<k>/<id>" -> row`` from a bench clip record's ``requests`` artifact (requests.jsonl)."""
666
+ out = {}
667
+ for row in artifact_lines(run, rec, source, "requests", "per-clip times") or []:
668
+ if isinstance(row.get("id"), str) and isinstance(row.get("round"), int):
669
+ out[f"r{row['round']}/{row['id']}"] = {k: row.get(k) for k in CLIP_ROW_KEYS if k in row}
670
+ return out
671
+
672
+
673
+ def clip_quality(run: Run, rec: dict, source: str, problem: str | None) -> list:
674
+ """A quality clip record covers every engine it was given: ``data.engines[label]`` holds each one's verdict,
675
+ its metrics are ``psnr`` / ``ssim`` / ``lpips`` with ``subject`` = the label, and the per-clip scores are the
676
+ ``clip_metrics`` artifact's rows (key ``engine:<label>|r<k>/<id>``, ``mean`` per metric). Returns the engines
677
+ it touched."""
678
+ data, eid = rec.get("data") or {}, rec["evidence_id"]
679
+ engines = data.get("engines") if isinstance(data.get("engines"), dict) else {}
680
+ touched = []
681
+ for label, block in engines.items():
682
+ info = run.engine(str(label))
683
+ if info is None or not isinstance(block, dict):
684
+ continue
685
+ touched.append(info)
686
+ verdict = "UNPROVEN" if problem else str(block.get("verdict") or "UNPROVEN")
687
+ for name, unit, names in CLIP_QUALITY:
688
+ value = _metric(rec, names, info.label)
689
+ if value is not None:
690
+ info.ev_quality.append({"name": name, "unit": unit, "evidence": eid, "value": value,
691
+ "verdict": verdict})
692
+ if not any(q["evidence"] == eid for q in info.ev_quality):
693
+ info.ev_quality.append({"name": "clip quality", "unit": None, "evidence": eid, "value": None,
694
+ "verdict": verdict})
695
+ if not touched:
696
+ return touched
697
+ for row in artifact_lines(run, rec, source, "clip_metrics", "per-clip quality") or []:
698
+ found = _QUALITY_KEY.match(str(row.get("key") or ""))
699
+ info = run.engine(found.group(1)) if found else None
700
+ if info is None or info not in touched or row.get("ok") is False:
701
+ continue
702
+ mean = row.get("mean") if isinstance(row.get("mean"), dict) else {}
703
+ got = {name: float(mean[name]) for name, _, _ in CLIP_QUALITY
704
+ if isinstance(mean.get(name), (int, float)) and not isinstance(mean.get(name), bool)}
705
+ if got:
706
+ info.clip_rows.setdefault(found.group(2), {}).update(got)
707
+ return touched
708
+
709
+
710
+ def merge_clip_rows(run: Run) -> None:
711
+ """Video: each cell gets its bench clip row (client-timed clip_s, self-reported stages, effective shape) and
712
+ quality clip's per-clip numbers. A clip the engine failed is FAILED with bench clip's error, not NOT RUN."""
713
+ for info in run.engines:
714
+ for item in run.items:
715
+ row = info.clip_rows.get(run.key(item))
716
+ cell = run.cells.get((info.label, run.key(item)))
717
+ if not row or cell is None:
718
+ continue
719
+ cell.clip_row = row
720
+ if isinstance(row.get("e2e_ms"), (int, float)):
721
+ cell.metrics.setdefault("clip_s", row["e2e_ms"] / 1000)
722
+ for name, _, _ in CLIP_QUALITY:
723
+ if isinstance(row.get(name), (int, float)):
724
+ cell.metrics.setdefault(name, row[name])
725
+ if row.get("ok") is False and cell.status != "failed":
726
+ cell.status, cell.reason = "failed", f"bench clip: {row.get('error') or 'the clip failed'}"
727
+
728
+
729
+ def run_evidence(root: Path, run: Run) -> list[tuple[str, dict]]:
730
+ out = []
731
+ d = root / "evidence"
732
+ if d.is_dir():
733
+ for p in sorted(d.glob("*.json")):
734
+ try:
735
+ out.append((p.relative_to(root).as_posix(), _read_json(p)))
736
+ run.sources.append(p.relative_to(root).as_posix())
737
+ except (OSError, ValueError) as e:
738
+ run.warnings.append(f"evidence/{p.name}: unreadable ({e})")
739
+ return out
740
+
741
+
742
+ def load_run(root: str | Path, reference: str | None = None, prompts: str | Path | None = None,
743
+ extra_evidence: list[dict] | None = None) -> Run:
744
+ """Read a run directory. ``reference`` names the outputs/<label> column the others are diffed against;
745
+ ``prompts`` (a prompt-set JSONL) fills the input column when inputs/ lacks them; ``extra_evidence``
746
+ adds records (e.g. gates) beyond the run's own evidence/ directory."""
747
+ root = Path(root)
748
+ run = Run(root=root, modality="llm", engines=[], reference=None, items=[], cells={})
749
+ items = load_inputs(root, run)
750
+ if not items and not (root / "inputs").is_dir() and not prompts:
751
+ run.warnings.append("no inputs/ directory (and no --prompts): rows come from the outputs, without prompts")
752
+
753
+ out_dir = root / "outputs"
754
+ labels = sorted((p.name for p in out_dir.iterdir() if p.is_dir()), key=natural_key) if out_dir.is_dir() else []
755
+ met_dir = root / "metrics"
756
+ if met_dir.is_dir():
757
+ for p in sorted(met_dir.iterdir(), key=lambda q: natural_key(q.name)):
758
+ if p.name.endswith(".load.json") or p.suffix not in (".json", ".jsonl"):
759
+ continue # load-mode levels belong to the engine of the same label, not a column of their own
760
+ ending = next((s for s in (".clip.json", ".image.json", ".audio.json") if p.name.endswith(s)), None)
761
+ label = p.name[:-len(ending)] if ending else p.name.rsplit(".", 1)[0]
762
+ if label not in labels:
763
+ labels.append(label)
764
+ # Manifest order first (baseline recipe, tuned, custom), then anything else found.
765
+ manifest = _manifest_engines(root)
766
+ roles = {}
767
+ order = []
768
+ for e in manifest:
769
+ label = str(e.get("label") or e.get("name") or "")
770
+ if label:
771
+ roles[label] = e.get("role")
772
+ if label in labels:
773
+ order.append(label)
774
+ order += [lb for lb in labels if lb not in order]
775
+
776
+ if reference is not None:
777
+ if reference not in order:
778
+ from inferencekit.registry import OpError
779
+
780
+ raise OpError(f"reference '{reference}' has no outputs/{reference}/ or metrics/{reference}.json in "
781
+ f"{root}; found: {order or 'nothing'}")
782
+ run.reference = reference
783
+ elif REFERENCE_DEFAULT in order:
784
+ run.reference = REFERENCE_DEFAULT
785
+ if run.reference:
786
+ order = [run.reference] + [lb for lb in order if lb != run.reference]
787
+
788
+ for label in order:
789
+ info = load_metrics(root, label, run)
790
+ from inferencekit.review.media import load_media_metrics
791
+
792
+ load_media_metrics(root, label, info, run, items)
793
+ info.role = roles.get(label) or info.role or ("reference" if label == run.reference else None)
794
+ run.engines.append(info)
795
+
796
+ # Rows: every input, plus anything an engine produced for an input that is not in inputs/.
797
+ for label in order:
798
+ base = out_dir / label
799
+ if not base.is_dir():
800
+ continue
801
+ deeper = 0
802
+ for path in base.rglob("*"):
803
+ if not path.is_file():
804
+ continue
805
+ rel = path.relative_to(base)
806
+ if len(rel.parts) != 2: # index.json at the root; load-level dirs (<level>/<bucket>/<id>) below
807
+ deeper += len(rel.parts) > 2
808
+ continue
809
+ bucket = rel.parts[0]
810
+ stem, _ = _split_name(path.name)
811
+ if f"{bucket}/{stem}" not in items:
812
+ items[f"{bucket}/{stem}"] = Item(id=stem, bucket=bucket)
813
+ if (root / "inputs").is_dir():
814
+ run.warnings.append(f"outputs/{label}/{rel.as_posix()}: no matching input under "
815
+ f"inputs/{bucket}/")
816
+ if deeper:
817
+ run.warnings.append(f"outputs/{label}/: {deeper} file(s) in load-level directories are not shown "
818
+ "(the page shows the single-user outputs, outputs/<label>/<bucket>/<id>)")
819
+ if prompts:
820
+ load_prompt_set(Path(prompts), items, run)
821
+ run.items = sorted(items.values(), key=lambda i: (bucket_key(i.bucket), natural_key(i.id)))
822
+ for item in run.items:
823
+ for info in run.engines:
824
+ run.cells[(info.label, run.key(item))] = load_cell(root, info.label, item, run)
825
+ attach_evidence(run, run_evidence(root, run) + [(f"evidence {str(r.get('evidence_id', '?'))[:15]}", r)
826
+ for r in extra_evidence or [] if isinstance(r, dict)])
827
+ from inferencekit.review.media import merge_rows
828
+
829
+ merge_rows(run)
830
+ run.modality = detect_modality(root, run.items, run.cells)
831
+ if run.modality == "llm" and not run.reference:
832
+ run.warnings.insert(0, "no reference outputs (pass --reference LABEL or add outputs/reference/): "
833
+ "divergence is not marked")
834
+ if run.modality == "video":
835
+ merge_clip_rows(run)
836
+ if not run.reference:
837
+ run.warnings.insert(0, "no reference clips (pass --reference LABEL or add outputs/reference/): there is "
838
+ "no column to compare the clips with by eye")
839
+ if run.modality == "tts":
840
+ for item in run.items:
841
+ if item.ground_truth is None and item.text:
842
+ item.ground_truth = item.text.strip()
843
+ run.sources = sorted(set(run.sources))
844
+ return run