"""Build the BabyYarros evaluation booth: voice A/B + beat->paragraph + delta_cb. Reads the eval jsonls and the voice_distance summary, emits a self-contained index.html for the Booth. Three panels: A VOICE — each opening line, the three arms' continuations side by side, so the operator can SEE whether the adapter pulls arbitrary prose toward Yarros. B BEAT -> PARAGRAPH — the Skaldsong question: does the Instruct arm still take direction (on-beat / in-band / ran-on) after training on raw Yarros text. C delta_cb — the seat-free relative measure, with its A-vs-A noise floor. """ from __future__ import annotations import html, json, re, sys from pathlib import Path D = Path(sys.argv[1]) # yarros-eval dir DIST = Path(sys.argv[2]) if len(sys.argv) > 2 else None # distance stdout captured to a file OUT = Path(sys.argv[3]) if len(sys.argv) > 3 else (D / "index.html") def rows(f): p = D / f return [json.loads(l) for l in p.read_text(encoding="utf-8").splitlines()] if p.exists() else [] ARMS = [("base-unadapted", "Base · no adapter (control)"), ("base-125-tuned", "Base · Yarros LoRA (ckpt-125)"), ("instruct-tuned", "Instruct · Yarros LoRA")] voice = {} for key, _ in ARMS: for r in rows(f"voice.{key}.jsonl"): voice.setdefault((r["id"], r["seed"]), {})[key] = r prompts = {} for key, _ in ARMS: for r in rows(f"voice.{key}.jsonl"): prompts[r["id"]] = (r["tier"], r["prompt"]) def wc(t): return len(t.split()) def beat_flags(r): kh, kws = r.get("keyword_hits", 0), r.get("beat_keywords", []) ratio = kh / max(len(kws), 1) on_beat = kh >= 1 and ratio >= 0.34 # at least a third of the beat's content words rendered return on_beat, f"{kh}/{len(kws)}", r.get("in_band"), r.get("ran_on"), r.get("words", 0) beats = rows("beats.instruct.jsonl") esc = lambda s: html.escape(s or "") parts = ["""BabyYarros — voice & beats

BabyYarros — does the voice transfer, can it do beats?

Qwen3-4B, one epoch on the leak-gated Yarros corpus. Base LoRA = ckpt-125 (its held-out minimum). Generated on gx10, same harness per arm. Not the frozen adjudication — the voice A/B and beat test the operator asked to see.

"""] # Panel A — voice parts.append('

A · Voice — arbitrary opening line, three arms continue it

') order = ["a1","a2","a3","b1","b2","b3","c1","c2","c3"] seedpick = 1234 for pid in order: if pid not in prompts: continue tier, ptext = prompts[pid] parts.append(f'
{esc(ptext)} · {tier}
') cell = voice.get((pid, seedpick), {}) for key, label in ARMS: r = cell.get(key) body = esc(r["continuation"].strip())[:1400] if r else "—" w = wc(r["continuation"]) if r else 0 parts.append(f'
{esc(label)}
{body}
{w} words
') parts.append('
') # Panel B — beats parts.append('

B · Beat → paragraph (Instruct, chat template) — the Skaldsong question

') parts.append('

Can the Instruct arm still take direction after training on raw Yarros continuation text? Each beat expanded to one paragraph in Yarros\' voice.

') if beats: parts.append('') for r in beats: ob, kw, ib, ro, w = beat_flags(r) yn = lambda v,good: (f'{"yes" if v else "no"}' if v==good else f'{"yes" if v else "no"}') if v is not None else '—' parts.append(f'' f'' f'' f'') parts.append('
beatseedon-beat (kw)in-band 90–140ran-onwords
{esc(r.get("beat",r.get("id","")))[:64]}{r.get("seed","")}{("yes" if ob else "no")} {kw}{yn(ib,True)}{yn(ro,False)}{w}
') # show the actual paragraphs for r in beats[:6]: para = esc(r.get("paragraph", r.get("text","")).strip())[:1400] parts.append(f'
{esc(r.get("beat",""))}
{para}
') else: parts.append('

beats file not present

') # Panel C — delta_cb parts.append('

C · delta_cb — did the adapter move the voice toward held-out Yarros?

') if DIST and DIST.exists(): parts.append(f'
{esc(DIST.read_text())}
') else: parts.append('

distance summary not present

') OUT.write_text("\n".join(parts), encoding="utf-8") print(f"wrote {OUT} ({OUT.stat().st_size} bytes)")