"""Render the base-vs-tuned voice A/B into a booth page. Layout is the argument. A flat gallery would let you read one arm at a time, which is exactly how you talk yourself into seeing a difference. So every prompt is one row, the arms are side by side, and BOTH seeds of each arm sit in the same cell -- so the within-arm variation is visible in the same glance as the between-arm variation. If the two base samples differ from each other as much as base differs from tuned, there is nothing here, and the layout should make that obvious rather than hide it. Prompts are ordered by tier, hardest first: modern/mundane, then period-neutral, then Victorian-adjacent. The modern tier is the one that matters -- Brontë showing up there is the adapter's doing, whereas Brontë showing up in the period tier could just be the prompt. """ import html import json import sys from collections import defaultdict from pathlib import Path base_f, tuned_f, out_dir = sys.argv[1], sys.argv[2], Path(sys.argv[3]) out_dir.mkdir(parents=True, exist_ok=True) def load(p): d = defaultdict(dict) for line in Path(p).read_text(encoding="utf-8").splitlines(): if not line.strip(): continue r = json.loads(line) d[r["id"]][r["seed"]] = r return d base, tuned = load(base_f), load(tuned_f) ids = [i for i in base if i in tuned] TIER = {"modern": ("Tier A — modern / mundane", "Nothing here invites Victorian prose. Brontë in this tier is the adapter's doing."), "neutral": ("Tier B — period-neutral", "Could be any century. A voice shift shows cleanly without the prompt supplying it."), "period": ("Tier C — Victorian-adjacent, plainly worded", "The setting leans period but the diction does not. Easiest tier; weakest evidence.")} order = {"modern": 0, "neutral": 1, "period": 2} ids.sort(key=lambda i: (order.get(base[i][list(base[i])[0]]["tier"], 9), i)) def cell(rec_by_seed): parts = [] for seed in sorted(rec_by_seed): t = (rec_by_seed[seed]["continuation"] or "").strip() parts.append(f'
seed {seed}' f'

{html.escape(t) or "(empty)"}

') return "".join(parts) rows, seen = [], set() for i in ids: any_rec = base[i][list(base[i])[0]] tier = any_rec["tier"] if tier not in seen: seen.add(tier) title, sub = TIER.get(tier, (tier, "")) rows.append(f'

{html.escape(title)}

{html.escape(sub)}

') rows.append(f"""
{html.escape(i)}{html.escape(any_rec["prompt"])}

Base Qwen3-0.6B-Base, no adapter

{cell(base[i])}

Tuned + H02 LoRA, 1 epoch, seed 4919

{cell(tuned[i])}
""") page = f"""BabyBronte — voice A/B

BabyBronte — did the voice move?

Same prompts, same sampler, same box, same seeds. The only difference between the columns is the H02 LoRA adapter (Charlotte Brontë, 680k words, 1 epoch, seed 4919).

Read this as an eyeball test, not a result. Two samples per arm per prompt is enough to see whether the gap between the columns is bigger than the gap between the two seeds inside a column — and not enough for anything else. No scoring, no statistics. The frozen adjudication rule and the Burrows's-Delta instrument are untouched by this page and nothing here feeds them.

Both arms are Qwen3-0.6B-Base doing continuation, not instruction-following. The adapter was trained as pure continuation, so each prompt is an opening line the model carries on from — asking a base model to "rewrite this in Brontë's voice" would test instruction-following instead of voice.
{''.join(rows)}
""" (out_dir / "index.html").write_text(page, encoding="utf-8") print(f"wrote {out_dir/'index.html'} ({len(ids)} prompts x 2 arms x 2 seeds)")