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,384 @@
1
+ """Speech screening: client-timed PCM, structural validation, never an audio quality proof.
2
+
3
+ Fixture JSONL rows: {id, text, reference_audio?: relative file, reference_text?: str,
4
+ unseen?: bool}. The unseen flag is an attestation, not independently verified.
5
+ One process serves all rounds; final comparative claims need a separate fresh-
6
+ process interleaved scheduler. Warmup requests are excluded from all metrics.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import re
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from urllib.parse import urlsplit
19
+
20
+ from inferencekit import evidence as ev, stats
21
+ from inferencekit.canonical import canonical_json
22
+ from inferencekit.ops.bench_serve import _record_call
23
+ from inferencekit.registry import Arg, OpError, operation
24
+ from inferencekit.serve.audio_client import AudioHTTP, AudioRequest, ENCODING, playback_metrics
25
+ from inferencekit.serve.client import Endpoint, api_key_from_env
26
+ from inferencekit.serve.metrics import pct
27
+
28
+ KIND = "audio_benchmark"
29
+ _ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.+-]{0,63}$")
30
+ ARGS = [
31
+ Arg("base_url", "string", "Speech server root; stored only as a keyed endpoint tag.", required=True, sensitive=True),
32
+ Arg("model", "string", "Pinned model identifier sent to the server.", required=True),
33
+ Arg("model_revision", "string", "Checkpoint revision, sent and required in final effective settings."),
34
+ Arg("model_dtype", "string", "Checkpoint dtype, sent as dtype and checked in effective settings."),
35
+ Arg("model_quant", "string", "Complete quantization recipe, sent as quant and checked in effective settings."),
36
+ Arg("fixtures", "string", "JSONL rows: id, text, reference_audio?, reference_text?, unseen?.", required=True),
37
+ Arg("label", "string", "Configuration label for artifacts.", default="audio"),
38
+ Arg("rounds", "integer", "Timed passes; screening in one server process.", default=3),
39
+ Arg("first_round", "integer", "First round index; default zero or previous maximum plus one."),
40
+ Arg("warmup", "integer", "Untimed distinct-text requests before measurement.", default=1),
41
+ Arg("seed_base", "integer", "Shared deterministic round-seed base.", default=0),
42
+ Arg("sample_rate", "integer", "Required decoded PCM sample rate.", default=44100),
43
+ Arg("channels", "integer", "Required PCM channels.", default=1),
44
+ Arg("settings", "string", "JSON object of sampling/streaming settings; final must report them.", default="{}"),
45
+ Arg("api_key_env", "string", "Environment variable containing the key; never a key value."),
46
+ Arg("engine_version", "string", "Pinned engine version, sent and checked in effective settings."),
47
+ Arg("hardware_name", "string", "Measured hardware label; never inferred from a remote client."),
48
+ Arg("run_dir", "string", "Also copy request/output artifacts and metrics here."),
49
+ Arg("timeout_s", "number", "Whole-response deadline including DNS, connect, headers and streamed body.", default=300.0),
50
+ Arg("resamples", "integer", "Round-cluster bootstrap resamples.", default=stats.DEFAULT_RESAMPLES),
51
+ Arg("save_outputs", "string", "Keep decoded PCM or hashes only.", default="all", choices=("all", "hash_only")),
52
+ Arg("slo_first_audio_p95_ms", "number", "p95 simulated nonzero-signal onset limit.", default=2000.0),
53
+ Arg("slo_rtf", "number", "Maximum complete-generation wall seconds per audio second.", default=1.0),
54
+ Arg("extend", "string", "Append rounds to the same configuration's earlier evidence record."),
55
+ ]
56
+
57
+
58
+ @dataclass
59
+ class AudioFixture:
60
+ id: str
61
+ text: str
62
+ text_sha256: str
63
+ reference_audio: bytes | None
64
+ reference_sha256: str | None
65
+ reference_text: str | None
66
+ reference_text_sha256: str | None
67
+ declared_unseen: bool
68
+
69
+ def identity(self):
70
+ return {"id": self.id, "text_sha256": self.text_sha256,
71
+ "reference_sha256": self.reference_sha256,
72
+ "reference_text_sha256": self.reference_text_sha256, "declared_unseen": self.declared_unseen}
73
+
74
+
75
+ def _sha(data: bytes) -> str:
76
+ return hashlib.sha256(data).hexdigest()
77
+
78
+
79
+ def load_fixtures(path) -> tuple[list[AudioFixture], str]:
80
+ p = Path(path)
81
+ try:
82
+ raw = p.read_bytes()
83
+ docs = [json.loads(line) for line in raw.decode("utf-8").splitlines() if line.strip()]
84
+ except (OSError, ValueError):
85
+ raise OpError("cannot read speech fixture JSONL") from None
86
+ fixtures, ids = [], set()
87
+ for n, doc in enumerate(docs, 1):
88
+ if not isinstance(doc, dict) or not isinstance(doc.get("id"), str) or not _ID.fullmatch(doc["id"]):
89
+ raise OpError(f"speech fixture row {n}: invalid id")
90
+ if doc["id"] in ids:
91
+ raise OpError(f"speech fixture row {n}: duplicate id")
92
+ ids.add(doc["id"])
93
+ if not isinstance(doc.get("text"), str) or not doc["text"].strip():
94
+ raise OpError(f"speech fixture row {n}: nonempty text required")
95
+ if set(doc) - {"id", "text", "reference_audio", "reference_text", "unseen"}:
96
+ raise OpError(f"speech fixture row {n}: unknown field")
97
+ if "unseen" in doc and type(doc["unseen"]) is not bool:
98
+ raise OpError(f"speech fixture row {n}: unseen must be boolean")
99
+ reference, reference_text = None, doc.get("reference_text")
100
+ if "reference_audio" in doc:
101
+ if not isinstance(doc["reference_audio"], str):
102
+ raise OpError(f"speech fixture row {n}: reference_audio must be a file path")
103
+ if not isinstance(reference_text, str) or not reference_text.strip():
104
+ raise OpError(f"speech fixture row {n}: reference audio needs its transcript")
105
+ try:
106
+ reference = (p.parent / doc["reference_audio"]).read_bytes()
107
+ except OSError:
108
+ raise OpError(f"speech fixture row {n}: reference audio unreadable") from None
109
+ if not reference:
110
+ raise OpError(f"speech fixture row {n}: reference audio empty")
111
+ elif reference_text is not None:
112
+ raise OpError(f"speech fixture row {n}: transcript requires reference audio")
113
+ fixtures.append(AudioFixture(doc["id"], doc["text"], _sha(doc["text"].encode()), reference,
114
+ _sha(reference) if reference is not None else None, reference_text,
115
+ _sha(reference_text.encode()) if reference_text is not None else None,
116
+ doc.get("unseen", False)))
117
+ if not fixtures:
118
+ raise OpError("speech fixture set is empty")
119
+ # Content identity includes the reference bytes, independent of local filenames.
120
+ return fixtures, _sha(canonical_json([f.identity() for f in fixtures]).encode())
121
+
122
+
123
+ def round_seed(seed_base: int, round_index: int) -> int:
124
+ return int.from_bytes(hashlib.sha256(f"inferencekit-audio:{seed_base}:{round_index}".encode()).digest()[:4],
125
+ "big") & 0x7fffffff
126
+
127
+
128
+ def audio_row(result, fixture, round_index, seed):
129
+ duration = result.audio_duration_s
130
+ complete = result.complete_audio_ms
131
+ return {**fixture.identity(), "round": round_index, "seed": seed, "ok": result.ok,
132
+ "error": result.error, "status": result.status, "t_send_ns": result.t_send_ns,
133
+ "t_done_ns": result.t_done_ns, "t_end_ns": result.t_end_ns,
134
+ "first_pcm_ms": result.first_audio_ms, "first_signal_ms": result.first_signal_ms,
135
+ "first_audio_ms": result.first_audible_ms, "complete_audio_ms": complete,
136
+ "audio_duration_s": duration, "rtf": complete / 1000 / duration if duration and complete is not None else None,
137
+ "leading_silence_ms": result.leading_silent_frames / result.sample_rate * 1000
138
+ if result.ok else None, "sample_rate": result.sample_rate, "channels": result.channels,
139
+ "sample_count": result.sample_count if result.ok else None,
140
+ "received_pcm_frames": sum(count for _, count in result.chunks),
141
+ "pcm_bytes": len(result.pcm) if result.ok else None,
142
+ "pcm_sha256": _sha(result.pcm) if result.ok else None, "effective": result.effective,
143
+ "chunks": [{"arrival_ms": (t - result.t_send_ns) / 1e6, "sample_count": count}
144
+ for t, count in result.chunks],
145
+ **(playback_metrics(result.chunks, result.sample_rate) if result.ok else
146
+ {"underruns": None, "underrun_ms": None, "max_stream_gap_ms": None})}
147
+
148
+
149
+ def audio_metrics(rows: list[dict], resamples: int) -> tuple[list[dict], dict]:
150
+ metrics, table = [], {}
151
+ for key, unit in (("first_audio_ms", "ms"), ("first_pcm_ms", "ms"), ("first_signal_ms", "ms"),
152
+ ("complete_audio_ms", "ms"), ("rtf", "ratio"), ("max_stream_gap_ms", "ms")):
153
+ valid = [row for row in rows if row["ok"] and row.get(key) is not None]
154
+ values, units = [row[key] for row in valid], [row["round"] for row in valid]
155
+ cis = stats.bootstrap_ci(values, units=units, ps=(50, 95), resamples=resamples)
156
+ table[key] = {"n": len(values), "median": pct(values, 50), "p95": pct(values, 95),
157
+ "max": max(values, default=None), "ci95_median": list(cis[50]), "ci95_p95": list(cis[95])}
158
+ for p, stat in ((50, "median"), (95, "p95")):
159
+ metrics.append(ev.metric(key, pct(values, p), unit, stat, n=len(values), ci95=cis[p]))
160
+ return metrics, table
161
+
162
+
163
+ def _write(roots, relative: str, data: bytes):
164
+ for root in roots:
165
+ target = root / relative
166
+ target.parent.mkdir(parents=True, exist_ok=True)
167
+ target.write_bytes(data)
168
+
169
+
170
+ def _previous(store, reference, config_sha, call):
171
+ if not reference:
172
+ return None, [], []
173
+ record = store.load(reference)
174
+ if record.get("kind") != KIND or record.get("data", {}).get("source") != "bench audio":
175
+ raise OpError("--extend requires a bench audio record")
176
+ if ev.integrity_problem(record):
177
+ raise OpError("--extend evidence integrity check failed")
178
+ if record["data"].get("configuration_sha256") != config_sha:
179
+ raise OpError("--extend requires the same workload, model, version, hardware and seed policy")
180
+ old_args = record["data"].get("request", {}).get("args", {})
181
+ if old_args.get("base_url") != call.request()["args"].get("base_url"):
182
+ raise OpError("--extend requires the same endpoint")
183
+ artifact = next((a for a in record["artifacts"] if a.get("role") == "requests"), None)
184
+ if artifact is None:
185
+ raise OpError("--extend per-request artifact missing")
186
+ previous_root = store.run_dir(record["evidence_id"])
187
+ path = previous_root / artifact["path"]
188
+ if not path.resolve().is_relative_to(previous_root.resolve()):
189
+ raise OpError("--extend per-request artifact escapes its record")
190
+ try:
191
+ raw = path.read_bytes()
192
+ if _sha(raw) != artifact["sha256"]:
193
+ raise ValueError
194
+ rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()]
195
+ except (OSError, ValueError):
196
+ raise OpError("--extend per-request artifact missing or changed") from None
197
+ return record, rows, record["data"]["rounds"]
198
+
199
+
200
+ @operation("bench_audio", ("bench", "audio"), "Client-timed streamed PCM speech screening; structural gates and "
201
+ "first-audio p95, complete audio, RTF and buffer underruns. Audio quality remains UNPROVEN.", ARGS)
202
+ def bench_audio(call, base_url, model, model_revision, model_dtype, model_quant, fixtures, label, rounds, first_round, warmup, seed_base, sample_rate,
203
+ channels, settings, api_key_env, engine_version, hardware_name, run_dir, timeout_s,
204
+ resamples, save_outputs, slo_first_audio_p95_ms, slo_rtf, extend):
205
+ if rounds < 1 or (first_round is not None and first_round < 0) or warmup < 0 or resamples < 1:
206
+ raise OpError("rounds/resamples must be positive; first_round/warmup nonnegative")
207
+ if not _ID.fullmatch(label):
208
+ raise OpError("invalid audio label")
209
+ if not model.strip():
210
+ raise OpError("audio model identifier must be nonempty")
211
+ if not 8000 <= sample_rate <= 192000 or not 1 <= channels <= 8:
212
+ raise OpError("PCM rate must be 8000..192000 Hz and channels 1..8")
213
+ if any(not math.isfinite(v) or v <= 0 for v in (timeout_s, slo_first_audio_p95_ms, slo_rtf)):
214
+ raise OpError("timeout and SLO limits must be finite and positive")
215
+ try:
216
+ setting_doc = json.loads(settings)
217
+ if not isinstance(setting_doc, dict):
218
+ raise ValueError
219
+ json.dumps(setting_doc, allow_nan=False)
220
+ if set(setting_doc) & {"model", "seed", "input", "sample_rate", "channels", "encoding", "stream"}:
221
+ raise ValueError
222
+ if ev.redact(setting_doc) != setting_doc:
223
+ raise ValueError
224
+ except ValueError:
225
+ raise OpError("settings must be a finite JSON object without reserved request fields") from None
226
+ pins = {key: value for key, value in (("model_revision", model_revision), ("dtype", model_dtype),
227
+ ("quant", model_quant), ("engine_version", engine_version)) if value is not None}
228
+ for key, value in pins.items():
229
+ if key in setting_doc and setting_doc[key] != value:
230
+ raise OpError(f"settings conflict with declared {key}")
231
+ setting_doc[key] = value
232
+ fixture_rows, fixture_sha = load_fixtures(fixtures)
233
+ try:
234
+ parsed_url = urlsplit(base_url if "://" in base_url else "http://" + base_url)
235
+ if parsed_url.username is not None or parsed_url.password is not None or parsed_url.query or parsed_url.fragment:
236
+ raise ValueError
237
+ endpoint = Endpoint.parse(base_url, api_key_from_env(api_key_env))
238
+ except (ValueError, OpError):
239
+ raise OpError("invalid speech endpoint or API-key environment configuration") from None
240
+ record_call, persistent = _record_call(call, endpoint)
241
+ backend, store = AudioHTTP(endpoint, timeout_s=timeout_s), ev.Store()
242
+ run = Path(run_dir) if run_dir else None
243
+ workload_config = {"fixtures": fixture_sha, "model": model, "revision": model_revision,
244
+ "dtype": model_dtype, "quant": model_quant, "sample_rate": sample_rate,
245
+ "channels": channels, "encoding": ENCODING,
246
+ "settings": {key: value for key, value in setting_doc.items() if key != "engine_version"},
247
+ "seed_policy": {"name": "per_round", "base": seed_base}}
248
+ request_sha = _sha(canonical_json(workload_config).encode())
249
+ config_sha = _sha(canonical_json({**workload_config, "label": label, "hardware": hardware_name,
250
+ "engine_version": engine_version, "effective_settings": setting_doc,
251
+ "save_outputs": save_outputs,
252
+ "slo_first_audio_p95_ms": slo_first_audio_p95_ms, "slo_rtf": slo_rtf}).encode())
253
+ previous, previous_rows, previous_rounds = _previous(store, extend, config_sha, record_call)
254
+ done = max((row["index"] for row in previous_rounds), default=-1)
255
+ first_round = done + 1 if first_round is None else first_round
256
+ if first_round <= done:
257
+ raise OpError("--first-round conflicts with rounds already present in --extend")
258
+ if previous is not None and first_round != done + 1:
259
+ raise OpError("--extend --first-round must be the previous maximum plus one; skipped rounds are refused")
260
+
261
+ def request(fixture, seed, text=None):
262
+ return AudioRequest(fixture.id, fixture.text if text is None else text, model, seed, sample_rate,
263
+ channels, setting_doc, fixture.reference_audio, fixture.reference_text)
264
+
265
+ try:
266
+ with store.staging_dir() as staging:
267
+ roots = [staging] + ([run] if run else [])
268
+ warm_rows, rows, round_rows, outputs = [], list(previous_rows), list(previous_rounds), []
269
+ if previous:
270
+ previous_root = store.run_dir(previous["evidence_id"])
271
+ for artifact in previous["artifacts"]:
272
+ if artifact.get("role") not in ("audio_pcm", "outputs_index"):
273
+ continue
274
+ src = previous_root / artifact["path"]
275
+ if not src.resolve().is_relative_to(previous_root.resolve()):
276
+ raise OpError("--extend artifact path escapes its record")
277
+ try:
278
+ blob = src.read_bytes()
279
+ except OSError:
280
+ raise OpError("--extend output artifact missing") from None
281
+ if _sha(blob) != artifact["sha256"]:
282
+ raise OpError("--extend output artifact changed")
283
+ if artifact["role"] == "outputs_index":
284
+ outputs = json.loads(blob)
285
+ else:
286
+ _write(roots, artifact["path"], blob)
287
+ for i in range(warmup):
288
+ fixture = fixture_rows[i % len(fixture_rows)]
289
+ text = f"Inferencekit untimed speech warmup {i}. " + fixture.text
290
+ while text in {f.text for f in fixture_rows}:
291
+ text = "Warmup. " + text
292
+ result = backend.generate(request(fixture, round_seed(seed_base, -i - 1), text))
293
+ warm_rows.append({"ok": result.ok, "error": result.error})
294
+ for round_index in range(first_round, first_round + rounds):
295
+ start = time.time()
296
+ current_rows = []
297
+ seed = round_seed(seed_base, round_index)
298
+ for fixture in fixture_rows:
299
+ result = backend.generate(request(fixture, seed))
300
+ row = audio_row(result, fixture, round_index, seed)
301
+ rows.append(row)
302
+ current_rows.append(row)
303
+ if result.ok and save_outputs == "all":
304
+ rel = f"outputs/{label}/r{round_index}/{fixture.id}.pcm"
305
+ _write(roots, rel, result.pcm)
306
+ outputs.append({"path": rel, "sha256": row["pcm_sha256"], "bytes": row["pcm_bytes"],
307
+ "sample_rate": sample_rate, "channels": channels, "encoding": ENCODING})
308
+ valid = [row for row in current_rows if row["ok"]]
309
+ round_rows.append({"index": round_index, "t_start": start, "t_end": time.time(), "seed": seed,
310
+ "n_failed": len(current_rows) - len(valid),
311
+ "audio_ms": sum(r["complete_audio_ms"] for r in valid) / len(valid) if valid else None,
312
+ "first_audio_p95_ms": pct([r["first_audio_ms"] for r in valid], 95),
313
+ "rtf": sum(r["rtf"] for r in valid) / len(valid) if valid else None})
314
+ metrics, table = audio_metrics(rows, resamples)
315
+ failed = sum(not row["ok"] for row in rows)
316
+ structure = "FAIL" if failed else "PASS"
317
+ timing = "FAIL" if failed or table["first_audio_ms"]["p95"] > slo_first_audio_p95_ms or \
318
+ table["rtf"]["max"] > slo_rtf or any(row["underruns"] for row in rows if row["ok"]) else "PASS"
319
+ verdict = stats.combine_claims([structure, timing, "UNPROVEN"])
320
+ data = {"source": "bench audio", "timer": "client perf_counter_ns", "label": label,
321
+ "endpoint_kind": "audio_http", "base_url_kind": "local" if endpoint.is_local() else "remote",
322
+ "fixture_set": {"sha256": fixture_sha, "fixtures": [f.identity() for f in fixture_rows]},
323
+ "configuration_sha256": config_sha, "extends": previous["evidence_id"] if previous else None,
324
+ "rounds": round_rows, "warmup": {"excluded": True, "requests": warm_rows},
325
+ "per_request": table, "failed_requests": failed, "total_requests": len(rows),
326
+ "structural_verdict": structure, "timing_verdict": timing, "quality_verdict": "UNPROVEN",
327
+ "verdict": verdict, "claim_validity": {"valid": not failed, "checked": [
328
+ "PCM metadata", "aligned nonempty chunks", "terminal final event", "sample count",
329
+ "nonzero signal", "effective settings (self-reported)"],
330
+ "violations": [r["error"] for r in rows if not r["ok"]]},
331
+ "slo": {"first_audio_p95_ms": slo_first_audio_p95_ms, "rtf_max": slo_rtf, "max_underruns": 0},
332
+ "memory": {"verdict": "NOT RUN", "source": "external process/Metal instrumentation required"},
333
+ "metric_claim_classes": {"first_pcm_ms": "observed (client timer)",
334
+ "first_signal_ms": "observed (client timer, nonzero-sample chunk arrival)",
335
+ "complete_audio_ms": "observed (client timer)", "max_stream_gap_ms": "observed (client timer)",
336
+ "first_audio_ms": "derived (PCM playback simulation, not an audio device measurement)",
337
+ "rtf": "derived (complete time / decoded PCM duration)",
338
+ "underruns": "derived (PCM playback simulation)", "audio_duration_s": "derived (frames / declared rate)"},
339
+ "definitions": {"first_pcm_ms": "First decoded nonempty PCM chunk, including silence.",
340
+ "sample_count": "Validated complete PCM frame count; null for failed responses.",
341
+ "received_pcm_frames": "Decoded frames received, including partial failed responses; diagnostic only.",
342
+ "first_signal_ms": "Arrival of the first chunk containing a nonzero int16 sample.",
343
+ "first_audio_ms": "Simulated nonzero-signal playback onset, including initial zero PCM and stalls.",
344
+ "complete_audio_ms": "Full HTTP body consumption minus request start, on the client clock.",
345
+ "rtf": "complete_audio_ms / (1000 * decoded PCM duration seconds); lower is faster.",
346
+ "underruns": "PCM playback simulation starting at first chunk, zero extra prebuffer."}}
347
+ limitations = ["Structural PCM checks do not establish intelligibility, completion, speaker similarity or perceptual quality; external reference proof required.",
348
+ "unseen is a fixture attestation; text/reference hashes bind inputs but do not prove the server has never seen them.",
349
+ "Rounds share one server process: screening only; comparative claims require fresh-process interleaved rounds.",
350
+ "NDJSON events must be flushed by the server. Arrival includes its buffering, HTTP line framing and network transfer.",
351
+ "Signal means a nonzero PCM sample, not validated speech; PCM silence is measured at exact zero.",
352
+ "Effective settings are self-reported, not independently proven.",
353
+ "Memory, swap and sustained repeated-run memory growth are NOT RUN; collect external telemetry."]
354
+ if not persistent:
355
+ limitations.append("Endpoint tag is process-local because no persistent HMAC key could be written.")
356
+ if warmup == 0 or any(not row["ok"] for row in warm_rows):
357
+ limitations.append("Warmup absent or failed: timed requests may include initialization.")
358
+ if len(round_rows) < 3:
359
+ limitations.append("Fewer than three rounds: insufficient independent-round evidence for a final claim.")
360
+ if save_outputs == "hash_only":
361
+ limitations.append("PCM outputs not retained: hashes alone cannot support later audio quality evaluation.")
362
+ _write(roots, "requests.jsonl", ("".join(json.dumps(ev.redact(row)) + "\n" for row in rows)).encode())
363
+ _write(roots, f"outputs/{label}/index.json", (json.dumps(outputs, indent=2) + "\n").encode())
364
+ artifacts = [ev.artifact(staging / "requests.jsonl", staging, "requests"),
365
+ ev.artifact(staging / f"outputs/{label}/index.json", staging, "outputs_index")]
366
+ for output in outputs:
367
+ artifacts.append(ev.artifact(staging / output["path"], staging, "audio_pcm"))
368
+ identity = ev.identity(gpu={"name": hardware_name}, engine={"name": label, "version": engine_version},
369
+ model={"id": model, "revision": model_revision, "dtype": model_dtype, "quant": model_quant},
370
+ workload={"mode": "single", "batch": 1, "endpoint_kind": "audio_http",
371
+ "request_set": {"sha256": request_sha, "n": len(fixture_rows)},
372
+ "seed_policy": {"name": "per_round", "base": seed_base}})
373
+ data["audio_config"] = {"sample_rate": sample_rate, "channels": channels,
374
+ "settings": setting_doc, "seed_base": seed_base, "save_outputs": save_outputs}
375
+ record = ev.build(record_call, kind=KIND, claim_class="observed",
376
+ summary=f"{label}: {len(rows)} speech requests, {failed} structural failures; quality UNPROVEN; {verdict}",
377
+ identity_=identity, metrics=metrics, artifacts=artifacts,
378
+ missing=["audio quality reference proof", "external memory telemetry"],
379
+ limitations=limitations, data=data)
380
+ if run:
381
+ _write([run], f"metrics/{label}.audio.json", (json.dumps(ev.redact(data), indent=2) + "\n").encode())
382
+ return store.save(record, staging)
383
+ finally:
384
+ backend.close()