"""Render the four-arm comparison as a self-contained booth page."""
import json, html, re, sys, statistics as st
from pathlib import Path
SP = Path("/tmp/claude-1000/-home-lkraven-development-eshpfi-management/d4d5ad1b-76a2-4698-9684-ea6045592d22/scratchpad")
d = json.loads((SP / "booth-cells.json").read_text(encoding="utf-8"))
OUT = Path(sys.argv[1])
COLS = [("voices-base-1200", "control", "unadapted carrier"),
("lv-bronte", "Brontë", "lv-bronte · ckpt475"),
("lv-yarros", "Yarros", "lv-yarros"),
("lv-hemingway", "Hemingway", "lv-hemingway · ckpt850")]
SEEDS = [1234, 5678]
def split(t):
if "" in t and "" in t:
return t.split("", 1)[1].split("", 1)[0], t.split("", 1)[1].strip()
if "" in t:
return t.split("", 1)[1], ""
return "", t.strip()
def paras(text):
out = []
for p in re.split(r"\n\s*\n", text.strip()):
p = " ".join(p.split())
if p:
out.append("
" + html.escape(p) + "
")
return "\n".join(out) or '
— the budget ran out before any prose was written —
'
# headline numbers, computed not asserted
short_base = [len(split(d["cells"][f"voices-base|{b}|{s}"]["text"])[1].split())
for b, _ in d["beats"] for s in SEEDS]
stats = {}
for model, label, _ in COLS:
w = [len(split(d["cells"][f"{model}|{b}|{s}"]["text"])[1].split())
for b, _ in d["beats"] for s in SEEDS]
th = [len(split(d["cells"][f"{model}|{b}|{s}"]["text"])[0].split())
for b, _ in d["beats"] for s in SEEDS]
stats[model] = (int(st.median(w)), int(st.median(th)), sum(1 for x in w if x == 0))
blocks = []
for bid, beat in d["beats"]:
cells = []
for model, label, sub in COLS:
panes = []
for s in SEEDS:
th, prose = split(d["cells"][f"{model}|{bid}|{s}"]["text"])
tw = len(th.split())
badge = (f'planned {tw}w first' if tw > 0
else 'straight to prose')
panes.append(
f'
'
f'
seed {s} · {len(prose.split())}w {badge}
'
f'
{paras(prose)}
')
cells.append(f'
'
f'
{html.escape(label)}'
f'{html.escape(sub)}
'
+ "".join(panes) + "
")
blocks.append(f'
{bid}'
f'{html.escape(beat)}
' + "".join(cells) + "
")
rows = "".join(
f"
{html.escape(l)}
{stats[m][0]}
{stats[m][1]}
"
f"
{stats[m][2]}/12
" for m, l, _ in COLS)
HTML = f"""
Four voices, one beat
Four voices, one beat
The same six beats and the same author-neutral prompt through the
unadapted carrier and the three lv-* LoRA adapters, all served from one
process on vllm-voices (fv-ml1 GPU 0 :8027). Only the adapter changes.
system — {html.escape(d["system"])}
user — BEAT: <the beat>
The prompt names no author, deliberately. Each adapter trained under a prompt
naming its own — driving all four with any one of those would hand that arm a hint the
others do not get, and the page would be measuring the prompt.
One disclosed asymmetry. Brontë and Hemingway trained on “a SHORT PASSAGE … may
run to several paragraphs”; Yarros trained on “ONE paragraph”. The neutral prompt uses
neither qualifier, so it sits slightly off-distribution for all three rather than for one.
The control gets a 4× larger token budget, and that is the fair thing to do.
At the gate's 320-token budget the carrier spends 181–257 words thinking and
5 of 12 cells never reach the prose at all. The three adapters emit an empty
think block in 12 of 12 — they learned to skip it. Publishing the starved control would
conflate voice with budget discipline, so the control here runs at 1200 tokens and
finishes every time.
Two seeds per cell, because one sample of a sampled process is an anecdote.
Sampler matches the gate harness: temperature 0.9, top_p 0.95.
These 36 adapter generations were checked for verbatim reuse before this page went
up, each arm against its own training corpus (scoring one author against another
returns a clean zero that only means “different book”). Brontë 0, Yarros
0, Hemingway 2 of 12, longest run 8 words — and the run is
“I don't know, I don't know”. Nothing on this page reproduces anything worth
reproducing.
This is a reading, not a measurement. The numbers that decide anything are in the
gate: persistent-memory.d/2026-09-17-lv-hemingway-gate.md.
arm
median prose
median planning
never wrote prose
{rows}
column order is fixed: control · Brontë · Yarros · Hemingway
{''.join(blocks)}
"""
OUT.write_text(HTML, encoding="utf-8")
print(f"wrote {OUT} ({len(HTML):,} bytes)")
for m, l, _ in COLS:
print(f" {l:<11} median prose {stats[m][0]:>4}w · median planning {stats[m][1]:>4}w · empty {stats[m][2]}/12")