Fuse rhyme groups that share a vowel family

Perfect subgroups (shoulder/older/colder, coaster/roaster/toaster) no
longer split colors with the slant family around them (soldier/
holster): groups with the same multisyllabic vowel projection merge,
perfect members keep strong styling, slant members keep their marks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 02:03:19 -04:00
parent b0c4af345a
commit e33bf58b11
2 changed files with 41 additions and 0 deletions
+25
View File
@@ -547,6 +547,31 @@ def analyze(draft: Draft):
raw_groups[gi]["toks"].append(p)
grouped.add(id(p))
# fuse groups that carry the same vowel family — a perfect subgroup
# (shoulder/older/colder) shouldn't split colors with the slant family
# it lives inside (soldier/holster/coaster). Perfect members keep the
# strong styling; the slant side keeps per-token slant marks.
by_family: dict[str, dict] = {}
fused: list[dict] = []
for g in raw_groups:
mk = founding_projections(g["key"]).get("multi")
tgt = by_family.get(mk) if mk else None
if tgt is None:
if mk:
by_family[mk] = g
fused.append(g)
continue
if g["slant"]:
for t in g["toks"]:
t["slant"] = True
elif tgt["slant"]:
for t in tgt["toks"]:
t["slant"] = True
tgt["slant"] = False
tgt["key"] = g["key"]
tgt["toks"].extend(g["toks"])
raw_groups = fused
# stable colors: order groups by first appearance
raw_groups.sort(key=lambda g: min((t["line"], t["start"]) for t in g["toks"]))
groups_out = []
+16
View File
@@ -281,3 +281,19 @@ def test_annotation_lines_ignored():
(st,) = res["stanzas"]
assert st["lines"] == [1, 2, 5, 6]
assert st["scheme"] == "aabb"
def test_perfect_subgroup_fuses_with_slant_family():
# shoulder/older/colder (perfect, OW L D ER) live inside the bigger
# OW-schwa family — one color for the whole column
text = ("Skunk, bug, soldier\n"
"Tongue, shrub, shoulder\n"
"One month older\n"
"Sponge, mob, colder\n"
"Nun, rug, holster\n"
"Lug nut, coaster\n"
"Lung, jug, roaster\n"
"Young Thug poster\n"
"Unplugged toaster")
group_with(text, "soldier", "shoulder", "older", "colder", "holster",
"coaster", "roaster", "poster", "toaster")