"""The Skaldsong question, answered: can a beat sentence be expanded to a paragraph? The page is built as an argument rather than a gallery, because the result is a negative one on the adapted carrier and a negative result presented as a gallery reads as "some of these look fine". Panel A the adapted 4B across ten prompt formats -- what does not work, and why Panel B the same beats through an instruct model with a style prompt -- what does Panel C Panel B's paragraphs stitched, which is the deliverable Skaldsong wants Two artifacts in Panel A are worth their own callout: two formats leaked *pretraining task data* -- NLI multiple choice and a grammar-correction exercise -- which is a base-model failure mode that no amount of style adaptation removes. """ import html, json, statistics, sys from collections import defaultdict from pathlib import Path D = Path(sys.argv[1]) def load(f): rows = [json.loads(l) for l in (D / f).read_text(encoding="utf-8").splitlines() if l.strip()] d = defaultdict(list) for r in rows: d[r["format"]].append(r) return d, rows bake, rows1 = load("bakeoff.jsonl") bake2, rows2 = load("bakeoff2.jsonl") inst, rows3 = load("instruct.jsonl") bake.update(bake2) allrows = rows1 + rows2 FMT_NOTES = { "bare": "The beat alone. Continues the situation rather than expanding it, and leaves the kitten out.", "para-break": "⚠ Leaked pretraining task data — NLI multiple choice. A standalone sentence followed by a blank line looks exactly like a dataset entry.", "labelled": "Named the artifact. Produced abstract moralising about punishment and husbands.", "epigraph": "The beat in italics as an epigraph. Drifts immediately.", "fewshot": "One worked example. Echoed the beat with pronouns flipped, then drifted to unrelated gossip.", "fewshot-bare": "One example, no labels. Returned single lines of dialogue, one borrowing a character from the example itself.", "fewshot3": "Three worked examples. Still drifts — into a woman and her husband, a child, a nurse.", "elaborate": "Beat plus “It happened in this way.” Commits to elaborating and elaborates something else.", "recount": "Beat plus “I remember the whole of it.” Same.", "label-begin": "The letter prompt's winning move applied to a beat — label it and seed the opening words. Closest of the ten, and one seed leaked a grammar-correction exercise instead.", } def para_block(r): tag = f"{r['words']}w" + (" · ran on" if r.get("ran_on") else "") return (f'
seed {r["seed"]} · {tag}' f'

{html.escape(r["paragraph"].strip()) or "(empty)"}

') beat_one = allrows[0]["beat"] if allrows else "" panelA = "".join( f'
{html.escape(f)}' f'{FMT_NOTES.get(f, "")}
' f'
{"".join(para_block(r) for r in bake[f])}
' for f in FMT_NOTES if f in bake) by_beat = defaultdict(list) for r in rows3: by_beat[r["id"]].append(r) panelB = "".join( f'
{html.escape(bid)}' f'{html.escape(rs[0]["beat"])}
' f'{"".join(para_block(r) for r in rs)}
' for bid, rs in sorted(by_beat.items())) stitched = "\n\n".join(r["paragraph"].strip() for bid, rs in sorted(by_beat.items()) for r in rs if r["seed"] == 1234) wl = [r["words"] for r in rows3] page = f"""Beat to paragraph

Beat → paragraph: can the adapter do Skaldsong's job?

Skaldsong wants to write story beats as single sentences, have a model expand each into a paragraph, and stitch the paragraphs into a passable story. That is a narrower job than free-form continuation, and it fails differently.

Four ways this job breaks, all of which had to be measured rather than eyeballed. Drift off the beat breaks the stitch, because the next paragraph no longer follows. Run-on breaks it too — the deliverable is a paragraph, and the following scene belongs to the next beat. Framing renders nothing at all ("I told it briefly"). Renaming is a live blocker: the entity-rename pool taught the adapter that character names come from it, so a caller's own name can be rewritten mid-passage.

Panel A — the adapted 4B, ten prompt formats

One beat, three seeds each, thirty samples. The beat is "{html.escape(beat_one)}". Read as many as you like; the finding is that none of them render it.

{panelA}
Ten formats, thirty samples, none that reliably expand the beat. The adapter writes Brontë well — that is settled elsewhere — but "write a paragraph about this sentence" is an instruction, and a completion model has no mechanism for about. It continues the text it is given. Two formats did something worse than drift and leaked pretraining task data: an NLI multiple-choice item and a grammar-correction exercise. That is a base-model artifact which no amount of style adaptation removes.

Panel B — the same beats through an instruct model

The gen seat (Qwen3.8-27B, post-trained, no Brontë adapter) with a style instruction asking for one paragraph of 90–140 words in her manner. Five beats, two seeds.

{panelB}
It takes direction perfectly and has the wrong voice. All {len(wl)} samples landed inside the requested band — {min(wl)}–{max(wl)} words, median {statistics.median(wl):.0f} — every one stayed on its beat, and none drifted into a following scene. But the prose is generic literary pastiche rather than Brontë: abstract-noun-heavy, fond of aphoristic openers ("There is a peculiar, chilling stillness that attends the discovery of a life nearly spent"), and it over-writes. Brontë is more concrete and more sharply observed than this.

Panel C — Panel B's paragraphs, stitched

The deliverable shape, so the failure modes of stitching are visible too. Each paragraph was generated independently, which is itself the next problem: watch the point of view slide between beats — by the fourth the narrator is both watching the girl and carrying the animals. A real stitcher has to feed prior paragraphs back as context.

{html.escape(stitched)}
The conclusion, and it settles an architecture question. The adapted completion carrier has the voice and cannot take direction. The instruct model takes direction and has no voice. Skaldsong's job needs both, which means the corpus has to be rebuilt as instruction→response pairs and trained onto an instruct carrier — not more prompt cleverness, which is now ten formats deep with nothing to show. This applies to Yarros identically: the carrier question is orthogonal to the author, so the next corpus does not need to re-run this experiment.
""" (D / "index.html").write_text(page, encoding="utf-8") print(f"wrote {D/'index.html'} (panel A {sum(len(v) for v in bake.values())} samples, " f"panel B {len(rows3)}, stitched {len(stitched.split())} words)")