ncm-benchmarks 2.0.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.
ncm_bench/cli.py ADDED
@@ -0,0 +1,650 @@
1
+ """Unified command-line interface for NCM benchmarks, sovereign inference, training, and inspection."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import pickle
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+
11
+ ROOT_DIR = Path(__file__).resolve().parent.parent
12
+ if str(ROOT_DIR) not in sys.path:
13
+ sys.path.insert(0, str(ROOT_DIR))
14
+
15
+
16
+ def get_model_bundle(
17
+ task: str = "xsum",
18
+ quantize: str = "none",
19
+ device: str | None = None,
20
+ verify_checksums: bool = True,
21
+ strict_checksums: bool = False,
22
+ dtype: str = "fp32",
23
+ compile_model: bool = False,
24
+ ):
25
+ from ncm_bench.inference import load_bundle
26
+
27
+ return load_bundle(
28
+ task=task,
29
+ device=device,
30
+ quantize=quantize,
31
+ dtype=dtype,
32
+ verify_checksums=verify_checksums,
33
+ strict_checksums=strict_checksums,
34
+ compile_model=compile_model,
35
+ root_dir=ROOT_DIR,
36
+ )
37
+
38
+
39
+ def _read_text_arg(args) -> str:
40
+ if getattr(args, "input_file", None):
41
+ from ncm_bench.longdoc import extract_text_from_file
42
+
43
+ return extract_text_from_file(Path(args.input_file))
44
+ text = getattr(args, "text", None)
45
+ if not text and not sys.stdin.isatty():
46
+ text = sys.stdin.read().strip()
47
+ return (text or "").strip()
48
+
49
+
50
+ def _emit_output(payload: str, args) -> None:
51
+ out_file = getattr(args, "output_file", None)
52
+ if out_file:
53
+ out_path = Path(out_file)
54
+ out_path.parent.mkdir(parents=True, exist_ok=True)
55
+ out_path.write_text(payload + "\n", encoding="utf-8")
56
+ print(f"[ncm-bench] Wrote output to {out_path}", file=sys.stderr)
57
+ else:
58
+ print(payload)
59
+
60
+
61
+ def cmd_serve(args):
62
+ from service.summarize_server import main as serve_main
63
+
64
+ argv = [
65
+ "summarize_server.py",
66
+ "--port",
67
+ str(args.port),
68
+ "--host",
69
+ args.host,
70
+ "--quantize",
71
+ args.quantize,
72
+ "--dtype",
73
+ args.dtype,
74
+ "--max-queue-depth",
75
+ str(args.max_queue_depth),
76
+ "--request-timeout-s",
77
+ str(args.request_timeout_s),
78
+ "--max-body-bytes",
79
+ str(args.max_body_bytes),
80
+ "--rate-limit-rpm",
81
+ str(args.rate_limit_rpm),
82
+ ]
83
+ if args.device:
84
+ argv.extend(["--device", args.device])
85
+ if args.uds:
86
+ argv.extend(["--uds", args.uds])
87
+ if getattr(args, "with_sudoku", False):
88
+ argv.append("--with-sudoku")
89
+ if args.skip_checksum:
90
+ argv.append("--skip-checksum")
91
+ if args.strict_checksums:
92
+ argv.append("--strict-checksums")
93
+ if args.compile:
94
+ argv.append("--compile")
95
+ sys.argv = argv
96
+ serve_main()
97
+
98
+
99
+ def cmd_summarize(args):
100
+ text = _read_text_arg(args)
101
+ if not text:
102
+ print("Error: No text provided via --text, --input-file, or standard input.", file=sys.stderr)
103
+ sys.exit(1)
104
+
105
+ from ncm_bench.longdoc import (
106
+ build_audit_receipt,
107
+ redact_pii,
108
+ restore_pii,
109
+ summarize_long_document,
110
+ )
111
+
112
+ pii_map: dict[str, str] = {}
113
+ proc_text = text
114
+ if args.redact_pii:
115
+ proc_text, pii_map = redact_pii(text)
116
+
117
+ print(f"[ncm-bench] Loading model (quantize={args.quantize}, dtype={args.dtype})...", file=sys.stderr)
118
+ bundle = get_model_bundle(
119
+ task="xsum",
120
+ quantize=args.quantize,
121
+ device=args.device,
122
+ verify_checksums=not args.skip_checksum,
123
+ strict_checksums=args.strict_checksums,
124
+ dtype=args.dtype,
125
+ compile_model=args.compile,
126
+ )
127
+
128
+ t0 = time.perf_counter()
129
+ if args.long_doc or len(proc_text) > 1800:
130
+ res = summarize_long_document(bundle, proc_text, use_mbr=False)
131
+ summary = res["summary"]
132
+ num_chunks = res["num_chunks"]
133
+ else:
134
+ from ncm_bench.inference import summarize_texts
135
+
136
+ summary = summarize_texts(bundle, [proc_text])[0]
137
+ num_chunks = 1
138
+ latency_ms = int((time.perf_counter() - t0) * 1000)
139
+
140
+ if args.restore_pii and pii_map:
141
+ summary = restore_pii(summary, pii_map)
142
+
143
+ if args.json or args.audit_receipt:
144
+ payload: dict[str, object] = {
145
+ "summary": summary,
146
+ "words": len(summary.split()),
147
+ "latency_ms": latency_ms,
148
+ "num_chunks": num_chunks,
149
+ "pii_redactions": len(pii_map),
150
+ }
151
+ if args.audit_receipt:
152
+ payload["audit_receipt"] = build_audit_receipt(
153
+ text,
154
+ summary,
155
+ bundle,
156
+ pii_redactions=len(pii_map),
157
+ mode="long-doc" if num_chunks > 1 else "standard",
158
+ )
159
+ _emit_output(json.dumps(payload, indent=2), args)
160
+ else:
161
+ _emit_output(summary, args)
162
+
163
+
164
+ def cmd_mbr(args):
165
+ text = _read_text_arg(args)
166
+ if not text:
167
+ print("Error: No text provided via --text, --input-file, or standard input.", file=sys.stderr)
168
+ sys.exit(1)
169
+
170
+ print(f"[ncm-bench] Running MBR decoding with N={args.candidates} candidates...", file=sys.stderr)
171
+ bundle = get_model_bundle(
172
+ task="xsum",
173
+ quantize=args.quantize,
174
+ device=args.device,
175
+ verify_checksums=not args.skip_checksum,
176
+ strict_checksums=args.strict_checksums,
177
+ dtype=args.dtype,
178
+ compile_model=args.compile,
179
+ )
180
+ from ncm_bench.mbr import generate_mbr_summary
181
+
182
+ res = generate_mbr_summary(
183
+ bundle,
184
+ text,
185
+ num_candidates=args.candidates,
186
+ temperature=args.temperature,
187
+ utility=args.utility,
188
+ )
189
+ if args.json:
190
+ _emit_output(json.dumps(res, indent=2), args)
191
+ else:
192
+ lines = [
193
+ f"Selected Summary (Cand #{res['selected_index']}):",
194
+ res["summary"],
195
+ "",
196
+ f"Candidate Pool Scores (Utility={args.utility}):",
197
+ ]
198
+ for i, (c, score) in enumerate(zip(res["candidates"], res["scores"], strict=True)):
199
+ mark = "-> [BEST]" if i == res["selected_index"] else " "
200
+ lines.append(f" {mark} #{i} (score: {score:.3f}): {c[:80]}...")
201
+ _emit_output("\n".join(lines), args)
202
+
203
+
204
+ def cmd_inspect(args):
205
+ text = _read_text_arg(args)
206
+ if not text:
207
+ text = (
208
+ "The UK government has announced a major investment into renewable energy infrastructure, "
209
+ "aiming to double wind and solar capacity by 2030. Ministers said the move will lower bills "
210
+ "and cut carbon emissions across England and Wales."
211
+ )
212
+
213
+ print("[ncm-bench] Inspecting concept hierarchy across all 9 levels...", file=sys.stderr)
214
+ bundle = get_model_bundle(
215
+ task="xsum",
216
+ quantize=args.quantize,
217
+ device=args.device,
218
+ verify_checksums=not args.skip_checksum,
219
+ strict_checksums=args.strict_checksums,
220
+ dtype=args.dtype,
221
+ compile_model=args.compile,
222
+ )
223
+ from ncm_bench.progressive import progressive_decode, render_progressive_html
224
+
225
+ res = progressive_decode(bundle, text)
226
+
227
+ if args.html:
228
+ html_doc = render_progressive_html(res, prompt_text=text)
229
+ out_path = Path(args.html)
230
+ out_path.parent.mkdir(parents=True, exist_ok=True)
231
+ out_path.write_text(html_doc, encoding="utf-8")
232
+ print(f"[ncm-bench] Wrote interactive visualizer to {out_path.resolve()}", file=sys.stderr)
233
+ if args.json:
234
+ _emit_output(json.dumps(res, indent=2), args)
235
+ elif not args.html:
236
+ print("=" * 70)
237
+ print("HIERARCHICAL COARSE-TO-FINE PROGRESSION (Levels 0 -> 8)")
238
+ print("=" * 70)
239
+ for lvl in res["levels"]:
240
+ print(
241
+ f"Level {lvl['level']:2d} [{lvl['codes_at_level']:2d} codes, "
242
+ f"{lvl['total_codes_so_far']:2d} accum]: {lvl['text'] or '(initial gist)'}"
243
+ )
244
+ print("=" * 70)
245
+ print(f"Final Summary: {res['final_summary']}")
246
+
247
+
248
+ def cmd_sudoku(args):
249
+ from ncm_bench.inference import solve_sudoku, verify_file_integrity
250
+ from ncm_bench.metrics import score_sudoku
251
+
252
+ task_name = f"sudoku/{args.difficulty}"
253
+ bundle = get_model_bundle(
254
+ task=task_name,
255
+ quantize=args.quantize,
256
+ device=args.device,
257
+ verify_checksums=not args.skip_checksum,
258
+ strict_checksums=args.strict_checksums,
259
+ dtype=args.dtype,
260
+ compile_model=args.compile,
261
+ )
262
+ repair = not args.no_repair
263
+ if args.puzzle:
264
+ t0 = time.perf_counter()
265
+ res = solve_sudoku(
266
+ bundle,
267
+ [args.puzzle],
268
+ best_of_k=args.best_of_k,
269
+ temperature=args.temperature,
270
+ repair=repair,
271
+ )[0]
272
+ res["latency_ms"] = int((time.perf_counter() - t0) * 1000)
273
+ if args.json:
274
+ _emit_output(json.dumps(res, indent=2), args)
275
+ else:
276
+ print(f"Valid Sudoku: {res['valid']} ({res['latency_ms']} ms)")
277
+ for row in res["board"].split("|"):
278
+ if row:
279
+ print(" " + " ".join(row))
280
+ return
281
+
282
+ puzzles_path = ROOT_DIR / f"data/sudoku/{args.difficulty}/puzzles.pickle"
283
+ solutions_path = ROOT_DIR / f"data/sudoku/{args.difficulty}/solutions.pickle"
284
+ if not args.skip_checksum:
285
+ verify_file_integrity(puzzles_path, ROOT_DIR, strict=args.strict_checksums)
286
+ verify_file_integrity(solutions_path, ROOT_DIR, strict=args.strict_checksums)
287
+ with open(puzzles_path, "rb") as f:
288
+ puzzles = pickle.load(f)[: args.n] # nosec B301 - verified by SHA256SUMS
289
+ with open(solutions_path, "rb") as f:
290
+ solutions = pickle.load(f)[: args.n] # nosec B301 - verified by SHA256SUMS
291
+
292
+ t0 = time.perf_counter()
293
+ preds_raw = []
294
+ for start in range(0, len(puzzles), args.batch_size):
295
+ batch = puzzles[start : start + args.batch_size]
296
+ preds_raw.extend(
297
+ r["board"]
298
+ for r in solve_sudoku(
299
+ bundle,
300
+ batch,
301
+ best_of_k=args.best_of_k,
302
+ temperature=args.temperature,
303
+ repair=repair,
304
+ )
305
+ )
306
+ elapsed = time.perf_counter() - t0
307
+
308
+ refs_str = [bundle["tk"].pre_tokenizer.decode(s, skip_special_tokens=True) for s in solutions]
309
+ stats = score_sudoku(preds_raw, refs_str)
310
+ stats["wall_s"] = round(elapsed, 3)
311
+ stats["boards_per_sec"] = round(len(puzzles) / max(elapsed, 1e-6), 2)
312
+ stats["best_of_k"] = args.best_of_k
313
+ stats["repair"] = repair
314
+ stats["difficulty"] = args.difficulty
315
+ if args.json:
316
+ _emit_output(json.dumps(stats, indent=2), args)
317
+ else:
318
+ ci = stats["ci_95"]
319
+ print(
320
+ f"Sudoku {args.difficulty} (n={stats['n_boards']}, K={args.best_of_k}, repair={repair}): "
321
+ f"solved={stats['solved_pct']}% ({stats['solved_count']}/{stats['n_boards']}, "
322
+ f"95% CI +/-{ci['margin_pp']}pp) | cell={stats['cell_pct']}% | "
323
+ f"valid={stats['valid_pct']}% | {stats['wall_s']}s ({stats['boards_per_sec']} boards/s)"
324
+ )
325
+
326
+
327
+ def cmd_translate(args):
328
+ text = _read_text_arg(args)
329
+ if not text:
330
+ print("Error: No German source text provided via --text, --input-file, or stdin.", file=sys.stderr)
331
+ sys.exit(1)
332
+ bundle = get_model_bundle(
333
+ task="wmt14",
334
+ quantize=args.quantize,
335
+ device=args.device,
336
+ verify_checksums=not args.skip_checksum,
337
+ strict_checksums=args.strict_checksums,
338
+ dtype=args.dtype,
339
+ compile_model=args.compile,
340
+ )
341
+ from ncm_bench.inference import summarize_texts
342
+
343
+ t0 = time.perf_counter()
344
+ translation = summarize_texts(bundle, [text])[0]
345
+ latency_ms = int((time.perf_counter() - t0) * 1000)
346
+ if args.json:
347
+ _emit_output(json.dumps({"translation": translation, "latency_ms": latency_ms}, indent=2), args)
348
+ else:
349
+ _emit_output(translation, args)
350
+
351
+
352
+ def cmd_benchmark(args):
353
+ from ncm_bench.inference import summarize_texts, verify_file_integrity
354
+ from ncm_bench.metrics import two_size_timing
355
+
356
+ if args.task == "sudoku":
357
+ args.puzzle = None
358
+ args.difficulty = getattr(args, "difficulty", "hard")
359
+ args.best_of_k = getattr(args, "best_of_k", 1)
360
+ args.temperature = getattr(args, "temperature", 0.4)
361
+ args.no_repair = False
362
+ cmd_sudoku(args)
363
+ return
364
+
365
+ passages_path = ROOT_DIR / "data/xsum/passages.pickle"
366
+ if not args.skip_checksum:
367
+ verify_file_integrity(passages_path, ROOT_DIR, strict=args.strict_checksums)
368
+ with open(passages_path, "rb") as f:
369
+ passages = pickle.load(f) # nosec B301 - verified by SHA256SUMS
370
+
371
+ def _timed_run(n_items: int) -> float:
372
+ t0 = time.perf_counter()
373
+ b = get_model_bundle(
374
+ task="xsum",
375
+ quantize=args.quantize,
376
+ device=args.device,
377
+ verify_checksums=not args.skip_checksum,
378
+ strict_checksums=args.strict_checksums,
379
+ dtype=args.dtype,
380
+ compile_model=args.compile,
381
+ )
382
+ for i in range(0, n_items, args.batch_size):
383
+ summarize_texts(b, passages[i : min(i + args.batch_size, n_items)])
384
+ return time.perf_counter() - t0
385
+
386
+ t_small = _timed_run(args.small_n)
387
+ t_large = _timed_run(args.large_n)
388
+ timing = two_size_timing(t_small, args.small_n, t_large, args.large_n)
389
+ timing.update(
390
+ {
391
+ "small_n": args.small_n,
392
+ "small_s": round(t_small, 3),
393
+ "large_n": args.large_n,
394
+ "large_s": round(t_large, 3),
395
+ "batch_size": args.batch_size,
396
+ "quantize": args.quantize,
397
+ "dtype": args.dtype,
398
+ }
399
+ )
400
+
401
+ if args.baseline_results:
402
+ base_data = json.loads(Path(args.baseline_results).read_text(encoding="utf-8"))
403
+ ref_marginal = base_data.get("runs", {}).get("latency_cpu", {}).get("marginal_s_per_doc_bs1")
404
+ if ref_marginal:
405
+ timing["baseline_marginal_s"] = ref_marginal
406
+ timing["delta_pct"] = round(
407
+ (timing["marginal_s_per_doc"] - ref_marginal) / ref_marginal * 100.0, 2
408
+ )
409
+
410
+ if args.json:
411
+ _emit_output(json.dumps(timing, indent=2), args)
412
+ else:
413
+ print(
414
+ f"2-Size Latency Benchmark (quantize={args.quantize}, dtype={args.dtype}, bs={args.batch_size}):\n"
415
+ f" T({args.small_n})={timing['small_s']}s, T({args.large_n})={timing['large_s']}s\n"
416
+ f" Load={timing['model_load_s']}s | Marginal={timing['marginal_s_per_doc']}s/doc "
417
+ f"({timing['items_per_min']} docs/min)"
418
+ )
419
+
420
+
421
+ def cmd_convert_safetensors(args):
422
+ from ncm_bench.inference import export_safetensors
423
+
424
+ paths = export_safetensors(
425
+ task=args.task,
426
+ root_dir=ROOT_DIR,
427
+ verify_checksums=not args.skip_checksum,
428
+ )
429
+ print(json.dumps({k: str(v) for k, v in paths.items()}, indent=2))
430
+
431
+
432
+ def cmd_train_smoke(args):
433
+ import torch
434
+
435
+ from ncm_train import Stage1Config, Stage2Config, train_stage1_vqvae, train_stage2_ncm
436
+
437
+ torch.manual_seed(42)
438
+ # Deterministic synthetic structured sequence-reversal dataset (real gradients, zero mocks)
439
+ n_samples = args.samples
440
+ L = 16
441
+ vocab = 16
442
+ prefixes = torch.randint(1, vocab, (n_samples, L))
443
+ targets = torch.flip(prefixes, dims=[1])
444
+
445
+ s1 = Stage1Config(
446
+ in_vocab_size=vocab,
447
+ context_length=L,
448
+ embed_dim=64,
449
+ levels=(2, 4, 8, 16),
450
+ vq_vocab_size=32,
451
+ n_layers=2,
452
+ batch_size=min(16, n_samples),
453
+ steps=args.s1_steps,
454
+ device="cpu",
455
+ )
456
+ res1 = train_stage1_vqvae(targets, s1, out_path=Path(args.out_dir) / "tokenizer.pt")
457
+
458
+ s2 = Stage2Config(
459
+ prefix_len=L,
460
+ n_layer=2,
461
+ n_head=4,
462
+ n_embd=64,
463
+ batch_size=min(16, n_samples),
464
+ steps=args.s2_steps,
465
+ device="cpu",
466
+ )
467
+ res2 = train_stage2_ncm(
468
+ prefixes,
469
+ targets,
470
+ res1["model"],
471
+ s1,
472
+ s2,
473
+ out_path=Path(args.out_dir) / "generator.pt",
474
+ )
475
+
476
+ with torch.inference_mode():
477
+ codes = res2["model"].generate(None, prefixes[:2], temperature=0.0)
478
+ recon_logits = res1["model"].decode(codes)
479
+ assert recon_logits.shape[:2] == (2, L)
480
+
481
+ print(
482
+ json.dumps(
483
+ {
484
+ "stage1_initial_loss": round(res1["history"][0]["loss"], 4),
485
+ "stage1_final_loss": round(res1["final_loss"], 4),
486
+ "stage2_initial_loss": round(res2["history"][0]["loss"], 4),
487
+ "stage2_final_loss": round(res2["final_loss"], 4),
488
+ "out_dir": str(Path(args.out_dir).resolve()),
489
+ },
490
+ indent=2,
491
+ )
492
+ )
493
+
494
+
495
+ def cmd_verify(args):
496
+ from scripts.fetch_artifacts import load_checksums, sha256_file
497
+
498
+ artifacts_only = bool(getattr(args, "artifacts_only", False))
499
+ sums = load_checksums()
500
+ if not sums:
501
+ print("Error: SHA256SUMS file not found or empty.", file=sys.stderr)
502
+ sys.exit(1)
503
+ entries = (
504
+ {k: v for k, v in sums.items() if k.startswith("artifacts/")}
505
+ if artifacts_only
506
+ else sums
507
+ )
508
+ all_ok = True
509
+ print(f"Verifying {len(entries)} artifact checksums...")
510
+ for rel_path, exp_hash in entries.items():
511
+ p = ROOT_DIR / rel_path
512
+ if not p.exists():
513
+ print(f" [MISSING] {rel_path}")
514
+ all_ok = False
515
+ continue
516
+ actual = sha256_file(p)
517
+ if actual == exp_hash:
518
+ print(f" [OK] {rel_path}")
519
+ else:
520
+ print(f" [FAILED] {rel_path} (expected {exp_hash[:10]}..., got {actual[:10]}...)")
521
+ all_ok = False
522
+ if all_ok:
523
+ print("\nAll artifact checksums successfully verified against pinned SHA256SUMS!")
524
+ else:
525
+ print("\nVerification failed.", file=sys.stderr)
526
+ sys.exit(1)
527
+
528
+
529
+ def _add_common_flags(p, *, with_io: bool = True):
530
+ if with_io:
531
+ p.add_argument("--input-file", "-f", default=None, help="Read input from .txt/.md/.json/.pdf")
532
+ p.add_argument("--output-file", "-o", default=None, help="Write output payload to file")
533
+ p.add_argument("--quantize", choices=["none", "int8"], default="none")
534
+ p.add_argument("--dtype", choices=["fp32", "fp16", "bf16"], default="fp32")
535
+ p.add_argument("--device", default=None)
536
+ p.add_argument("--skip-checksum", action="store_true", help="Skip SHA-256 verification before load")
537
+ p.add_argument("--strict-checksums", action="store_true", help="Fail closed on unlisted artifacts")
538
+ p.add_argument("--compile", action="store_true", help="Apply torch.compile optimization")
539
+
540
+
541
+ def main():
542
+ parser = argparse.ArgumentParser(
543
+ prog="ncm-bench",
544
+ description="NCM Sovereign Inference Engine, Training Suite, Benchmark CLI & Inspector",
545
+ )
546
+ sub = parser.add_subparsers(dest="command", required=True)
547
+
548
+ # serve
549
+ p_serve = sub.add_parser("serve", help="Run local REST API server")
550
+ p_serve.add_argument("--port", type=int, default=8031)
551
+ p_serve.add_argument("--host", default="127.0.0.1")
552
+ p_serve.add_argument("--uds", default=None, help="Bind to a Unix domain socket path instead of TCP")
553
+ p_serve.add_argument("--max-queue-depth", type=int, default=8)
554
+ p_serve.add_argument("--request-timeout-s", type=float, default=60.0)
555
+ p_serve.add_argument("--max-body-bytes", type=int, default=2_000_000)
556
+ p_serve.add_argument("--rate-limit-rpm", type=int, default=0, help="Requests/min per client IP (0=disabled)")
557
+ p_serve.add_argument("--with-sudoku", action="store_true", help="Also load sudoku/hard model for POST /sudoku")
558
+ _add_common_flags(p_serve, with_io=False)
559
+ p_serve.set_defaults(func=cmd_serve)
560
+
561
+ # summarize
562
+ p_sum = sub.add_parser("summarize", help="Summarize text or document file offline")
563
+ p_sum.add_argument("--text", "-t", default=None, help="Input text to summarize")
564
+ p_sum.add_argument("--long-doc", action="store_true", help="Enable map-reduce chunked summarization")
565
+ p_sum.add_argument("--redact-pii", action="store_true", help="Scrub PII before encoding")
566
+ p_sum.add_argument("--restore-pii", action="store_true", help="Restore PII placeholders in output")
567
+ p_sum.add_argument("--audit-receipt", action="store_true", help="Include SHA-256 compliance audit receipt")
568
+ p_sum.add_argument("--json", action="store_true", help="Output JSON response")
569
+ _add_common_flags(p_sum, with_io=True)
570
+ p_sum.set_defaults(func=cmd_summarize)
571
+
572
+ # mbr
573
+ p_mbr = sub.add_parser("mbr", help="Run MBR consensus decoding")
574
+ p_mbr.add_argument("--text", "-t", default=None, help="Input text to summarize")
575
+ p_mbr.add_argument("--candidates", "-k", type=int, default=4)
576
+ p_mbr.add_argument("--temperature", type=float, default=0.7)
577
+ p_mbr.add_argument("--utility", choices=["rougeL", "jaccard", "cosine"], default="rougeL")
578
+ p_mbr.add_argument("--json", action="store_true")
579
+ _add_common_flags(p_mbr, with_io=True)
580
+ p_mbr.set_defaults(func=cmd_mbr)
581
+
582
+ # inspect
583
+ p_insp = sub.add_parser("inspect", help="Progressively inspect concept hierarchy (Levels 0..8)")
584
+ p_insp.add_argument("--text", "-t", default=None, help="Input text")
585
+ p_insp.add_argument("--html", help="Path to write standalone HTML visualizer slider")
586
+ p_insp.add_argument("--json", action="store_true")
587
+ _add_common_flags(p_insp, with_io=True)
588
+ p_insp.set_defaults(func=cmd_inspect)
589
+
590
+ # sudoku
591
+ p_sud = sub.add_parser("sudoku", help="Solve Sudoku puzzles or run constraint-verified evaluation")
592
+ p_sud.add_argument("--puzzle", "-p", default=None, help="81-char Sudoku puzzle (0, ., or _ for blanks)")
593
+ p_sud.add_argument("--difficulty", choices=["easy", "medium", "hard"], default="hard")
594
+ p_sud.add_argument("--n", type=int, default=20)
595
+ p_sud.add_argument("--batch-size", type=int, default=16)
596
+ p_sud.add_argument("--best-of-k", type=int, default=1, help="Resample failing boards up to K candidates")
597
+ p_sud.add_argument("--temperature", type=float, default=0.4)
598
+ p_sud.add_argument("--no-repair", action="store_true", help="Disable post-prediction Sudoku constraint repair")
599
+ p_sud.add_argument("--json", action="store_true")
600
+ _add_common_flags(p_sud, with_io=True)
601
+ p_sud.set_defaults(func=cmd_sudoku)
602
+
603
+ # translate
604
+ p_tr = sub.add_parser("translate", help="Translate German to English via NCM WMT14")
605
+ p_tr.add_argument("--text", "-t", default=None)
606
+ p_tr.add_argument("--json", action="store_true")
607
+ _add_common_flags(p_tr, with_io=True)
608
+ p_tr.set_defaults(func=cmd_translate)
609
+
610
+ # benchmark
611
+ p_bench = sub.add_parser("benchmark", help="Run timed latency or accuracy benchmarks")
612
+ p_bench.add_argument("--task", choices=["latency", "sudoku"], default="latency")
613
+ p_bench.add_argument("--small-n", type=int, default=4)
614
+ p_bench.add_argument("--large-n", type=int, default=16)
615
+ p_bench.add_argument("--n", type=int, default=20)
616
+ p_bench.add_argument("--batch-size", type=int, default=1)
617
+ p_bench.add_argument("--baseline-results", default=None, help="Compare against baseline results.json")
618
+ p_bench.add_argument("--json", action="store_true")
619
+ _add_common_flags(p_bench, with_io=False)
620
+ p_bench.set_defaults(func=cmd_benchmark)
621
+
622
+ # convert-safetensors
623
+ p_st = sub.add_parser("convert-safetensors", help="Convert verified .pt weights to zero-pickle .safetensors")
624
+ p_st.add_argument("--task", default="xsum", help="Artifact subdirectory under artifacts/ (e.g. xsum, sudoku/hard)")
625
+ p_st.add_argument("--skip-checksum", action="store_true")
626
+ p_st.set_defaults(func=cmd_convert_safetensors)
627
+
628
+ # train-smoke
629
+ p_ts = sub.add_parser("train-smoke", help="Run clean-room Stage-1 + Stage-2 NCM training smoke check")
630
+ p_ts.add_argument("--samples", type=int, default=32)
631
+ p_ts.add_argument("--s1-steps", type=int, default=10)
632
+ p_ts.add_argument("--s2-steps", type=int, default=10)
633
+ p_ts.add_argument("--out-dir", default="artifacts/train_smoke")
634
+ p_ts.set_defaults(func=cmd_train_smoke)
635
+
636
+ # verify
637
+ p_ver = sub.add_parser("verify", help="Verify model and data SHA256 checksums")
638
+ p_ver.add_argument(
639
+ "--artifacts-only",
640
+ action="store_true",
641
+ help="Verify pinned model weights under artifacts/ only (skip optional data/ datasets)",
642
+ )
643
+ p_ver.set_defaults(func=cmd_verify)
644
+
645
+ args = parser.parse_args()
646
+ args.func(args)
647
+
648
+
649
+ if __name__ == "__main__":
650
+ main()